UNPKG

@paperbits/common

Version:
297 lines (240 loc) 9.61 kB
export function isObject(item: any): boolean { return item !== undefined && (item && typeof item === "object" && !Array.isArray(item)); } export function getObjectAt<T>(path: string, source: object, delimiter: string = "/"): T { const segments = path.split(delimiter); let segmentObject = source; for (const segment of segments) { segmentObject = segmentObject[segment]; if (segmentObject === null || segmentObject === undefined) { return <any>segmentObject; } } return <any>segmentObject; } /** * Applies changes to a target and returns reverse changeset that can be used to rollback changes. * @param target {object} Object to which changes are being applied. * @param changeset {object} Object that needs to be merged into targt object. * @param removeNulls {boolean} Indicates that properties with "null" values should be removed. * @returns {object} Reverse changeset. */ export function mergeDeep(target: Object, changeset: Object, removeNulls: boolean = false): Object { const reverseChangeset = {}; if (isObject(target) && isObject(changeset)) { Object.keys(changeset).forEach(key => { const sourceProperty = changeset[key]; if (isObject(sourceProperty)) { if (target[key] !== undefined && target[key] !== null) { reverseChangeset[key] = mergeDeep(target[key], sourceProperty, removeNulls); } else { // if not present in target, safely assign whole source. if (target[key] !== sourceProperty) { reverseChangeset[key] = target[key] || null; target[key] = sourceProperty; if (removeNulls) { if (target[key] === null || target[key] === undefined) { delete target[key]; } } } } } else { // if value or array, just assign as is. if (target[key] !== sourceProperty) { reverseChangeset[key] = target[key] || null; target[key] = sourceProperty; if (removeNulls) { if (target[key] === null || target[key] === undefined) { delete target[key]; } } } } }); } return reverseChangeset; } export function mergeDeepAt(path: string, target: any, source: any, removeNulls: boolean = false): void { if (Array.isArray(source)) { setValueWithCompensation(path, target, source); } else { const updatingObject = setStructure(path, target); mergeDeep(updatingObject, source, removeNulls); } } export function setStructure(path: string, target: object, delimiter: string = "/"): object { const segments = path.split(delimiter); let segmentObject = target; segments.forEach(segment => { if (!segmentObject[segment]) { segmentObject[segment] = {}; } segmentObject = segmentObject[segment]; }); return segmentObject; } export interface CleanupOptions { /** * If the node property value is null, the property gets removed from the node. */ removeNulls?: boolean; /** * If all child properties of the node are null, the node itself becomes null. */ collapseNulls?: boolean; /** * If the node property is an empty string, the property gets removed from the node. */ removeEmptyString?: boolean; } /** * Removes all properties with `undefined` value from object. * @param source {object} An object that needs to be cleaned. */ export function cleanupObject(source: object, options?: CleanupOptions): void { if (options?.removeNulls && options?.collapseNulls) { throw new Error(`Cannot remove and collapse nulls at the same time.`); } if (!(source instanceof Object)) { return; } Object.keys(source).forEach(key => { const childNode = source[key]; if (Array.isArray(childNode)) { childNode.forEach(x => cleanupObject(x, options)); if (childNode.length === 0) { source[key] = null; if (options?.removeNulls) { delete source[key]; } } } else if (childNode instanceof Object) { if (childNode === source) { console.log(`Ciruclar reference: `, source); throw new Error(`Ciruclar reference detected. See the message above.`); } cleanupObject(childNode, options); if (Object.keys(childNode).length === 0) { delete source[key]; } else if (options?.collapseNulls) { const hasNonNullProps = Object.values(childNode).some(x => x !== null); if (!hasNonNullProps) { // collapse empty nodes source[key] = null; } } } else if (childNode === undefined || (options?.removeNulls && childNode === null) || (options?.removeEmptyString && childNode === "")) { delete source[key]; } }); } /** * Sets value in an object tree and returns compensation tree. * @param path Object tree path. * @param target Object tree root. * @param value */ export function setValueWithCompensation(path: string, target: object, value: any): object { const original: any = clone(target); const compensation = getObjectAt(path, original); setValue(path, target, value); return <any>compensation; } /** * Sets value in an object tree. * @param path Object tree path. * @param target Object tree root. * @param value */ export function setValue(path: string, target: object, value: any): void { const segments = path.split("/"); let segmentObject = target; for (let i = 0; i < segments.length - 1; i++) { const segment = segments[i]; if (!segmentObject[segment]) { segmentObject[segment] = {}; } segmentObject = segmentObject[segment]; } segmentObject[segments[segments.length - 1]] = value; } export function deleteNodeAt(path: string, target: object): void { const segments = path.split("/"); let segmentObject = target; for (let i = 0; i < segments.length - 1; i++) { const segment = segments[i]; if (!segmentObject[segment]) { segmentObject[segment] = {}; } segmentObject = segmentObject[segment]; } delete segmentObject[segments[segments.length - 1]]; // TODO: Try to delete entire trail, if empty } export function clone<T>(obj: Object): T { return <T>JSON.parse(JSON.stringify(obj)); } export function findObjects(source: object, predicate: (node: object) => boolean): object[] { let result = []; const keys = Object.keys(source); for (const key of keys) { const node = source[key]; if (predicate(node)) { result.push(node); } else { const childResult = findObjects(node, predicate); result = result.concat(childResult); } } return result; } export function isEmpty(source: object): boolean { return !source || Object.keys(source).length === 0; } export function deepFreeze(obj: object): void { const propNames = Object.getOwnPropertyNames(obj); for (const name of propNames) { const value = obj[name]; if (value && typeof value === "object") { this.deepFreeze(value); } } Object.freeze(obj); } export function generateChangeset(target: Object, source: Object, collapseEmptyNodes: boolean = false): Object { const changeset = {}; if (!isObject(target) || !isObject(source)) { return changeset; } Object.keys(source).forEach(key => { const sourceProperty = source[key]; const targetProperty = target[key]; if (isObject(sourceProperty)) { if (targetProperty !== undefined && targetProperty !== null) { changeset[key] = generateChangeset(targetProperty, sourceProperty); if (collapseEmptyNodes && !Object.values(changeset[key]).some(x => x !== null)) { // collapse empty nodes empty nodes changeset[key] = null; } } else if (targetProperty !== sourceProperty && sourceProperty !== undefined) { // if not present in target, safely assign whole source. changeset[key] = sourceProperty; } } else if (targetProperty !== sourceProperty && sourceProperty !== undefined) { // if value or array, just assign as is. changeset[key] = sourceProperty || null; } }); Object.keys(target).forEach(key => { const targetProperty = target[key]; const sourceProperty = source[key]; if (targetProperty !== undefined && (sourceProperty === undefined || sourceProperty === null)) { changeset[key] = null; } }); return changeset; }