import { cloneDeep, isEqual, isPlainObject } from 'lodash'; /** * If path is 'a.b.c', returns the value of object.a.b.c */ const getPropertyAtPath = (object: Record, path: string) => { const pathSegments = path.split('.'); return pathSegments.reduce((current, prop) => { if (current === undefined) { return undefined; } return current[prop]; }, object); }; /** * Returns the path to the property that should be used to match the objects in the array. */ const getPathToMatchObjects = (object: Record) => { const pathsToMatchBy = [ 'key', // schemas, fulfillment types, alteration hooks, embed config (screening questions) 'functionName', // scheduled functions 'outputPathList', // schemas (list component) 'type', // settings.policyDocuments 'label', // embed config (wording summary) 'block.key', // claims blocks schema, group blocks 'id', // api docs ]; for (const path of pathsToMatchBy) { if (getPropertyAtPath(object, path) !== undefined) { return path; } } return undefined; }; /** * Recursively sorts the keys of an object to match the key order of a target object. * If the object contains an array of objects, the keys of the objects in the array will also sorted. */ export const sortObjectKeysToMatchTarget = (params: { objectToSort: Record; targetObject: Record; }) => { const { objectToSort, targetObject } = params; // If no target object exists return the object as is if (!targetObject) { return objectToSort; } // We will use this copy to retain properties for which we do not find a match in the target object const objectToSortCopy = cloneDeep(objectToSort); const sortedProperties: Record = {}; // Iterate over the keys of the target object for (const key of Object.keys(targetObject)) { // Check if the object to sort has a property with the same key if (Object.prototype.hasOwnProperty.call(objectToSort, key)) { const value = objectToSort[key]; // Remove the property from the copy so that we can add the remaining properties at the end delete objectToSortCopy[key]; // Check if the value is an array of objects if (Array.isArray(value) && Array.isArray(targetObject[key]) && isPlainObject(value[0])) { sortedProperties[key] = sortObjectKeysInArray({ arrayToSort: value, targetArray: targetObject[key] }); } else if (isPlainObject(value)) { sortedProperties[key] = sortObjectKeysToMatchTarget({ objectToSort: value, targetObject: targetObject[key], }); } else { sortedProperties[key] = value; } } } // Add the non-matching properties to the output object // Return a deep copy to avoid returning a reference to the original object const sortedObject = cloneDeep({ ...sortedProperties, ...objectToSortCopy }); return sortedObject; }; /** * Sorts the keys of the objects in an array to match the key order of a matching object in the target array. */ export const sortObjectKeysInArray = (params: { arrayToSort: Record[]; targetArray: Record[]; // path?: string; }) => { const { arrayToSort, targetArray } = params; // Make a deep copy to avoid mutating the original array const targetArrayCopy = cloneDeep(targetArray); const outputArray: Record[] = []; for (const object of arrayToSort) { // Search function defaults to checking for deep equality let searchFunction = (targetObject: Record) => isEqual(object, targetObject); // Check if the object has a property that can be used to match it to a target object const path = getPathToMatchObjects(object); if (path) { // If we found a matching property, use this for the search function searchFunction = (targetObject: Record) => getPropertyAtPath(object, path) === getPropertyAtPath(targetObject, path); } // Find the index of the first matching object in the target array const targetIndex = targetArrayCopy.findIndex(searchFunction); // Below will evaluate to undefined if targetIndex is -1 const targetObject = targetArrayCopy[targetIndex]; // If a match is found, sort the object keys to match the target object if (targetObject) { // Remove the matching object from the target array so that we do not match on it again targetArrayCopy.splice(targetIndex, 1); outputArray.push(sortObjectKeysToMatchTarget({ objectToSort: object, targetObject })); } else { outputArray.push(object); } } // Return a deep copy to avoid returning a reference to the original array return cloneDeep(outputArray); };