json-diff-ts 4.9.1 → 5.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +461 -284
- package/dist/index.cjs +753 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -1
- package/dist/index.d.ts +99 -1
- package/dist/index.js +741 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -24,15 +24,26 @@ __export(index_exports, {
|
|
|
24
24
|
Operation: () => Operation,
|
|
25
25
|
applyChangelist: () => applyChangelist,
|
|
26
26
|
applyChangeset: () => applyChangeset,
|
|
27
|
+
applyDelta: () => applyDelta,
|
|
27
28
|
atomizeChangeset: () => atomizeChangeset,
|
|
29
|
+
buildDeltaPath: () => buildDeltaPath,
|
|
28
30
|
compare: () => compare2,
|
|
29
31
|
createContainer: () => createContainer,
|
|
30
32
|
createValue: () => createValue,
|
|
31
33
|
diff: () => diff,
|
|
34
|
+
diffDelta: () => diffDelta,
|
|
32
35
|
enrich: () => enrich,
|
|
36
|
+
formatFilterLiteral: () => formatFilterLiteral,
|
|
37
|
+
fromDelta: () => fromDelta,
|
|
33
38
|
getTypeOfObj: () => getTypeOfObj,
|
|
39
|
+
invertDelta: () => invertDelta,
|
|
40
|
+
parseDeltaPath: () => parseDeltaPath,
|
|
41
|
+
parseFilterLiteral: () => parseFilterLiteral,
|
|
34
42
|
revertChangeset: () => revertChangeset,
|
|
35
|
-
|
|
43
|
+
revertDelta: () => revertDelta,
|
|
44
|
+
toDelta: () => toDelta,
|
|
45
|
+
unatomizeChangeset: () => unatomizeChangeset,
|
|
46
|
+
validateDelta: () => validateDelta
|
|
36
47
|
});
|
|
37
48
|
module.exports = __toCommonJS(index_exports);
|
|
38
49
|
|
|
@@ -99,7 +110,7 @@ var Operation = /* @__PURE__ */ ((Operation2) => {
|
|
|
99
110
|
return Operation2;
|
|
100
111
|
})(Operation || {});
|
|
101
112
|
function diff(oldObj, newObj, options = {}) {
|
|
102
|
-
let
|
|
113
|
+
let embeddedObjKeys = options.arrayIdentityKeys ?? options.embeddedObjKeys;
|
|
103
114
|
const { keysToSkip, treatTypeChangeAsReplace } = options;
|
|
104
115
|
if (embeddedObjKeys instanceof Map) {
|
|
105
116
|
embeddedObjKeys = new Map(
|
|
@@ -772,20 +783,759 @@ var compare2 = (oldObject, newObject) => {
|
|
|
772
783
|
}
|
|
773
784
|
return applyChangelist(enrich(oldObject), atomizeChangeset(diff(oldObject, newObject)));
|
|
774
785
|
};
|
|
786
|
+
|
|
787
|
+
// src/deltaPath.ts
|
|
788
|
+
function formatFilterLiteral(value) {
|
|
789
|
+
if (value === null) return "null";
|
|
790
|
+
if (typeof value === "boolean") return String(value);
|
|
791
|
+
if (typeof value === "number") {
|
|
792
|
+
if (!Number.isFinite(value)) throw new Error(`Cannot format non-finite number as filter literal: ${value}`);
|
|
793
|
+
return String(value);
|
|
794
|
+
}
|
|
795
|
+
if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
|
|
796
|
+
throw new Error(`Cannot format filter literal for type ${typeof value}`);
|
|
797
|
+
}
|
|
798
|
+
function parseFilterLiteral(s) {
|
|
799
|
+
if (s === "true") return true;
|
|
800
|
+
if (s === "false") return false;
|
|
801
|
+
if (s === "null") return null;
|
|
802
|
+
if (s.startsWith("'") && s.endsWith("'")) {
|
|
803
|
+
return s.slice(1, -1).replace(/''/g, "'");
|
|
804
|
+
}
|
|
805
|
+
if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(s)) {
|
|
806
|
+
return Number(s);
|
|
807
|
+
}
|
|
808
|
+
throw new Error(`Invalid filter literal: ${s}`);
|
|
809
|
+
}
|
|
810
|
+
function extractQuotedString(s, start) {
|
|
811
|
+
const result = [];
|
|
812
|
+
let i = start;
|
|
813
|
+
while (i < s.length) {
|
|
814
|
+
if (s[i] === "'") {
|
|
815
|
+
if (i + 1 < s.length && s[i + 1] === "'") {
|
|
816
|
+
result.push("'");
|
|
817
|
+
i += 2;
|
|
818
|
+
} else {
|
|
819
|
+
return [result.join(""), i];
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
result.push(s[i]);
|
|
823
|
+
i += 1;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
throw new Error("Unterminated quoted string");
|
|
827
|
+
}
|
|
828
|
+
function findFilterClose(s, from) {
|
|
829
|
+
let i = from;
|
|
830
|
+
while (i < s.length) {
|
|
831
|
+
if (s[i] === "'") {
|
|
832
|
+
i += 1;
|
|
833
|
+
while (i < s.length) {
|
|
834
|
+
if (s[i] === "'") {
|
|
835
|
+
if (i + 1 < s.length && s[i + 1] === "'") {
|
|
836
|
+
i += 2;
|
|
837
|
+
} else {
|
|
838
|
+
i += 1;
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
} else {
|
|
842
|
+
i += 1;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
} else if (s[i] === ")" && i + 1 < s.length && s[i + 1] === "]") {
|
|
846
|
+
return i;
|
|
847
|
+
} else {
|
|
848
|
+
i += 1;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return -1;
|
|
852
|
+
}
|
|
853
|
+
function parseFilter(inner) {
|
|
854
|
+
if (inner.startsWith("@.")) {
|
|
855
|
+
const eq = inner.indexOf("==");
|
|
856
|
+
if (eq === -1) throw new Error(`Invalid filter: missing '==' in ${inner}`);
|
|
857
|
+
const key = inner.slice(2, eq);
|
|
858
|
+
return { type: "keyFilter", property: key, value: parseFilterLiteral(inner.slice(eq + 2)) };
|
|
859
|
+
}
|
|
860
|
+
if (inner.startsWith("@['")) {
|
|
861
|
+
const [key, endIdx] = extractQuotedString(inner, 3);
|
|
862
|
+
const valStart = endIdx + 4;
|
|
863
|
+
return { type: "keyFilter", property: key, value: parseFilterLiteral(inner.slice(valStart)) };
|
|
864
|
+
}
|
|
865
|
+
if (inner.startsWith("@==")) {
|
|
866
|
+
return { type: "valueFilter", value: parseFilterLiteral(inner.slice(3)) };
|
|
867
|
+
}
|
|
868
|
+
throw new Error(`Invalid filter expression: ${inner}`);
|
|
869
|
+
}
|
|
870
|
+
function parseDeltaPath(path) {
|
|
871
|
+
if (!path.startsWith("$")) {
|
|
872
|
+
throw new Error(`Path must start with '$': ${path}`);
|
|
873
|
+
}
|
|
874
|
+
const segments = [{ type: "root" }];
|
|
875
|
+
let i = 1;
|
|
876
|
+
while (i < path.length) {
|
|
877
|
+
if (path[i] === ".") {
|
|
878
|
+
i += 1;
|
|
879
|
+
const start = i;
|
|
880
|
+
while (i < path.length && /[a-zA-Z0-9_]/.test(path[i])) {
|
|
881
|
+
i += 1;
|
|
882
|
+
}
|
|
883
|
+
if (i === start) throw new Error(`Empty property name at position ${i} in: ${path}`);
|
|
884
|
+
segments.push({ type: "property", name: path.slice(start, i) });
|
|
885
|
+
} else if (path[i] === "[") {
|
|
886
|
+
if (i + 1 >= path.length) throw new Error(`Unexpected end of path after '[': ${path}`);
|
|
887
|
+
if (path[i + 1] === "?") {
|
|
888
|
+
const closingIdx = findFilterClose(path, i + 2);
|
|
889
|
+
if (closingIdx === -1) throw new Error(`Unterminated filter expression in: ${path}`);
|
|
890
|
+
const inner = path.slice(i + 3, closingIdx);
|
|
891
|
+
segments.push(parseFilter(inner));
|
|
892
|
+
i = closingIdx + 2;
|
|
893
|
+
} else if (path[i + 1] === "'") {
|
|
894
|
+
const [key, endIdx] = extractQuotedString(path, i + 2);
|
|
895
|
+
if (path[endIdx + 1] !== "]") throw new Error(`Expected ']' after bracket property in: ${path}`);
|
|
896
|
+
segments.push({ type: "property", name: key });
|
|
897
|
+
i = endIdx + 2;
|
|
898
|
+
} else if (/\d/.test(path[i + 1])) {
|
|
899
|
+
const end = path.indexOf("]", i);
|
|
900
|
+
if (end === -1) throw new Error(`Unterminated array index in: ${path}`);
|
|
901
|
+
const indexStr = path.slice(i + 1, end);
|
|
902
|
+
if (indexStr.length > 1 && indexStr[0] === "0") {
|
|
903
|
+
throw new Error(`Leading zeros not allowed in array index: [${indexStr}]`);
|
|
904
|
+
}
|
|
905
|
+
segments.push({ type: "index", index: Number(indexStr) });
|
|
906
|
+
i = end + 1;
|
|
907
|
+
} else {
|
|
908
|
+
throw new Error(`Unexpected character after '[': '${path[i + 1]}' in: ${path}`);
|
|
909
|
+
}
|
|
910
|
+
} else {
|
|
911
|
+
throw new Error(`Unexpected character '${path[i]}' at position ${i} in: ${path}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return segments;
|
|
915
|
+
}
|
|
916
|
+
var SIMPLE_PROPERTY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
917
|
+
function formatMemberAccess(name) {
|
|
918
|
+
if (SIMPLE_PROPERTY_RE.test(name)) {
|
|
919
|
+
return `.${name}`;
|
|
920
|
+
}
|
|
921
|
+
return `['${name.replace(/'/g, "''")}']`;
|
|
922
|
+
}
|
|
923
|
+
function buildDeltaPath(segments) {
|
|
924
|
+
let result = "";
|
|
925
|
+
for (const seg of segments) {
|
|
926
|
+
switch (seg.type) {
|
|
927
|
+
case "root":
|
|
928
|
+
result += "$";
|
|
929
|
+
break;
|
|
930
|
+
case "property":
|
|
931
|
+
result += formatMemberAccess(seg.name);
|
|
932
|
+
break;
|
|
933
|
+
case "index":
|
|
934
|
+
result += `[${seg.index}]`;
|
|
935
|
+
break;
|
|
936
|
+
case "keyFilter": {
|
|
937
|
+
const memberAccess = SIMPLE_PROPERTY_RE.test(seg.property) ? `.${seg.property}` : `['${seg.property.replace(/'/g, "''")}']`;
|
|
938
|
+
result += `[?(@${memberAccess}==${formatFilterLiteral(seg.value)})]`;
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
case "valueFilter":
|
|
942
|
+
result += `[?(@==${formatFilterLiteral(seg.value)})]`;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return result;
|
|
947
|
+
}
|
|
948
|
+
function atomicPathToDeltaPath(atomicPath) {
|
|
949
|
+
if (atomicPath === "$.$root") return "$";
|
|
950
|
+
if (atomicPath.startsWith("$.$root.")) return "$" + atomicPath.slice(7);
|
|
951
|
+
if (!atomicPath.startsWith("$")) {
|
|
952
|
+
throw new Error(`Atomic path must start with '$': ${atomicPath}`);
|
|
953
|
+
}
|
|
954
|
+
let result = "$";
|
|
955
|
+
let i = 1;
|
|
956
|
+
while (i < atomicPath.length) {
|
|
957
|
+
if (atomicPath[i] === ".") {
|
|
958
|
+
i += 1;
|
|
959
|
+
const start = i;
|
|
960
|
+
while (i < atomicPath.length && atomicPath[i] !== "." && atomicPath[i] !== "[") {
|
|
961
|
+
i += 1;
|
|
962
|
+
}
|
|
963
|
+
const name = atomicPath.slice(start, i);
|
|
964
|
+
result += formatMemberAccess(name);
|
|
965
|
+
} else if (atomicPath[i] === "[") {
|
|
966
|
+
if (atomicPath[i + 1] === "?") {
|
|
967
|
+
const closingIdx = findFilterClose(atomicPath, i + 2);
|
|
968
|
+
if (closingIdx === -1) throw new Error(`Unterminated filter in: ${atomicPath}`);
|
|
969
|
+
result += atomicPath.slice(i, closingIdx + 2);
|
|
970
|
+
i = closingIdx + 2;
|
|
971
|
+
} else if (atomicPath[i + 1] === "'" || /\d/.test(atomicPath[i + 1])) {
|
|
972
|
+
const end = atomicPath.indexOf("]", i);
|
|
973
|
+
if (end === -1) throw new Error(`Unterminated bracket in: ${atomicPath}`);
|
|
974
|
+
result += atomicPath.slice(i, end + 1);
|
|
975
|
+
i = end + 1;
|
|
976
|
+
} else {
|
|
977
|
+
const end = atomicPath.indexOf("]", i);
|
|
978
|
+
if (end === -1) throw new Error(`Unterminated bracket in: ${atomicPath}`);
|
|
979
|
+
const name = atomicPath.slice(i + 1, end);
|
|
980
|
+
result += `['${name.replace(/'/g, "''")}']`;
|
|
981
|
+
i = end + 1;
|
|
982
|
+
}
|
|
983
|
+
} else {
|
|
984
|
+
throw new Error(`Unexpected character '${atomicPath[i]}' in atomic path: ${atomicPath}`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return result;
|
|
988
|
+
}
|
|
989
|
+
function deltaPathToAtomicPath(deltaPath) {
|
|
990
|
+
if (!deltaPath.startsWith("$")) {
|
|
991
|
+
throw new Error(`Delta path must start with '$': ${deltaPath}`);
|
|
992
|
+
}
|
|
993
|
+
if (deltaPath === "$") {
|
|
994
|
+
return "$.$root";
|
|
995
|
+
}
|
|
996
|
+
let result = "$";
|
|
997
|
+
let i = 1;
|
|
998
|
+
while (i < deltaPath.length) {
|
|
999
|
+
if (deltaPath[i] === ".") {
|
|
1000
|
+
i += 1;
|
|
1001
|
+
const start = i;
|
|
1002
|
+
while (i < deltaPath.length && /[a-zA-Z0-9_]/.test(deltaPath[i])) {
|
|
1003
|
+
i += 1;
|
|
1004
|
+
}
|
|
1005
|
+
result += "." + deltaPath.slice(start, i);
|
|
1006
|
+
} else if (deltaPath[i] === "[") {
|
|
1007
|
+
if (deltaPath[i + 1] === "?") {
|
|
1008
|
+
const closingIdx = findFilterClose(deltaPath, i + 2);
|
|
1009
|
+
if (closingIdx === -1) throw new Error(`Unterminated filter in: ${deltaPath}`);
|
|
1010
|
+
const filterContent = deltaPath.slice(i, closingIdx + 2);
|
|
1011
|
+
result += normalizeFilterToStringLiterals(filterContent);
|
|
1012
|
+
i = closingIdx + 2;
|
|
1013
|
+
} else if (deltaPath[i + 1] === "'") {
|
|
1014
|
+
const [key, endIdx] = extractQuotedString(deltaPath, i + 2);
|
|
1015
|
+
if (deltaPath[endIdx + 1] !== "]") throw new Error(`Expected ']' in: ${deltaPath}`);
|
|
1016
|
+
result += `[${key}]`;
|
|
1017
|
+
i = endIdx + 2;
|
|
1018
|
+
} else if (/\d/.test(deltaPath[i + 1])) {
|
|
1019
|
+
const end = deltaPath.indexOf("]", i);
|
|
1020
|
+
if (end === -1) throw new Error(`Unterminated bracket in: ${deltaPath}`);
|
|
1021
|
+
result += deltaPath.slice(i, end + 1);
|
|
1022
|
+
i = end + 1;
|
|
1023
|
+
} else {
|
|
1024
|
+
throw new Error(`Unexpected character after '[' in: ${deltaPath}`);
|
|
1025
|
+
}
|
|
1026
|
+
} else {
|
|
1027
|
+
throw new Error(`Unexpected character '${deltaPath[i]}' in delta path: ${deltaPath}`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return result;
|
|
1031
|
+
}
|
|
1032
|
+
function normalizeFilterToStringLiterals(filter) {
|
|
1033
|
+
const eqIdx = filter.indexOf("==");
|
|
1034
|
+
if (eqIdx === -1) return filter;
|
|
1035
|
+
const literalStart = eqIdx + 2;
|
|
1036
|
+
const literalEnd = filter.length - 2;
|
|
1037
|
+
const literal = filter.slice(literalStart, literalEnd);
|
|
1038
|
+
if (literal.startsWith("'") && literal.endsWith("'")) {
|
|
1039
|
+
return filter;
|
|
1040
|
+
}
|
|
1041
|
+
const value = parseFilterLiteral(literal);
|
|
1042
|
+
const stringValue = String(value).replace(/'/g, "''");
|
|
1043
|
+
return filter.slice(0, literalStart) + `'${stringValue}'` + filter.slice(literalEnd);
|
|
1044
|
+
}
|
|
1045
|
+
function extractKeyFromAtomicPath(atomicPath) {
|
|
1046
|
+
if (atomicPath === "$.$root") return "$root";
|
|
1047
|
+
if (atomicPath.endsWith(")]")) {
|
|
1048
|
+
const filterStart = atomicPath.lastIndexOf("[?(");
|
|
1049
|
+
if (filterStart !== -1) {
|
|
1050
|
+
const inner = atomicPath.slice(filterStart + 3, atomicPath.length - 2);
|
|
1051
|
+
if (inner.startsWith("@==")) {
|
|
1052
|
+
const val = parseFilterLiteral(inner.slice(3));
|
|
1053
|
+
return String(val);
|
|
1054
|
+
}
|
|
1055
|
+
const eqIdx = inner.indexOf("==");
|
|
1056
|
+
if (eqIdx !== -1) {
|
|
1057
|
+
const val = parseFilterLiteral(inner.slice(eqIdx + 2));
|
|
1058
|
+
return String(val);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (atomicPath.endsWith("]")) {
|
|
1063
|
+
const bracketStart = atomicPath.lastIndexOf("[");
|
|
1064
|
+
if (bracketStart !== -1) {
|
|
1065
|
+
const inner = atomicPath.slice(bracketStart + 1, atomicPath.length - 1);
|
|
1066
|
+
if (/^\d+$/.test(inner)) return inner;
|
|
1067
|
+
return inner;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const lastDot = atomicPath.lastIndexOf(".");
|
|
1071
|
+
if (lastDot > 0) {
|
|
1072
|
+
return atomicPath.slice(lastDot + 1);
|
|
1073
|
+
}
|
|
1074
|
+
return atomicPath;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/jsonDelta.ts
|
|
1078
|
+
function validateDelta(delta) {
|
|
1079
|
+
const errors = [];
|
|
1080
|
+
if (typeof delta !== "object" || delta === null) {
|
|
1081
|
+
return { valid: false, errors: ["Delta must be a non-null object"] };
|
|
1082
|
+
}
|
|
1083
|
+
const d = delta;
|
|
1084
|
+
if (d.format !== "json-delta") {
|
|
1085
|
+
errors.push(`Invalid or missing format: expected 'json-delta', got '${d.format}'`);
|
|
1086
|
+
}
|
|
1087
|
+
if (typeof d.version !== "number") {
|
|
1088
|
+
errors.push(`Missing or invalid version: expected number, got '${typeof d.version}'`);
|
|
1089
|
+
}
|
|
1090
|
+
if (!Array.isArray(d.operations)) {
|
|
1091
|
+
errors.push("Missing or invalid operations: expected array");
|
|
1092
|
+
} else {
|
|
1093
|
+
for (let i = 0; i < d.operations.length; i++) {
|
|
1094
|
+
const op = d.operations[i];
|
|
1095
|
+
if (!op || typeof op !== "object") {
|
|
1096
|
+
errors.push(`operations[${i}]: must be an object`);
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
if (!["add", "remove", "replace"].includes(op.op)) {
|
|
1100
|
+
errors.push(`operations[${i}]: invalid op '${op.op}'`);
|
|
1101
|
+
}
|
|
1102
|
+
if (typeof op.path !== "string") {
|
|
1103
|
+
errors.push(`operations[${i}]: path must be a string`);
|
|
1104
|
+
}
|
|
1105
|
+
if (op.op === "add") {
|
|
1106
|
+
if (!("value" in op)) {
|
|
1107
|
+
errors.push(`operations[${i}]: add operation must have value`);
|
|
1108
|
+
}
|
|
1109
|
+
if ("oldValue" in op) {
|
|
1110
|
+
errors.push(`operations[${i}]: add operation must not have oldValue`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (op.op === "remove" && "value" in op) {
|
|
1114
|
+
errors.push(`operations[${i}]: remove operation must not have value`);
|
|
1115
|
+
}
|
|
1116
|
+
if (op.op === "replace" && !("value" in op)) {
|
|
1117
|
+
errors.push(`operations[${i}]: replace operation must have value`);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return { valid: errors.length === 0, errors };
|
|
1122
|
+
}
|
|
1123
|
+
function diffDelta(oldObj, newObj, options = {}) {
|
|
1124
|
+
const changeset = diff(oldObj, newObj, {
|
|
1125
|
+
...options,
|
|
1126
|
+
treatTypeChangeAsReplace: true
|
|
1127
|
+
// Always true — merging REMOVE+ADD is more reliable (B.1)
|
|
1128
|
+
});
|
|
1129
|
+
const operations = [];
|
|
1130
|
+
walkChanges(changeset, "$", oldObj, newObj, operations, options);
|
|
1131
|
+
return {
|
|
1132
|
+
format: "json-delta",
|
|
1133
|
+
version: 1,
|
|
1134
|
+
operations
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
function mergeTypeChangePairs(changes) {
|
|
1138
|
+
const result = [];
|
|
1139
|
+
let i = 0;
|
|
1140
|
+
while (i < changes.length) {
|
|
1141
|
+
if (i + 1 < changes.length && changes[i].type === "REMOVE" /* REMOVE */ && changes[i + 1].type === "ADD" /* ADD */ && changes[i].key === changes[i + 1].key) {
|
|
1142
|
+
result.push({
|
|
1143
|
+
...changes[i],
|
|
1144
|
+
isMergedReplace: true,
|
|
1145
|
+
removeValue: changes[i].value,
|
|
1146
|
+
addValue: changes[i + 1].value
|
|
1147
|
+
});
|
|
1148
|
+
i += 2;
|
|
1149
|
+
} else {
|
|
1150
|
+
result.push(changes[i]);
|
|
1151
|
+
i += 1;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return result;
|
|
1155
|
+
}
|
|
1156
|
+
var SIMPLE_PROPERTY_RE2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
1157
|
+
function appendCanonicalProperty(basePath, name) {
|
|
1158
|
+
if (SIMPLE_PROPERTY_RE2.test(name)) {
|
|
1159
|
+
return `${basePath}.${name}`;
|
|
1160
|
+
}
|
|
1161
|
+
return `${basePath}['${name.replace(/'/g, "''")}']`;
|
|
1162
|
+
}
|
|
1163
|
+
function walkChanges(changes, basePath, oldCtx, newCtx, ops, options) {
|
|
1164
|
+
const merged = mergeTypeChangePairs(changes);
|
|
1165
|
+
for (const change of merged) {
|
|
1166
|
+
if (change.isMergedReplace) {
|
|
1167
|
+
const mc = change;
|
|
1168
|
+
const path = mc.key === "$root" ? "$" : appendCanonicalProperty(basePath, mc.key);
|
|
1169
|
+
const op = { op: "replace", path, value: mc.addValue };
|
|
1170
|
+
if (options.reversible !== false) {
|
|
1171
|
+
op.oldValue = mc.removeValue;
|
|
1172
|
+
}
|
|
1173
|
+
ops.push(op);
|
|
1174
|
+
} else if (change.changes) {
|
|
1175
|
+
const childPath = change.key === "$root" ? "$" : appendCanonicalProperty(basePath, change.key);
|
|
1176
|
+
const childOld = change.key === "$root" ? oldCtx : oldCtx?.[change.key];
|
|
1177
|
+
const childNew = change.key === "$root" ? newCtx : newCtx?.[change.key];
|
|
1178
|
+
if (change.embeddedKey) {
|
|
1179
|
+
for (const childChange of change.changes) {
|
|
1180
|
+
const filterPath = buildCanonicalFilterPath(
|
|
1181
|
+
childPath,
|
|
1182
|
+
change.embeddedKey,
|
|
1183
|
+
childChange.key,
|
|
1184
|
+
childOld,
|
|
1185
|
+
childNew,
|
|
1186
|
+
childChange
|
|
1187
|
+
);
|
|
1188
|
+
if (childChange.changes) {
|
|
1189
|
+
const oldEl = findElement(childOld, change.embeddedKey, childChange.key);
|
|
1190
|
+
const newEl = findElement(childNew, change.embeddedKey, childChange.key);
|
|
1191
|
+
walkChanges(childChange.changes, filterPath, oldEl, newEl, ops, options);
|
|
1192
|
+
} else {
|
|
1193
|
+
emitLeafOp(childChange, filterPath, ops, options);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
} else {
|
|
1197
|
+
walkChanges(change.changes, childPath, childOld, childNew, ops, options);
|
|
1198
|
+
}
|
|
1199
|
+
} else {
|
|
1200
|
+
const path = change.key === "$root" ? "$" : appendCanonicalProperty(basePath, change.key);
|
|
1201
|
+
emitLeafOp(change, path, ops, options);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function emitLeafOp(change, path, ops, options) {
|
|
1206
|
+
switch (change.type) {
|
|
1207
|
+
case "ADD" /* ADD */: {
|
|
1208
|
+
ops.push({ op: "add", path, value: change.value });
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
case "REMOVE" /* REMOVE */: {
|
|
1212
|
+
const op = { op: "remove", path };
|
|
1213
|
+
if (options.reversible !== false) {
|
|
1214
|
+
op.oldValue = change.value;
|
|
1215
|
+
}
|
|
1216
|
+
ops.push(op);
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
case "UPDATE" /* UPDATE */: {
|
|
1220
|
+
const op = { op: "replace", path, value: change.value };
|
|
1221
|
+
if (options.reversible !== false) {
|
|
1222
|
+
op.oldValue = change.oldValue;
|
|
1223
|
+
}
|
|
1224
|
+
ops.push(op);
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function buildCanonicalFilterPath(basePath, embeddedKey, changeKey, oldArr, newArr, change) {
|
|
1230
|
+
if (embeddedKey === "$index") {
|
|
1231
|
+
return `${basePath}[${changeKey}]`;
|
|
1232
|
+
}
|
|
1233
|
+
if (embeddedKey === "$value") {
|
|
1234
|
+
const typedVal2 = findActualValue(oldArr, newArr, changeKey, change.type);
|
|
1235
|
+
return `${basePath}[?(@==${formatFilterLiteral(typedVal2)})]`;
|
|
1236
|
+
}
|
|
1237
|
+
if (typeof embeddedKey === "function") {
|
|
1238
|
+
const sample = oldArr && oldArr.length > 0 ? oldArr[0] : newArr?.[0];
|
|
1239
|
+
const keyName = sample ? embeddedKey(sample, true) : changeKey;
|
|
1240
|
+
const element2 = findElementByFn(oldArr, newArr, embeddedKey, changeKey, change.type);
|
|
1241
|
+
if (element2 && typeof keyName === "string") {
|
|
1242
|
+
const typedVal2 = element2[keyName];
|
|
1243
|
+
const memberAccess3 = SIMPLE_PROPERTY_RE2.test(keyName) ? `.${keyName}` : `['${keyName.replace(/'/g, "''")}']`;
|
|
1244
|
+
return `${basePath}[?(@${memberAccess3}==${formatFilterLiteral(typedVal2)})]`;
|
|
1245
|
+
}
|
|
1246
|
+
const memberAccess2 = typeof keyName === "string" && SIMPLE_PROPERTY_RE2.test(keyName) ? `.${keyName}` : `.${changeKey}`;
|
|
1247
|
+
return `${basePath}[?(@${memberAccess2}=='${changeKey}')]`;
|
|
1248
|
+
}
|
|
1249
|
+
const element = findElementByKey(oldArr, newArr, embeddedKey, changeKey, change.type);
|
|
1250
|
+
const typedVal = element ? element[embeddedKey] : changeKey;
|
|
1251
|
+
const memberAccess = SIMPLE_PROPERTY_RE2.test(embeddedKey) ? `.${embeddedKey}` : `['${embeddedKey.replace(/'/g, "''")}']`;
|
|
1252
|
+
return `${basePath}[?(@${memberAccess}==${formatFilterLiteral(typedVal)})]`;
|
|
1253
|
+
}
|
|
1254
|
+
function findActualValue(oldArr, newArr, stringKey, opType) {
|
|
1255
|
+
if (opType === "REMOVE" /* REMOVE */ && oldArr) {
|
|
1256
|
+
for (const item of oldArr) {
|
|
1257
|
+
if (String(item) === stringKey) return item;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (opType === "ADD" /* ADD */ && newArr) {
|
|
1261
|
+
for (const item of newArr) {
|
|
1262
|
+
if (String(item) === stringKey) return item;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (oldArr) {
|
|
1266
|
+
for (const item of oldArr) {
|
|
1267
|
+
if (String(item) === stringKey) return item;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (newArr) {
|
|
1271
|
+
for (const item of newArr) {
|
|
1272
|
+
if (String(item) === stringKey) return item;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return stringKey;
|
|
1276
|
+
}
|
|
1277
|
+
function findElement(arr, embeddedKey, changeKey) {
|
|
1278
|
+
if (!arr || !Array.isArray(arr)) return void 0;
|
|
1279
|
+
if (embeddedKey === "$index") {
|
|
1280
|
+
return arr[Number(changeKey)];
|
|
1281
|
+
}
|
|
1282
|
+
if (embeddedKey === "$value") {
|
|
1283
|
+
return arr.find((item) => String(item) === changeKey);
|
|
1284
|
+
}
|
|
1285
|
+
if (typeof embeddedKey === "function") {
|
|
1286
|
+
return arr.find((item) => String(embeddedKey(item)) === changeKey);
|
|
1287
|
+
}
|
|
1288
|
+
return arr.find((item) => item && String(item[embeddedKey]) === changeKey);
|
|
1289
|
+
}
|
|
1290
|
+
function findElementByKey(oldArr, newArr, embeddedKey, changeKey, opType) {
|
|
1291
|
+
if (opType === "REMOVE" /* REMOVE */ || opType === "UPDATE" /* UPDATE */) {
|
|
1292
|
+
const el = oldArr?.find((item) => item && String(item[embeddedKey]) === changeKey);
|
|
1293
|
+
if (el) return el;
|
|
1294
|
+
}
|
|
1295
|
+
if (opType === "ADD" /* ADD */ || opType === "UPDATE" /* UPDATE */) {
|
|
1296
|
+
const el = newArr?.find((item) => item && String(item[embeddedKey]) === changeKey);
|
|
1297
|
+
if (el) return el;
|
|
1298
|
+
}
|
|
1299
|
+
return void 0;
|
|
1300
|
+
}
|
|
1301
|
+
function findElementByFn(oldArr, newArr, fn, changeKey, opType) {
|
|
1302
|
+
if (opType === "REMOVE" /* REMOVE */ || opType === "UPDATE" /* UPDATE */) {
|
|
1303
|
+
const el = oldArr?.find((item) => String(fn(item)) === changeKey);
|
|
1304
|
+
if (el) return el;
|
|
1305
|
+
}
|
|
1306
|
+
if (opType === "ADD" /* ADD */ || opType === "UPDATE" /* UPDATE */) {
|
|
1307
|
+
const el = newArr?.find((item) => String(fn(item)) === changeKey);
|
|
1308
|
+
if (el) return el;
|
|
1309
|
+
}
|
|
1310
|
+
return void 0;
|
|
1311
|
+
}
|
|
1312
|
+
function toDelta(changeset, options = {}) {
|
|
1313
|
+
let atoms;
|
|
1314
|
+
if (changeset.length === 0) {
|
|
1315
|
+
return { format: "json-delta", version: 1, operations: [] };
|
|
1316
|
+
}
|
|
1317
|
+
if ("path" in changeset[0]) {
|
|
1318
|
+
atoms = changeset;
|
|
1319
|
+
} else {
|
|
1320
|
+
atoms = atomizeChangeset(changeset);
|
|
1321
|
+
}
|
|
1322
|
+
const rawOps = atoms.map((atom) => {
|
|
1323
|
+
const path = atomicPathToDeltaPath(atom.path);
|
|
1324
|
+
switch (atom.type) {
|
|
1325
|
+
case "ADD" /* ADD */:
|
|
1326
|
+
return { op: "add", path, value: atom.value };
|
|
1327
|
+
case "REMOVE" /* REMOVE */: {
|
|
1328
|
+
const op = { op: "remove", path };
|
|
1329
|
+
if (options.reversible !== false && atom.value !== void 0) {
|
|
1330
|
+
op.oldValue = atom.value;
|
|
1331
|
+
}
|
|
1332
|
+
return op;
|
|
1333
|
+
}
|
|
1334
|
+
case "UPDATE" /* UPDATE */: {
|
|
1335
|
+
const op = { op: "replace", path, value: atom.value };
|
|
1336
|
+
if (options.reversible !== false && atom.oldValue !== void 0) {
|
|
1337
|
+
op.oldValue = atom.oldValue;
|
|
1338
|
+
}
|
|
1339
|
+
return op;
|
|
1340
|
+
}
|
|
1341
|
+
/* istanbul ignore next -- exhaustive switch */
|
|
1342
|
+
default:
|
|
1343
|
+
throw new Error(`Unknown operation type: ${atom.type}`);
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
const operations = mergeConsecutiveOps(rawOps);
|
|
1347
|
+
return { format: "json-delta", version: 1, operations };
|
|
1348
|
+
}
|
|
1349
|
+
function mergeConsecutiveOps(ops) {
|
|
1350
|
+
const result = [];
|
|
1351
|
+
let i = 0;
|
|
1352
|
+
while (i < ops.length) {
|
|
1353
|
+
if (i + 1 < ops.length && ops[i].op === "remove" && ops[i + 1].op === "add" && ops[i].path === ops[i + 1].path) {
|
|
1354
|
+
const merged = {
|
|
1355
|
+
op: "replace",
|
|
1356
|
+
path: ops[i].path,
|
|
1357
|
+
value: ops[i + 1].value
|
|
1358
|
+
};
|
|
1359
|
+
if (ops[i].oldValue !== void 0) {
|
|
1360
|
+
merged.oldValue = ops[i].oldValue;
|
|
1361
|
+
}
|
|
1362
|
+
result.push(merged);
|
|
1363
|
+
i += 2;
|
|
1364
|
+
} else {
|
|
1365
|
+
result.push(ops[i]);
|
|
1366
|
+
i += 1;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return result;
|
|
1370
|
+
}
|
|
1371
|
+
function fromDelta(delta) {
|
|
1372
|
+
const validation = validateDelta(delta);
|
|
1373
|
+
if (!validation.valid) {
|
|
1374
|
+
throw new Error(`Invalid delta: ${validation.errors.join(", ")}`);
|
|
1375
|
+
}
|
|
1376
|
+
return delta.operations.map((op) => {
|
|
1377
|
+
const atomicPath = deltaPathToAtomicPath(op.path);
|
|
1378
|
+
const key = extractKeyFromAtomicPath(atomicPath);
|
|
1379
|
+
switch (op.op) {
|
|
1380
|
+
case "add": {
|
|
1381
|
+
const valueType = getValueType(op.value);
|
|
1382
|
+
return { type: "ADD" /* ADD */, key, path: atomicPath, valueType, value: op.value };
|
|
1383
|
+
}
|
|
1384
|
+
case "remove": {
|
|
1385
|
+
const valueType = op.oldValue !== void 0 ? getValueType(op.oldValue) : null;
|
|
1386
|
+
return { type: "REMOVE" /* REMOVE */, key, path: atomicPath, valueType, value: op.oldValue };
|
|
1387
|
+
}
|
|
1388
|
+
case "replace": {
|
|
1389
|
+
const valueType = getValueType(op.value);
|
|
1390
|
+
const atom = { type: "UPDATE" /* UPDATE */, key, path: atomicPath, valueType, value: op.value };
|
|
1391
|
+
if (op.oldValue !== void 0) {
|
|
1392
|
+
atom.oldValue = op.oldValue;
|
|
1393
|
+
}
|
|
1394
|
+
return atom;
|
|
1395
|
+
}
|
|
1396
|
+
/* istanbul ignore next -- exhaustive switch */
|
|
1397
|
+
default:
|
|
1398
|
+
throw new Error(`Unknown operation: ${op.op}`);
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
function getValueType(value) {
|
|
1403
|
+
if (value === void 0) return "undefined";
|
|
1404
|
+
if (value === null) return null;
|
|
1405
|
+
if (Array.isArray(value)) return "Array";
|
|
1406
|
+
const type = typeof value;
|
|
1407
|
+
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
1408
|
+
}
|
|
1409
|
+
function invertDelta(delta) {
|
|
1410
|
+
const validation = validateDelta(delta);
|
|
1411
|
+
if (!validation.valid) {
|
|
1412
|
+
throw new Error(`Invalid delta: ${validation.errors.join(", ")}`);
|
|
1413
|
+
}
|
|
1414
|
+
for (let i = 0; i < delta.operations.length; i++) {
|
|
1415
|
+
const op = delta.operations[i];
|
|
1416
|
+
if (op.op === "replace" && !("oldValue" in op)) {
|
|
1417
|
+
throw new Error(`operations[${i}]: replace operation missing oldValue \u2014 delta is not reversible`);
|
|
1418
|
+
}
|
|
1419
|
+
if (op.op === "remove" && !("oldValue" in op)) {
|
|
1420
|
+
throw new Error(`operations[${i}]: remove operation missing oldValue \u2014 delta is not reversible`);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const invertedOps = [...delta.operations].reverse().map((op) => {
|
|
1424
|
+
const extensions = {};
|
|
1425
|
+
for (const key of Object.keys(op)) {
|
|
1426
|
+
if (!["op", "path", "value", "oldValue"].includes(key)) {
|
|
1427
|
+
extensions[key] = op[key];
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
switch (op.op) {
|
|
1431
|
+
case "add":
|
|
1432
|
+
return { op: "remove", path: op.path, oldValue: op.value, ...extensions };
|
|
1433
|
+
case "remove":
|
|
1434
|
+
return { op: "add", path: op.path, value: op.oldValue, ...extensions };
|
|
1435
|
+
case "replace":
|
|
1436
|
+
return { op: "replace", path: op.path, value: op.oldValue, oldValue: op.value, ...extensions };
|
|
1437
|
+
/* istanbul ignore next -- exhaustive switch */
|
|
1438
|
+
default:
|
|
1439
|
+
throw new Error(`Unknown operation: ${op.op}`);
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
const envelope = { format: "json-delta", version: delta.version, operations: invertedOps };
|
|
1443
|
+
for (const key of Object.keys(delta)) {
|
|
1444
|
+
if (!["format", "version", "operations"].includes(key)) {
|
|
1445
|
+
envelope[key] = delta[key];
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return envelope;
|
|
1449
|
+
}
|
|
1450
|
+
function applyDelta(obj, delta) {
|
|
1451
|
+
const validation = validateDelta(delta);
|
|
1452
|
+
if (!validation.valid) {
|
|
1453
|
+
throw new Error(`Invalid delta: ${validation.errors.join(", ")}`);
|
|
1454
|
+
}
|
|
1455
|
+
let result = obj;
|
|
1456
|
+
for (const op of delta.operations) {
|
|
1457
|
+
if (op.path === "$") {
|
|
1458
|
+
result = applyRootOp(result, op);
|
|
1459
|
+
} else {
|
|
1460
|
+
const atomicChange = deltaOpToAtomicChange(op);
|
|
1461
|
+
const miniChangeset = unatomizeChangeset([atomicChange]);
|
|
1462
|
+
applyChangeset(result, miniChangeset);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return result;
|
|
1466
|
+
}
|
|
1467
|
+
function applyRootOp(obj, op) {
|
|
1468
|
+
switch (op.op) {
|
|
1469
|
+
case "add":
|
|
1470
|
+
return op.value;
|
|
1471
|
+
case "remove":
|
|
1472
|
+
return null;
|
|
1473
|
+
case "replace": {
|
|
1474
|
+
if (typeof obj === "object" && obj !== null && !Array.isArray(obj) && typeof op.value === "object" && op.value !== null && !Array.isArray(op.value)) {
|
|
1475
|
+
for (const key of Object.keys(obj)) {
|
|
1476
|
+
delete obj[key];
|
|
1477
|
+
}
|
|
1478
|
+
Object.assign(obj, op.value);
|
|
1479
|
+
return obj;
|
|
1480
|
+
}
|
|
1481
|
+
return op.value;
|
|
1482
|
+
}
|
|
1483
|
+
/* istanbul ignore next -- exhaustive switch */
|
|
1484
|
+
default:
|
|
1485
|
+
throw new Error(`Unknown operation: ${op.op}`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
function deltaOpToAtomicChange(op) {
|
|
1489
|
+
const atomicPath = deltaPathToAtomicPath(op.path);
|
|
1490
|
+
const key = extractKeyFromAtomicPath(atomicPath);
|
|
1491
|
+
switch (op.op) {
|
|
1492
|
+
case "add":
|
|
1493
|
+
return { type: "ADD" /* ADD */, key, path: atomicPath, valueType: getValueType(op.value), value: op.value };
|
|
1494
|
+
case "remove":
|
|
1495
|
+
return { type: "REMOVE" /* REMOVE */, key, path: atomicPath, valueType: getValueType(op.oldValue), value: op.oldValue };
|
|
1496
|
+
case "replace":
|
|
1497
|
+
return {
|
|
1498
|
+
type: "UPDATE" /* UPDATE */,
|
|
1499
|
+
key,
|
|
1500
|
+
path: atomicPath,
|
|
1501
|
+
valueType: getValueType(op.value),
|
|
1502
|
+
value: op.value,
|
|
1503
|
+
oldValue: op.oldValue
|
|
1504
|
+
};
|
|
1505
|
+
/* istanbul ignore next -- exhaustive switch */
|
|
1506
|
+
default:
|
|
1507
|
+
throw new Error(`Unknown operation: ${op.op}`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function revertDelta(obj, delta) {
|
|
1511
|
+
const inverse = invertDelta(delta);
|
|
1512
|
+
return applyDelta(obj, inverse);
|
|
1513
|
+
}
|
|
775
1514
|
// Annotate the CommonJS export names for ESM import in node:
|
|
776
1515
|
0 && (module.exports = {
|
|
777
1516
|
CompareOperation,
|
|
778
1517
|
Operation,
|
|
779
1518
|
applyChangelist,
|
|
780
1519
|
applyChangeset,
|
|
1520
|
+
applyDelta,
|
|
781
1521
|
atomizeChangeset,
|
|
1522
|
+
buildDeltaPath,
|
|
782
1523
|
compare,
|
|
783
1524
|
createContainer,
|
|
784
1525
|
createValue,
|
|
785
1526
|
diff,
|
|
1527
|
+
diffDelta,
|
|
786
1528
|
enrich,
|
|
1529
|
+
formatFilterLiteral,
|
|
1530
|
+
fromDelta,
|
|
787
1531
|
getTypeOfObj,
|
|
1532
|
+
invertDelta,
|
|
1533
|
+
parseDeltaPath,
|
|
1534
|
+
parseFilterLiteral,
|
|
788
1535
|
revertChangeset,
|
|
789
|
-
|
|
1536
|
+
revertDelta,
|
|
1537
|
+
toDelta,
|
|
1538
|
+
unatomizeChangeset,
|
|
1539
|
+
validateDelta
|
|
790
1540
|
});
|
|
791
1541
|
//# sourceMappingURL=index.cjs.map
|