import * as JSONPath from 'jsonpath'; type FilterFunc = (path: string, object: any) => boolean; type FilterSelector = string | string[] | FilterFunc export interface DeepSyncOptions { /** * Accepts a list of JSONPath patterns. * Any objects that are set at those paths are directly stored as * references instead of being duplicated into an observable object. */ refs?: FilterSelector, /** * Accepts a list of JSONPath patterns. * Any objects that are set at those paths will not be included * in the duplicated object. */ exclude?: FilterSelector, } export function deep_sync_adv(target: any, obj: T, options: DeepSyncOptions = {}): T { deepSyncInternal(target, obj, { refPaths: normalizeFilter(options.refs, obj), excludePaths: normalizeFilter(options.exclude, obj), currentPath: ['$'], delete_missing: true, }) return target } export function deep_merge_adv(target: T, obj: T, options: DeepSyncOptions = {}): T { deepSyncInternal(target, obj, { refPaths: normalizeFilter(options.refs, obj), excludePaths: normalizeFilter(options.exclude, obj), currentPath: ['$'], delete_missing: false, }) return target } export function deep_duplicate_adv(obj: T, options: DeepSyncOptions = {}): T { const target = Array.isArray(obj) ? [] : {}; return deep_merge_adv(target, obj, options) as T; } export function deepSyncInternal(target: any, source: any, options: { refPaths: FilterFunc, excludePaths: FilterFunc, currentPath: string[], delete_missing?: boolean, initializeMissingValue?: (v: any, recurse: (target: any) => any) => any, }) { const initMissingValue = options.initializeMissingValue || ((v, recurse) => { const dest = Array.isArray(v) ? [] : {}; return recurse(dest) }) for (let [k, v] of Object.entries(source)) { const currentPath = [...options.currentPath, k]; const currentPathSpec = JSONPath.stringify(currentPath); if (options.excludePaths(currentPathSpec, v)) continue; const recurse = (dest) => { deepSyncInternal(dest, v, { ...options, currentPath, }); return dest; } if (options.refPaths(currentPathSpec, v) || typeof v != 'object' || v == null) { target[k] = v; } else if (Object.getPrototypeOf(v) != Object.prototype && !Array.isArray(v)) { target[k] = v; } else if (typeof v != typeof target[k] || target[k] === v) { target[k] = initMissingValue(v, recurse); } else if (Array.isArray(v) && Array.isArray(target[k])) { recurse(target[k]) } else if (target[k] == null || (Object.getPrototypeOf(target[k]) != Object.prototype)) { target[k] = initMissingValue(v, recurse); } else { recurse(target[k]) } } if (options.delete_missing !== false) { const keys = Object.keys(target); for (let k of keys) { if (!(k in source)) { if (Array.isArray(target)) target.splice(k as any, 1); else delete target[k] } } } } export function normalizeFilter(spec: FilterSelector, obj): FilterFunc { if (typeof spec == 'function') return spec; if (typeof spec == 'string') spec = [spec]; const pathset = buildPathSets(obj, spec); return (path, obj) => pathset.has(path); } function buildPathSets(obj, paths: string[]) { const pathSet = new Set(); for (let path of paths || []) { const js_paths = JSONPath.paths(obj, path); for (let jsp of js_paths) { pathSet.add(JSONPath.stringify(jsp)); } } return pathSet; }