import { concat, differenceWith, intersectionWith, unionWith } from "lodash-es"; import { emptyValue, 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 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 start (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 = target.hasOwnProperty(label); const targetHas = targetKey && !isNullish(target[label]); if (!targetHas && !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.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 = target.hasOwnProperty(label); const targetHas = targetKey && !isNullish(target[label]); const targetVec = isArrayOfAny(target[label]); if (targetHas && !targetVec) { //should T => T[] change be allowed throw new TypeError("type change to vector for " + label); } //todo verify same array types if (!sourceVec) { sourceVals = [sourceVals] as unknown[]; sourceVec = true; } 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 = unionWith(target[label], sourceVals, deepEquals); break; // case UpdateCode.D: case UpdateCode.XD: targetVals = differenceWith(target[label], sourceVals, deepEquals); break; case UpdateCode.XI: targetVals = 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; };