objectUtils.js

/**
 * Set of utils for working with any kind of objects in SFCC.
 * @module objectUtils
 */

const Logger = require('dw/system/Logger');

/**
 * Parses stringified JSON value. Never throws any exceptions,
 * but outputs JSON parsing errors into logs.
 * 
 * @param {string} jsonString - JSON string.
 * @returns {Object|null} Parsed JSON value or `null`.
 * 
 * @example
 * parseJSON('{ "countryCode":"CZ"}' );
 * // { "countryCode":"CZ" }
 */
const parseJSON = (jsonString) => {
  let jsonObject = null;
  try {
    jsonObject = JSON.parse(jsonString);
  }
  catch (jsonParsingError) {
    Logger.error(jsonParsingError);
  }
  return jsonObject;
};

/**
 * Retrieves a property from an object based on the provided path.
 * Never throws any exceptions, but outputs retrieving property errors into logs.
 * 
 * @param {Object} obj - The object from which to retrieve the property.
 * @param {string} path - The path to the desired property.
 * Use dot notation for object properties and square brackets
 * with indices for array elements (e.g., 'property[0].nested.property').
 * @returns {*} The value of the specified property if found;
 * otherwise, returns undefined.
 * 
 * @example
 * get(customer1, 'addressBook.preferredAddress.city');
 * get(product1, 'pageMetaTags[0].ID');
 */
const get = (obj, path) => {
  try {
    const properties = path.split('.');
    return properties.reduce((acc, current) => {
      const match = current.match(/(\w+)(\[(\d+)\])?/);
      if (match) {
        const key = match[1];
        const index = match[3];
        if (index !== undefined) {
          return acc && acc[key] ? acc[key][index] : undefined;
        } else {
          return acc && acc[key] ? acc[key] : undefined;
        }
      } else {
        return undefined;
      }
    }, obj);
  } catch (error) {
    Logger.error(error);
  }
};

/**
 * Creates a new object by picking specific properties from a source object
 * based on either a list of property names or a filtering function.
 * 
 * @param {Object} primaryObject - The source object from which 
 * properties will be picked.
 * @param {...(string|function)} args - Either an array of property 
 * names to pick, or a filtering function to determine inclusion.
 * @returns {Object} A new object containing only the selected properties
 * from the source object.
 * 
 * @example
 * Picking specific properties by name
 * pick({ id: 1, name: 'productName', size: 500 }, 'id', 'name');
 * { id: 1, name: 'productName'}
 * 
 * Picking properties based on a filtering function
 * pick({ id: 1, name: 'productName', size: 500 }, (key, value) => value >= 500);
 * { size: 500 }
 */
const pick = function() {
  const newObject = {};
  const primaryObject = arguments[0];
  const args = Array.prototype.slice.call(arguments);
  const argsToPick = args.slice(1);
  if (typeof args[1]  !== 'function' && Object.keys(primaryObject).length !== 0) {
      argsToPick.forEach(key => {
      newObject[key] = primaryObject[key];
    });
  } else {
    const filterFn = args[1];
    Object.keys(primaryObject).forEach(key => {
      const val = primaryObject[key];
      const toInclude = filterFn(key, val);
      if (toInclude) {
        newObject[key] = primaryObject[key];
      }
    });
  }
  
  return newObject;
};

/**
 * Recursively checks if two objects are equal.
 *
 * @param {Object} obj1 - The first object to compare.
 * @param {Object} obj2 - The second object to compare.
 * @returns {boolean} Returns `true` if the objects are  equal, `false` otherwise.
 * 
 * @example
 * isEqual({ id: 1, name: 'productName'}, { id: 1, name: 'productName'});
 * true
 * isEqual({ id: 1, category: { name: 'Other'}}, { id: 1, category: { name: 'Other'}});
 * true
 * isEqual({ id: 1}, { id: 1, category: { name: 'Other'}});
 * false
 */
const isEqual = (obj1, obj2) => {
  if (obj1 === obj2) {
    return true;
  }

  if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
    return false;
  }

  const obj1Keys = Object.keys(obj1);
  const obj2Keys = Object.keys(obj2);

  if (obj1Keys.length !== obj2Keys.length) {
    return false;
  }

  for (const key of obj1Keys) {
    if (!(key in obj2) || !isEqual(obj1[key], obj2[key])) {
      return false;
    }
  }

  return true;
};

/**
 * Deeply clones the given data.
 * 
 * @param {*} data - The data to be cloned.
 * @returns {*} Returns the deeply cloned data.
 * 
 * @example
 * const data = { id: 1, category: { name: 'Dresses' }};
 * const clone = deepClone(data);
 * data.category.name = 'Shoes';
 * clone.category.name;  // 'Dresses'
 */
const deepClone = (data) => {
  if (data === null || typeof data !== 'object') {
    return data;
  }

  const clonedData = Array.isArray(data) ? [] : {};

  Object.keys(data).forEach(key => {
    const value = data[key];
    clonedData[key] = deepClone(value);
 });

  return clonedData;
};

module.exports = {
  parseJSON,
  get,
  pick,
  isEqual,
  deepClone
};