UNPKG

datum-merge

Version:

Simplified diff and merging for deeply nested objects

204 lines (198 loc) 7.12 kB
import { concat, difference, differenceWith, intersection, intersectionWith, union, unionWith } from "lodash-es"; import { emptyValue, isArrayOf, isArrayOfAny, isNullish, isPrimitive, typeOfValue } from "./type-utils"; import { areArraysEqual, deepClone, deepEquals } from "./datum-utils"; export const UpdateCode = { T: "T", //touch, blank update C: "C", //create instance // scalar/def codes N: "N", //ignore change Y: "Y", //accept any change F: "F", //insert or update or cleanup B: "B", //insert or update, no delete U: "U", //update or delete only H: "H", //update only if exists I: "I", //insert only D: "D", //delete only // vector/xref codes XR: "XR", //full replace XM: "XM", //set union, vector merge XD: "XD", //set difference, delete given values XI: "XI", //set intersection, delete missing values XS: "XS", //preserve order insert (allows dupes) XF: "XF", //insert from front (allows dupes) } as const; export type MergeCode = typeof UpdateCode[keyof typeof UpdateCode]; export function mergeScalarField( target: any, source: any, label: string, mergeCode: MergeCode, ): boolean { const sourceHas = !isNullish(source[label]); const targetKey = Object.prototype.hasOwnProperty.call(target, label); const targetHas = targetKey && !isNullish(target[label]); if (!targetKey && !sourceHas) { return false; } if (targetHas && sourceHas) { //lhs vs rhs type check if (typeOfValue(target[label]) !== typeOfValue(source[label])) { throw new TypeError("scalar type mismatch for " + label); } if (target[label] === source[label]) { return false; } } let migrateVal = false; switch (mergeCode) { case UpdateCode.N: return false; case UpdateCode.Y: target[label] = deepClone(source[label]); return true; //should bypass case UpdateCode.F: migrateVal = true; break; //should cleanup case UpdateCode.B: if (sourceHas) migrateVal = true; break; case UpdateCode.U: if (targetHas) migrateVal = true; break; case UpdateCode.I: if (!targetHas) migrateVal = true; break; case UpdateCode.H: if (targetHas && sourceHas) migrateVal = true; break; case UpdateCode.D: if (targetHas && !sourceHas) migrateVal = true; break; case UpdateCode.XR: case UpdateCode.XS: case UpdateCode.XF: case UpdateCode.XM: case UpdateCode.XD: case UpdateCode.XI: return mergeVectorField(target, source, label, mergeCode); } if (!migrateVal) { return false; } target[label] = !sourceHas || isPrimitive(source[label]) ? source[label] : deepClone(source[label]); if (emptyValue(target[label])) { delete target[label]; return targetKey; } return true; }; export function mergeVectorField( target: any, source: any, label: string, mergeCode: MergeCode, ): boolean { let sourceVals = source[label]; if (isNullish(sourceVals)) { return false; //sourceHas } let sourceVec = isArrayOfAny(sourceVals); const targetKey = Object.prototype.hasOwnProperty.call(target, label); const targetHas = targetKey && !isNullish(target[label]); const targetVec = isArrayOfAny(target[label]); if (targetHas && !targetVec) { //blocks T => T[] type change throw new TypeError("type change to vector for " + label); } if (!sourceVec) { sourceVals = [sourceVals] as unknown[]; sourceVec = true; } //optimize common scenario const skipEquals = (!targetHas || isArrayOf(target[label], isPrimitive)) && isArrayOf(sourceVals, isPrimitive); let targetVals: unknown[]; switch (mergeCode) { case UpdateCode.N: return false; case UpdateCode.Y: target[label] = deepClone(sourceVals); return true; //should bypass case UpdateCode.XR: targetVals = [...sourceVals]; break; // case UpdateCode.C: case UpdateCode.XS: targetVals = concat(target[label] ?? [], sourceVals); break; case UpdateCode.XF: //entire array is diff (use orderInd=true) targetVals = concat(sourceVals, target[label] ?? []); break; case UpdateCode.B: case UpdateCode.XM: targetVals = skipEquals ? union(target[label], sourceVals) : unionWith(target[label], sourceVals, deepEquals); break; // case UpdateCode.D: case UpdateCode.XD: targetVals = skipEquals ? difference(target[label], sourceVals) : differenceWith(target[label], sourceVals, deepEquals); break; case UpdateCode.XI: //like XR for empty target? targetVals = skipEquals ? intersection(target[label], sourceVals) : intersectionWith(target[label], sourceVals, deepEquals); break; default: return false; } if (!targetVals?.length) { delete target[label]; return targetKey; } //array replaced const changed = !areArraysEqual(targetVals, target[label]); // && !deepEquals(targetVals, target[label]); target[label] = !changed ? targetVals : deepClone(targetVals); return changed; }; export function mergeVectors<T>( mergeCode: "XM" | "XI" | "XD" | "XS", arr1: readonly T[], arr2: readonly T[], equalsCond?: boolean | ((v1: T, v2: T) => boolean), ): T[] { if (!arr1?.length && !arr2?.length) return []; if (equalsCond === false) { equalsCond = undefined; } else if (typeof equalsCond !== "function") { equalsCond = isArrayOf(arr1, isPrimitive) && isArrayOf(arr2, isPrimitive) ? undefined : deepEquals; } if (equalsCond) { //slow quadratic comparison switch (mergeCode) { case UpdateCode.XM: return unionWith<T>(arr1, arr2, equalsCond); case UpdateCode.XI: return intersectionWith<T, T>(arr1, arr2, equalsCond); case UpdateCode.XD: return differenceWith<T, T>(arr1, arr2, equalsCond); } } switch (mergeCode) { case UpdateCode.XM: return union(arr1, arr2); case UpdateCode.XI: return intersection(arr1, arr2); case UpdateCode.XD: return difference(arr1, arr2); case UpdateCode.XS: return concat<T>(arr1 ?? [], arr2 ?? []); } throw new Error("invalid merge code " + mergeCode); }