json-diff-ts 5.0.0-alpha.1 → 5.0.0-alpha.4
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 +613 -318
- package/dist/index.cjs +1063 -126
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +188 -10
- package/dist/index.d.ts +188 -10
- package/dist/index.js +1040 -125
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -22,17 +22,39 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CompareOperation: () => CompareOperation,
|
|
24
24
|
Operation: () => Operation,
|
|
25
|
+
applyAtom: () => applyAtom,
|
|
25
26
|
applyChangelist: () => applyChangelist,
|
|
26
27
|
applyChangeset: () => applyChangeset,
|
|
28
|
+
atomExtensions: () => atomExtensions,
|
|
29
|
+
atomGroupBy: () => atomGroupBy,
|
|
30
|
+
atomMap: () => atomMap,
|
|
31
|
+
atomSpecDict: () => atomSpecDict,
|
|
32
|
+
atomStamp: () => atomStamp,
|
|
27
33
|
atomizeChangeset: () => atomizeChangeset,
|
|
34
|
+
buildAtomPath: () => buildAtomPath,
|
|
28
35
|
compare: () => compare2,
|
|
36
|
+
comparisonToDict: () => comparisonToDict,
|
|
37
|
+
comparisonToFlatList: () => comparisonToFlatList,
|
|
29
38
|
createContainer: () => createContainer,
|
|
30
39
|
createValue: () => createValue,
|
|
31
40
|
diff: () => diff,
|
|
41
|
+
diffAtom: () => diffAtom,
|
|
32
42
|
enrich: () => enrich,
|
|
43
|
+
formatFilterLiteral: () => formatFilterLiteral,
|
|
44
|
+
fromAtom: () => fromAtom,
|
|
33
45
|
getTypeOfObj: () => getTypeOfObj,
|
|
46
|
+
invertAtom: () => invertAtom,
|
|
47
|
+
leafProperty: () => leafProperty,
|
|
48
|
+
operationExtensions: () => operationExtensions,
|
|
49
|
+
operationSpecDict: () => operationSpecDict,
|
|
50
|
+
parseAtomPath: () => parseAtomPath,
|
|
51
|
+
parseFilterLiteral: () => parseFilterLiteral,
|
|
52
|
+
revertAtom: () => revertAtom,
|
|
34
53
|
revertChangeset: () => revertChangeset,
|
|
35
|
-
|
|
54
|
+
squashAtoms: () => squashAtoms,
|
|
55
|
+
toAtom: () => toAtom,
|
|
56
|
+
unatomizeChangeset: () => unatomizeChangeset,
|
|
57
|
+
validateAtom: () => validateAtom
|
|
36
58
|
});
|
|
37
59
|
module.exports = __toCommonJS(index_exports);
|
|
38
60
|
|
|
@@ -73,13 +95,13 @@ function arrayIntersection(first, second) {
|
|
|
73
95
|
}
|
|
74
96
|
function keyBy(arr, getKey2) {
|
|
75
97
|
const result = {};
|
|
76
|
-
for (const item of arr) {
|
|
77
|
-
result[String(getKey2(item))] = item;
|
|
98
|
+
for (const [index, item] of Object.entries(arr)) {
|
|
99
|
+
result[String(getKey2(item, false, Number(index)))] = item;
|
|
78
100
|
}
|
|
79
101
|
return result;
|
|
80
102
|
}
|
|
81
103
|
function setByPath(obj, path, value) {
|
|
82
|
-
const parts = path.
|
|
104
|
+
const parts = path.replaceAll(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
83
105
|
let current = obj;
|
|
84
106
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
85
107
|
const part = parts[i];
|
|
@@ -88,7 +110,7 @@ function setByPath(obj, path, value) {
|
|
|
88
110
|
}
|
|
89
111
|
current = current[part];
|
|
90
112
|
}
|
|
91
|
-
current[parts
|
|
113
|
+
current[parts.at(-1)] = value;
|
|
92
114
|
}
|
|
93
115
|
|
|
94
116
|
// src/jsonDiff.ts
|
|
@@ -96,12 +118,11 @@ var Operation = /* @__PURE__ */ ((Operation2) => {
|
|
|
96
118
|
Operation2["REMOVE"] = "REMOVE";
|
|
97
119
|
Operation2["ADD"] = "ADD";
|
|
98
120
|
Operation2["UPDATE"] = "UPDATE";
|
|
99
|
-
Operation2["MOVE"] = "MOVE";
|
|
100
121
|
return Operation2;
|
|
101
122
|
})(Operation || {});
|
|
102
123
|
function diff(oldObj, newObj, options = {}) {
|
|
103
|
-
let
|
|
104
|
-
const { keysToSkip, treatTypeChangeAsReplace
|
|
124
|
+
let embeddedObjKeys = options.arrayIdentityKeys ?? options.embeddedObjKeys;
|
|
125
|
+
const { keysToSkip, treatTypeChangeAsReplace } = options;
|
|
105
126
|
if (embeddedObjKeys instanceof Map) {
|
|
106
127
|
embeddedObjKeys = new Map(
|
|
107
128
|
Array.from(embeddedObjKeys.entries()).map(([key, value]) => [
|
|
@@ -117,8 +138,7 @@ function diff(oldObj, newObj, options = {}) {
|
|
|
117
138
|
return compare(oldObj, newObj, [], [], {
|
|
118
139
|
embeddedObjKeys,
|
|
119
140
|
keysToSkip: keysToSkip ?? [],
|
|
120
|
-
treatTypeChangeAsReplace: treatTypeChangeAsReplace ?? true
|
|
121
|
-
detectArrayMoves: detectArrayMoves2 ?? false
|
|
141
|
+
treatTypeChangeAsReplace: treatTypeChangeAsReplace ?? true
|
|
122
142
|
});
|
|
123
143
|
}
|
|
124
144
|
var applyChangeset = (obj, changeset) => {
|
|
@@ -128,7 +148,7 @@ var applyChangeset = (obj, changeset) => {
|
|
|
128
148
|
if (value !== null && value !== void 0 || type === "REMOVE" /* REMOVE */ || value === null && type === "ADD" /* ADD */ || value === void 0 && type === "ADD" /* ADD */) {
|
|
129
149
|
applyLeafChange(obj, change, embeddedKey);
|
|
130
150
|
} else {
|
|
131
|
-
applyBranchChange(obj[key], change);
|
|
151
|
+
applyBranchChange(key === "$root" ? obj : obj[key], change);
|
|
132
152
|
}
|
|
133
153
|
});
|
|
134
154
|
}
|
|
@@ -141,7 +161,7 @@ var revertChangeset = (obj, changeset) => {
|
|
|
141
161
|
if (!change.changes || value === null && type === "REMOVE" /* REMOVE */) {
|
|
142
162
|
revertLeafChange(obj, change);
|
|
143
163
|
} else {
|
|
144
|
-
revertBranchChange(obj[change.key], change);
|
|
164
|
+
revertBranchChange(change.key === "$root" ? obj : obj[change.key], change);
|
|
145
165
|
}
|
|
146
166
|
});
|
|
147
167
|
}
|
|
@@ -165,22 +185,18 @@ var atomizeChangeset = (obj, path = "$", embeddedKey) => {
|
|
|
165
185
|
const valueType = getTypeOfObj(obj.value);
|
|
166
186
|
let finalPath = path;
|
|
167
187
|
if (!finalPath.endsWith(`[${obj.key}]`)) {
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (filterStartIdx !== -1) {
|
|
176
|
-
const filterValue = path.slice(filterStartIdx + 2, filterEndIdx).replace(/(^'|'$)/g, "");
|
|
177
|
-
endsWithFilterValue = filterValue === String(obj.key);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (!endsWithFilterValue) {
|
|
181
|
-
finalPath = append(path, obj.key);
|
|
188
|
+
let endsWithFilterValue = false;
|
|
189
|
+
const filterEndIdx = path.lastIndexOf(")]");
|
|
190
|
+
if (filterEndIdx !== -1) {
|
|
191
|
+
const filterStartIdx = path.lastIndexOf("==", filterEndIdx);
|
|
192
|
+
if (filterStartIdx !== -1) {
|
|
193
|
+
const filterValue = path.slice(filterStartIdx + 2, filterEndIdx).replaceAll(/(^'|'$)/g, "");
|
|
194
|
+
endsWithFilterValue = filterValue === String(obj.key);
|
|
182
195
|
}
|
|
183
196
|
}
|
|
197
|
+
if (!endsWithFilterValue) {
|
|
198
|
+
finalPath = append(path, obj.key);
|
|
199
|
+
}
|
|
184
200
|
}
|
|
185
201
|
return [
|
|
186
202
|
{
|
|
@@ -230,27 +246,23 @@ var unatomizeChangeset = (changes) => {
|
|
|
230
246
|
ptr.type = change.type;
|
|
231
247
|
ptr.value = change.value;
|
|
232
248
|
ptr.oldValue = change.oldValue;
|
|
233
|
-
if (change.type === "MOVE" /* MOVE */) {
|
|
234
|
-
ptr.oldIndex = change.oldIndex;
|
|
235
|
-
ptr.newIndex = change.newIndex;
|
|
236
|
-
}
|
|
237
249
|
changesArr.push(ptr);
|
|
238
250
|
} else {
|
|
239
251
|
for (let i = 1; i < segments.length; i++) {
|
|
240
252
|
const segment = segments[i];
|
|
241
|
-
const result = /^([^[\]]+)\[\?\(
|
|
253
|
+
const result = /^([^[\]]+)\[\?\(@(?:\.?([^=[]*)|(?:\['([^']*)'\]))=+'([^']+)'\)\]$|^(.+)\[(\d+)\]$/.exec(segment);
|
|
242
254
|
if (result) {
|
|
243
255
|
let key;
|
|
244
256
|
let embeddedKey;
|
|
245
257
|
let arrKey;
|
|
246
258
|
if (result[1]) {
|
|
247
259
|
key = result[1];
|
|
248
|
-
embeddedKey = result[2] || "$value";
|
|
249
|
-
arrKey = result[
|
|
260
|
+
embeddedKey = result[3] || result[2] || "$value";
|
|
261
|
+
arrKey = result[4];
|
|
250
262
|
} else {
|
|
251
|
-
key = result[
|
|
263
|
+
key = result[5];
|
|
252
264
|
embeddedKey = "$index";
|
|
253
|
-
arrKey = Number(result[
|
|
265
|
+
arrKey = Number(result[6]);
|
|
254
266
|
}
|
|
255
267
|
if (i === segments.length - 1) {
|
|
256
268
|
ptr.key = key;
|
|
@@ -261,8 +273,7 @@ var unatomizeChangeset = (changes) => {
|
|
|
261
273
|
type: change.type,
|
|
262
274
|
key: arrKey,
|
|
263
275
|
value: change.value,
|
|
264
|
-
oldValue: change.oldValue
|
|
265
|
-
...change.type === "MOVE" /* MOVE */ && { oldIndex: change.oldIndex, newIndex: change.newIndex }
|
|
276
|
+
oldValue: change.oldValue
|
|
266
277
|
}
|
|
267
278
|
];
|
|
268
279
|
} else {
|
|
@@ -285,10 +296,6 @@ var unatomizeChangeset = (changes) => {
|
|
|
285
296
|
ptr.type = change.type;
|
|
286
297
|
ptr.value = change.value;
|
|
287
298
|
ptr.oldValue = change.oldValue;
|
|
288
|
-
if (change.type === "MOVE" /* MOVE */) {
|
|
289
|
-
ptr.oldIndex = change.oldIndex;
|
|
290
|
-
ptr.newIndex = change.newIndex;
|
|
291
|
-
}
|
|
292
299
|
} else {
|
|
293
300
|
ptr.key = segment;
|
|
294
301
|
ptr.type = "UPDATE" /* UPDATE */;
|
|
@@ -304,7 +311,7 @@ var unatomizeChangeset = (changes) => {
|
|
|
304
311
|
return changesArr;
|
|
305
312
|
};
|
|
306
313
|
var getTypeOfObj = (obj) => {
|
|
307
|
-
if (
|
|
314
|
+
if (obj === void 0) {
|
|
308
315
|
return "undefined";
|
|
309
316
|
}
|
|
310
317
|
if (obj === null) {
|
|
@@ -313,8 +320,8 @@ var getTypeOfObj = (obj) => {
|
|
|
313
320
|
return Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1];
|
|
314
321
|
};
|
|
315
322
|
var getKey = (path) => {
|
|
316
|
-
const left = path
|
|
317
|
-
return left
|
|
323
|
+
const left = path.at(-1);
|
|
324
|
+
return left ?? "$root";
|
|
318
325
|
};
|
|
319
326
|
var compare = (oldObj, newObj, path, keyPath, options) => {
|
|
320
327
|
let changes = [];
|
|
@@ -469,22 +476,17 @@ var compareArray = (oldObj, newObj, path, keyPath, options) => {
|
|
|
469
476
|
return [{ type: "UPDATE" /* UPDATE */, key: getKey(path), value: newObj, oldValue: oldObj }];
|
|
470
477
|
}
|
|
471
478
|
const left = getObjectKey(options.embeddedObjKeys, keyPath);
|
|
472
|
-
const uniqKey = left
|
|
479
|
+
const uniqKey = left ?? "$index";
|
|
473
480
|
const indexedOldObj = convertArrayToObj(oldObj, uniqKey);
|
|
474
481
|
const indexedNewObj = convertArrayToObj(newObj, uniqKey);
|
|
475
482
|
const diffs = compareObject(indexedOldObj, indexedNewObj, path, keyPath, true, options);
|
|
476
|
-
|
|
477
|
-
if (options.detectArrayMoves && uniqKey !== "$index" && left != null) {
|
|
478
|
-
moveDiffs = detectArrayMoves(oldObj, newObj, uniqKey);
|
|
479
|
-
}
|
|
480
|
-
const allDiffs = [...diffs, ...moveDiffs];
|
|
481
|
-
if (allDiffs.length) {
|
|
483
|
+
if (diffs.length) {
|
|
482
484
|
return [
|
|
483
485
|
{
|
|
484
486
|
type: "UPDATE" /* UPDATE */,
|
|
485
487
|
key: getKey(path),
|
|
486
488
|
embeddedKey: typeof uniqKey === "function" && uniqKey.length === 2 ? uniqKey(newObj[0], true) : uniqKey,
|
|
487
|
-
changes:
|
|
489
|
+
changes: diffs
|
|
488
490
|
}
|
|
489
491
|
];
|
|
490
492
|
} else {
|
|
@@ -512,49 +514,20 @@ var getObjectKey = (embeddedObjKeys, keyPath) => {
|
|
|
512
514
|
}
|
|
513
515
|
return void 0;
|
|
514
516
|
};
|
|
515
|
-
var detectArrayMoves = (oldArray, newArray, uniqKey) => {
|
|
516
|
-
const moves = [];
|
|
517
|
-
const keyFunction = typeof uniqKey === "string" ? (item) => item[uniqKey] : uniqKey;
|
|
518
|
-
const oldIndexMap = /* @__PURE__ */ new Map();
|
|
519
|
-
const newIndexMap = /* @__PURE__ */ new Map();
|
|
520
|
-
oldArray.forEach((item, index) => {
|
|
521
|
-
const key = keyFunction(item);
|
|
522
|
-
oldIndexMap.set(key, index);
|
|
523
|
-
});
|
|
524
|
-
newArray.forEach((item, index) => {
|
|
525
|
-
const key = keyFunction(item);
|
|
526
|
-
newIndexMap.set(key, index);
|
|
527
|
-
});
|
|
528
|
-
for (const [key, newIndex] of newIndexMap) {
|
|
529
|
-
if (oldIndexMap.has(key)) {
|
|
530
|
-
const oldIndex = oldIndexMap.get(key);
|
|
531
|
-
if (oldIndex !== newIndex) {
|
|
532
|
-
moves.push({
|
|
533
|
-
type: "MOVE" /* MOVE */,
|
|
534
|
-
key,
|
|
535
|
-
oldIndex,
|
|
536
|
-
newIndex,
|
|
537
|
-
value: newArray[newIndex]
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return moves;
|
|
543
|
-
};
|
|
544
517
|
var convertArrayToObj = (arr, uniqKey) => {
|
|
545
518
|
let obj = {};
|
|
546
519
|
if (uniqKey === "$value") {
|
|
547
520
|
arr.forEach((value) => {
|
|
548
521
|
obj[value] = value;
|
|
549
522
|
});
|
|
550
|
-
} else if (uniqKey
|
|
551
|
-
const keyFunction = typeof uniqKey === "string" ? (item) => item[uniqKey] : uniqKey;
|
|
552
|
-
obj = keyBy(arr, keyFunction);
|
|
553
|
-
} else {
|
|
523
|
+
} else if (uniqKey === "$index") {
|
|
554
524
|
for (let i = 0; i < arr.length; i++) {
|
|
555
525
|
const value = arr[i];
|
|
556
526
|
obj[i] = value;
|
|
557
527
|
}
|
|
528
|
+
} else {
|
|
529
|
+
const keyFunction = typeof uniqKey === "string" ? (item) => item[uniqKey] : uniqKey;
|
|
530
|
+
obj = keyBy(arr, keyFunction);
|
|
558
531
|
}
|
|
559
532
|
return obj;
|
|
560
533
|
};
|
|
@@ -578,10 +551,10 @@ var removeKey = (obj, key, embeddedKey) => {
|
|
|
578
551
|
}
|
|
579
552
|
const index = indexOfItemInArray(obj, embeddedKey, key);
|
|
580
553
|
if (index === -1) {
|
|
581
|
-
console.warn(`Element with the key '${embeddedKey}' and value '${key}' could not be found in the array
|
|
554
|
+
console.warn(`Element with the key '${embeddedKey}' and value '${key}' could not be found in the array!`);
|
|
582
555
|
return;
|
|
583
556
|
}
|
|
584
|
-
return obj.splice(index
|
|
557
|
+
return obj.splice(index ?? key, 1);
|
|
585
558
|
} else {
|
|
586
559
|
delete obj[key];
|
|
587
560
|
return;
|
|
@@ -600,29 +573,6 @@ var indexOfItemInArray = (arr, key, value) => {
|
|
|
600
573
|
return -1;
|
|
601
574
|
};
|
|
602
575
|
var modifyKeyValue = (obj, key, value) => obj[key] = value;
|
|
603
|
-
var moveArrayElement = (arr, key, oldIndex, newIndex, embeddedKey) => {
|
|
604
|
-
if (!Array.isArray(arr)) {
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
let elementIndex = -1;
|
|
608
|
-
if (embeddedKey === "$index") {
|
|
609
|
-
elementIndex = oldIndex;
|
|
610
|
-
} else if (embeddedKey === "$value") {
|
|
611
|
-
elementIndex = arr.indexOf(key);
|
|
612
|
-
} else {
|
|
613
|
-
elementIndex = arr.findIndex((item) => {
|
|
614
|
-
const keyFunction = typeof embeddedKey === "string" ? (item2) => item2[embeddedKey] : embeddedKey;
|
|
615
|
-
return keyFunction(item)?.toString() === key.toString();
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
if (elementIndex === -1) {
|
|
619
|
-
console.warn(`Element with key '${key}' not found for MOVE operation`);
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
const element = arr.splice(elementIndex, 1)[0];
|
|
623
|
-
arr.splice(newIndex, 0, element);
|
|
624
|
-
return arr;
|
|
625
|
-
};
|
|
626
576
|
var addKeyValue = (obj, key, value, embeddedKey) => {
|
|
627
577
|
if (Array.isArray(obj)) {
|
|
628
578
|
if (embeddedKey === "$index") {
|
|
@@ -635,7 +585,7 @@ var addKeyValue = (obj, key, value, embeddedKey) => {
|
|
|
635
585
|
}
|
|
636
586
|
};
|
|
637
587
|
var applyLeafChange = (obj, change, embeddedKey) => {
|
|
638
|
-
const { type, key, value
|
|
588
|
+
const { type, key, value } = change;
|
|
639
589
|
switch (type) {
|
|
640
590
|
case "ADD" /* ADD */:
|
|
641
591
|
return addKeyValue(obj, key, value, embeddedKey);
|
|
@@ -643,8 +593,6 @@ var applyLeafChange = (obj, change, embeddedKey) => {
|
|
|
643
593
|
return modifyKeyValue(obj, key, value);
|
|
644
594
|
case "REMOVE" /* REMOVE */:
|
|
645
595
|
return removeKey(obj, key, embeddedKey);
|
|
646
|
-
case "MOVE" /* MOVE */:
|
|
647
|
-
return moveArrayElement(obj, key, oldIndex, newIndex, embeddedKey);
|
|
648
596
|
}
|
|
649
597
|
};
|
|
650
598
|
var applyArrayChange = (arr, change) => {
|
|
@@ -660,7 +608,7 @@ var applyArrayChange = (arr, change) => {
|
|
|
660
608
|
});
|
|
661
609
|
}
|
|
662
610
|
for (const subchange of changes) {
|
|
663
|
-
if (subchange.value !== null && subchange.value !== void 0 || subchange.type === "REMOVE" /* REMOVE */ || subchange.value === null && subchange.type === "ADD" /* ADD */ || subchange.
|
|
611
|
+
if (subchange.value !== null && subchange.value !== void 0 || subchange.type === "REMOVE" /* REMOVE */ || subchange.value === null && subchange.type === "ADD" /* ADD */ || subchange.value === void 0 && subchange.type === "ADD" /* ADD */) {
|
|
664
612
|
applyLeafChange(arr, subchange, change.embeddedKey);
|
|
665
613
|
} else {
|
|
666
614
|
let element;
|
|
@@ -689,7 +637,7 @@ var applyBranchChange = (obj, change) => {
|
|
|
689
637
|
}
|
|
690
638
|
};
|
|
691
639
|
var revertLeafChange = (obj, change, embeddedKey = "$index") => {
|
|
692
|
-
const { type, key, value, oldValue
|
|
640
|
+
const { type, key, value, oldValue } = change;
|
|
693
641
|
if (key === "$root") {
|
|
694
642
|
switch (type) {
|
|
695
643
|
case "ADD" /* ADD */:
|
|
@@ -723,13 +671,11 @@ var revertLeafChange = (obj, change, embeddedKey = "$index") => {
|
|
|
723
671
|
return modifyKeyValue(obj, key, oldValue);
|
|
724
672
|
case "REMOVE" /* REMOVE */:
|
|
725
673
|
return addKeyValue(obj, key, value);
|
|
726
|
-
case "MOVE" /* MOVE */:
|
|
727
|
-
return moveArrayElement(obj, key, newIndex, oldIndex, embeddedKey);
|
|
728
674
|
}
|
|
729
675
|
};
|
|
730
676
|
var revertArrayChange = (arr, change) => {
|
|
731
677
|
for (const subchange of change.changes) {
|
|
732
|
-
if (subchange.value != null || subchange.type === "REMOVE" /* REMOVE */
|
|
678
|
+
if (subchange.value != null || subchange.type === "REMOVE" /* REMOVE */) {
|
|
733
679
|
revertLeafChange(arr, subchange, change.embeddedKey);
|
|
734
680
|
} else {
|
|
735
681
|
let element;
|
|
@@ -760,9 +706,11 @@ var revertBranchChange = (obj, change) => {
|
|
|
760
706
|
function append(basePath, nextSegment) {
|
|
761
707
|
return nextSegment.includes(".") ? `${basePath}[${nextSegment}]` : `${basePath}.${nextSegment}`;
|
|
762
708
|
}
|
|
709
|
+
var IDENT_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
763
710
|
function filterExpression(basePath, filterKey, filterValue) {
|
|
764
711
|
const value = typeof filterValue === "number" ? filterValue : `'${filterValue}'`;
|
|
765
|
-
|
|
712
|
+
const memberAccess = typeof filterKey === "string" && !IDENT_RE.test(filterKey) ? `['${filterKey}']` : `.${filterKey}`;
|
|
713
|
+
return `${basePath}[?(@${memberAccess}==${value})]`;
|
|
766
714
|
}
|
|
767
715
|
|
|
768
716
|
// src/jsonCompare.ts
|
|
@@ -796,18 +744,35 @@ var enrich = (object) => {
|
|
|
796
744
|
return createValue(object);
|
|
797
745
|
}
|
|
798
746
|
};
|
|
747
|
+
var buildEnrichedPath = (atomicPath) => {
|
|
748
|
+
const segments = splitJSONPath(atomicPath);
|
|
749
|
+
let result = "value";
|
|
750
|
+
for (let i = 1; i < segments.length; i++) {
|
|
751
|
+
const seg = segments[i];
|
|
752
|
+
const isLast = i === segments.length - 1;
|
|
753
|
+
const arrayMatch = /^(.+?)\[(\d+)\]$/.exec(seg);
|
|
754
|
+
if (arrayMatch) {
|
|
755
|
+
const [, key, index] = arrayMatch;
|
|
756
|
+
result += `.${key}.value[${index}]`;
|
|
757
|
+
} else {
|
|
758
|
+
result += `.${seg}`;
|
|
759
|
+
}
|
|
760
|
+
if (!isLast) {
|
|
761
|
+
result += ".value";
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return result;
|
|
765
|
+
};
|
|
799
766
|
var applyChangelist = (object, changelist) => {
|
|
800
|
-
changelist.
|
|
801
|
-
|
|
802
|
-
path: entry.path.replace(/(\[(?<array>\d)\]\.)/g, "ARRVAL_START$<array>ARRVAL_END")
|
|
803
|
-
})).map((entry) => ({ ...entry, path: entry.path.replace(/(?<dot>\.)/g, ".value$<dot>") })).map((entry) => ({ ...entry, path: entry.path.replace(/\./, "") })).map((entry) => ({ ...entry, path: entry.path.replace(/ARRVAL_START/g, ".value[") })).map((entry) => ({ ...entry, path: entry.path.replace(/ARRVAL_END/g, "].value.") })).forEach((entry) => {
|
|
767
|
+
changelist.forEach((entry) => {
|
|
768
|
+
const path = buildEnrichedPath(entry.path);
|
|
804
769
|
switch (entry.type) {
|
|
805
770
|
case "ADD" /* ADD */:
|
|
806
771
|
case "UPDATE" /* UPDATE */:
|
|
807
|
-
setByPath(object,
|
|
772
|
+
setByPath(object, path, { type: entry.type, value: entry.value, oldValue: entry.oldValue });
|
|
808
773
|
break;
|
|
809
774
|
case "REMOVE" /* REMOVE */:
|
|
810
|
-
setByPath(object,
|
|
775
|
+
setByPath(object, path, { type: entry.type, value: void 0, oldValue: entry.value });
|
|
811
776
|
break;
|
|
812
777
|
default:
|
|
813
778
|
throw new Error();
|
|
@@ -815,23 +780,995 @@ var applyChangelist = (object, changelist) => {
|
|
|
815
780
|
});
|
|
816
781
|
return object;
|
|
817
782
|
};
|
|
783
|
+
var ARRAY_WRAPPER_KEY = "_$arr";
|
|
818
784
|
var compare2 = (oldObject, newObject) => {
|
|
785
|
+
if (Array.isArray(oldObject) || Array.isArray(newObject)) {
|
|
786
|
+
const wrappedOld = { [ARRAY_WRAPPER_KEY]: oldObject };
|
|
787
|
+
const wrappedNew = { [ARRAY_WRAPPER_KEY]: newObject };
|
|
788
|
+
const enriched = enrich(wrappedOld);
|
|
789
|
+
const changes = atomizeChangeset(diff(wrappedOld, wrappedNew));
|
|
790
|
+
const result = applyChangelist(enriched, changes);
|
|
791
|
+
return result.value[ARRAY_WRAPPER_KEY];
|
|
792
|
+
}
|
|
819
793
|
return applyChangelist(enrich(oldObject), atomizeChangeset(diff(oldObject, newObject)));
|
|
820
794
|
};
|
|
795
|
+
var comparisonToDict = (node) => {
|
|
796
|
+
const result = { type: node.type };
|
|
797
|
+
if (node.type === "CONTAINER" /* CONTAINER */) {
|
|
798
|
+
if (Array.isArray(node.value)) {
|
|
799
|
+
const children = node.value;
|
|
800
|
+
const serialized = new Array(children.length);
|
|
801
|
+
for (let i = 0; i < children.length; i++) {
|
|
802
|
+
const child = children[i];
|
|
803
|
+
serialized[i] = child != null ? comparisonToDict(child) : null;
|
|
804
|
+
}
|
|
805
|
+
result.value = serialized;
|
|
806
|
+
} else if (node.value && typeof node.value === "object") {
|
|
807
|
+
const obj = /* @__PURE__ */ Object.create(null);
|
|
808
|
+
for (const [key, child] of Object.entries(
|
|
809
|
+
node.value
|
|
810
|
+
)) {
|
|
811
|
+
if (child == null) continue;
|
|
812
|
+
obj[key] = comparisonToDict(child);
|
|
813
|
+
}
|
|
814
|
+
result.value = obj;
|
|
815
|
+
}
|
|
816
|
+
} else {
|
|
817
|
+
if (node.type === "UNCHANGED" /* UNCHANGED */ || node.type === "ADD" /* ADD */ || node.type === "UPDATE" /* UPDATE */) {
|
|
818
|
+
result.value = node.value;
|
|
819
|
+
}
|
|
820
|
+
if (node.type === "REMOVE" /* REMOVE */ || node.type === "UPDATE" /* UPDATE */) {
|
|
821
|
+
result.oldValue = node.oldValue;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return result;
|
|
825
|
+
};
|
|
826
|
+
var IDENT_RE2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
827
|
+
var comparisonToFlatList = (node, options = {}) => {
|
|
828
|
+
const results = [];
|
|
829
|
+
flattenNode(node, "$", options.includeUnchanged ?? false, results);
|
|
830
|
+
return results;
|
|
831
|
+
};
|
|
832
|
+
function flattenNode(node, path, includeUnchanged, results) {
|
|
833
|
+
if (node.type === "CONTAINER" /* CONTAINER */) {
|
|
834
|
+
if (Array.isArray(node.value)) {
|
|
835
|
+
for (let i = 0; i < node.value.length; i++) {
|
|
836
|
+
const child = node.value[i];
|
|
837
|
+
if (child == null) continue;
|
|
838
|
+
flattenNode(child, `${path}[${i}]`, includeUnchanged, results);
|
|
839
|
+
}
|
|
840
|
+
} else if (node.value && typeof node.value === "object") {
|
|
841
|
+
for (const [key, child] of Object.entries(
|
|
842
|
+
node.value
|
|
843
|
+
)) {
|
|
844
|
+
if (child == null) continue;
|
|
845
|
+
const childPath = IDENT_RE2.test(key) ? `${path}.${key}` : `${path}['${key.replace(/'/g, "''")}']`;
|
|
846
|
+
flattenNode(child, childPath, includeUnchanged, results);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (node.type === "UNCHANGED" /* UNCHANGED */ && !includeUnchanged) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const entry = { path, type: node.type };
|
|
855
|
+
if (node.type === "UNCHANGED" /* UNCHANGED */ || node.type === "ADD" /* ADD */ || node.type === "UPDATE" /* UPDATE */) {
|
|
856
|
+
entry.value = node.value;
|
|
857
|
+
}
|
|
858
|
+
if (node.type === "REMOVE" /* REMOVE */ || node.type === "UPDATE" /* UPDATE */) {
|
|
859
|
+
entry.oldValue = node.oldValue;
|
|
860
|
+
}
|
|
861
|
+
results.push(entry);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/atomPath.ts
|
|
865
|
+
function formatFilterLiteral(value) {
|
|
866
|
+
if (value === null) return "null";
|
|
867
|
+
if (typeof value === "boolean") return String(value);
|
|
868
|
+
if (typeof value === "number") {
|
|
869
|
+
if (!Number.isFinite(value)) throw new Error(`Cannot format non-finite number as filter literal: ${value}`);
|
|
870
|
+
return String(value);
|
|
871
|
+
}
|
|
872
|
+
if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
|
|
873
|
+
throw new Error(`Cannot format filter literal for type ${typeof value}`);
|
|
874
|
+
}
|
|
875
|
+
function parseFilterLiteral(s) {
|
|
876
|
+
if (s === "true") return true;
|
|
877
|
+
if (s === "false") return false;
|
|
878
|
+
if (s === "null") return null;
|
|
879
|
+
if (s.startsWith("'") && s.endsWith("'")) {
|
|
880
|
+
return s.slice(1, -1).replace(/''/g, "'");
|
|
881
|
+
}
|
|
882
|
+
if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(s)) {
|
|
883
|
+
return Number(s);
|
|
884
|
+
}
|
|
885
|
+
throw new Error(`Invalid filter literal: ${s}`);
|
|
886
|
+
}
|
|
887
|
+
function extractQuotedString(s, start) {
|
|
888
|
+
const result = [];
|
|
889
|
+
let i = start;
|
|
890
|
+
while (i < s.length) {
|
|
891
|
+
if (s[i] === "'") {
|
|
892
|
+
if (i + 1 < s.length && s[i + 1] === "'") {
|
|
893
|
+
result.push("'");
|
|
894
|
+
i += 2;
|
|
895
|
+
} else {
|
|
896
|
+
return [result.join(""), i];
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
result.push(s[i]);
|
|
900
|
+
i += 1;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
throw new Error("Unterminated quoted string");
|
|
904
|
+
}
|
|
905
|
+
function findFilterClose(s, from) {
|
|
906
|
+
let i = from;
|
|
907
|
+
while (i < s.length) {
|
|
908
|
+
if (s[i] === "'") {
|
|
909
|
+
i += 1;
|
|
910
|
+
while (i < s.length) {
|
|
911
|
+
if (s[i] === "'") {
|
|
912
|
+
if (i + 1 < s.length && s[i + 1] === "'") {
|
|
913
|
+
i += 2;
|
|
914
|
+
} else {
|
|
915
|
+
i += 1;
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
} else {
|
|
919
|
+
i += 1;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
} else if (s[i] === ")" && i + 1 < s.length && s[i + 1] === "]") {
|
|
923
|
+
return i;
|
|
924
|
+
} else {
|
|
925
|
+
i += 1;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return -1;
|
|
929
|
+
}
|
|
930
|
+
function parseFilter(inner) {
|
|
931
|
+
if (inner.startsWith("@.")) {
|
|
932
|
+
const eq = inner.indexOf("==");
|
|
933
|
+
if (eq === -1) throw new Error(`Invalid filter: missing '==' in ${inner}`);
|
|
934
|
+
const key = inner.slice(2, eq);
|
|
935
|
+
if (!key || !SIMPLE_PROPERTY_RE.test(key)) {
|
|
936
|
+
throw new Error(`Invalid property name in filter: '${key}'. Use bracket notation for non-identifier keys: @['${key}']`);
|
|
937
|
+
}
|
|
938
|
+
return { type: "keyFilter", property: key, value: parseFilterLiteral(inner.slice(eq + 2)) };
|
|
939
|
+
}
|
|
940
|
+
if (inner.startsWith("@['")) {
|
|
941
|
+
const [key, endIdx] = extractQuotedString(inner, 3);
|
|
942
|
+
const valStart = endIdx + 4;
|
|
943
|
+
return { type: "keyFilter", property: key, value: parseFilterLiteral(inner.slice(valStart)) };
|
|
944
|
+
}
|
|
945
|
+
if (inner.startsWith("@==")) {
|
|
946
|
+
return { type: "valueFilter", value: parseFilterLiteral(inner.slice(3)) };
|
|
947
|
+
}
|
|
948
|
+
throw new Error(`Invalid filter expression: ${inner}`);
|
|
949
|
+
}
|
|
950
|
+
function parseAtomPath(path) {
|
|
951
|
+
if (!path.startsWith("$")) {
|
|
952
|
+
throw new Error(`Path must start with '$': ${path}`);
|
|
953
|
+
}
|
|
954
|
+
const segments = [{ type: "root" }];
|
|
955
|
+
let i = 1;
|
|
956
|
+
while (i < path.length) {
|
|
957
|
+
if (path[i] === ".") {
|
|
958
|
+
i += 1;
|
|
959
|
+
const start = i;
|
|
960
|
+
while (i < path.length && /[a-zA-Z0-9_]/.test(path[i])) {
|
|
961
|
+
i += 1;
|
|
962
|
+
}
|
|
963
|
+
if (i === start) throw new Error(`Empty property name at position ${i} in: ${path}`);
|
|
964
|
+
segments.push({ type: "property", name: path.slice(start, i) });
|
|
965
|
+
} else if (path[i] === "[") {
|
|
966
|
+
if (i + 1 >= path.length) throw new Error(`Unexpected end of path after '[': ${path}`);
|
|
967
|
+
if (path[i + 1] === "?") {
|
|
968
|
+
const closingIdx = findFilterClose(path, i + 2);
|
|
969
|
+
if (closingIdx === -1) throw new Error(`Unterminated filter expression in: ${path}`);
|
|
970
|
+
const inner = path.slice(i + 3, closingIdx);
|
|
971
|
+
segments.push(parseFilter(inner));
|
|
972
|
+
i = closingIdx + 2;
|
|
973
|
+
} else if (path[i + 1] === "'") {
|
|
974
|
+
const [key, endIdx] = extractQuotedString(path, i + 2);
|
|
975
|
+
if (path[endIdx + 1] !== "]") throw new Error(`Expected ']' after bracket property in: ${path}`);
|
|
976
|
+
segments.push({ type: "property", name: key });
|
|
977
|
+
i = endIdx + 2;
|
|
978
|
+
} else if (/\d/.test(path[i + 1])) {
|
|
979
|
+
const end = path.indexOf("]", i);
|
|
980
|
+
if (end === -1) throw new Error(`Unterminated array index in: ${path}`);
|
|
981
|
+
const indexStr = path.slice(i + 1, end);
|
|
982
|
+
if (indexStr.length > 1 && indexStr[0] === "0") {
|
|
983
|
+
throw new Error(`Leading zeros not allowed in array index: [${indexStr}]`);
|
|
984
|
+
}
|
|
985
|
+
segments.push({ type: "index", index: Number(indexStr) });
|
|
986
|
+
i = end + 1;
|
|
987
|
+
} else {
|
|
988
|
+
throw new Error(`Unexpected character after '[': '${path[i + 1]}' in: ${path}`);
|
|
989
|
+
}
|
|
990
|
+
} else {
|
|
991
|
+
throw new Error(`Unexpected character '${path[i]}' at position ${i} in: ${path}`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return segments;
|
|
995
|
+
}
|
|
996
|
+
var SIMPLE_PROPERTY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
997
|
+
function formatMemberAccess(name) {
|
|
998
|
+
if (SIMPLE_PROPERTY_RE.test(name)) {
|
|
999
|
+
return `.${name}`;
|
|
1000
|
+
}
|
|
1001
|
+
return `['${name.replace(/'/g, "''")}']`;
|
|
1002
|
+
}
|
|
1003
|
+
function buildAtomPath(segments) {
|
|
1004
|
+
let result = "";
|
|
1005
|
+
for (const seg of segments) {
|
|
1006
|
+
switch (seg.type) {
|
|
1007
|
+
case "root":
|
|
1008
|
+
result += "$";
|
|
1009
|
+
break;
|
|
1010
|
+
case "property":
|
|
1011
|
+
result += formatMemberAccess(seg.name);
|
|
1012
|
+
break;
|
|
1013
|
+
case "index":
|
|
1014
|
+
result += `[${seg.index}]`;
|
|
1015
|
+
break;
|
|
1016
|
+
case "keyFilter": {
|
|
1017
|
+
const memberAccess = SIMPLE_PROPERTY_RE.test(seg.property) ? `.${seg.property}` : `['${seg.property.replace(/'/g, "''")}']`;
|
|
1018
|
+
result += `[?(@${memberAccess}==${formatFilterLiteral(seg.value)})]`;
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
case "valueFilter":
|
|
1022
|
+
result += `[?(@==${formatFilterLiteral(seg.value)})]`;
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return result;
|
|
1027
|
+
}
|
|
1028
|
+
function canonicalizeFilterForAtom(inner) {
|
|
1029
|
+
if (inner.startsWith("@.")) {
|
|
1030
|
+
const eqIdx = inner.indexOf("==");
|
|
1031
|
+
if (eqIdx === -1) return `[?(${inner})]`;
|
|
1032
|
+
const key = inner.slice(2, eqIdx);
|
|
1033
|
+
const valuePart = inner.slice(eqIdx);
|
|
1034
|
+
if (SIMPLE_PROPERTY_RE.test(key)) {
|
|
1035
|
+
return `[?(@.${key}${valuePart})]`;
|
|
1036
|
+
}
|
|
1037
|
+
return `[?(@['${key.replace(/'/g, "''")}']${valuePart})]`;
|
|
1038
|
+
}
|
|
1039
|
+
return `[?(${inner})]`;
|
|
1040
|
+
}
|
|
1041
|
+
function canonicalizeFilterForV4(inner) {
|
|
1042
|
+
if (inner.startsWith("@['")) {
|
|
1043
|
+
const [key, endIdx] = extractQuotedString(inner, 3);
|
|
1044
|
+
const rest = inner.slice(endIdx + 2);
|
|
1045
|
+
if (rest.startsWith("==")) {
|
|
1046
|
+
const literal = rest.slice(2);
|
|
1047
|
+
const normalizedLiteral = normalizeToStringQuoted(literal);
|
|
1048
|
+
return `[?(@.${key}==${normalizedLiteral})]`;
|
|
1049
|
+
}
|
|
1050
|
+
return `[?(${inner})]`;
|
|
1051
|
+
}
|
|
1052
|
+
return normalizeFilterToStringLiterals(`[?(${inner})]`);
|
|
1053
|
+
}
|
|
1054
|
+
function normalizeToStringQuoted(literal) {
|
|
1055
|
+
if (literal.startsWith("'") && literal.endsWith("'")) {
|
|
1056
|
+
return literal;
|
|
1057
|
+
}
|
|
1058
|
+
const value = parseFilterLiteral(literal);
|
|
1059
|
+
const stringValue = String(value).replace(/'/g, "''");
|
|
1060
|
+
return `'${stringValue}'`;
|
|
1061
|
+
}
|
|
1062
|
+
function atomicPathToAtomPath(atomicPath) {
|
|
1063
|
+
if (atomicPath === "$.$root") return "$";
|
|
1064
|
+
if (atomicPath.startsWith("$.$root.")) return "$" + atomicPath.slice(7);
|
|
1065
|
+
if (!atomicPath.startsWith("$")) {
|
|
1066
|
+
throw new Error(`Atomic path must start with '$': ${atomicPath}`);
|
|
1067
|
+
}
|
|
1068
|
+
let result = "$";
|
|
1069
|
+
let i = 1;
|
|
1070
|
+
while (i < atomicPath.length) {
|
|
1071
|
+
if (atomicPath[i] === ".") {
|
|
1072
|
+
i += 1;
|
|
1073
|
+
const start = i;
|
|
1074
|
+
while (i < atomicPath.length && atomicPath[i] !== "." && atomicPath[i] !== "[") {
|
|
1075
|
+
i += 1;
|
|
1076
|
+
}
|
|
1077
|
+
const name = atomicPath.slice(start, i);
|
|
1078
|
+
result += formatMemberAccess(name);
|
|
1079
|
+
} else if (atomicPath[i] === "[") {
|
|
1080
|
+
if (atomicPath[i + 1] === "?") {
|
|
1081
|
+
const closingIdx = findFilterClose(atomicPath, i + 2);
|
|
1082
|
+
if (closingIdx === -1) throw new Error(`Unterminated filter in: ${atomicPath}`);
|
|
1083
|
+
const inner = atomicPath.slice(i + 3, closingIdx);
|
|
1084
|
+
result += canonicalizeFilterForAtom(inner);
|
|
1085
|
+
i = closingIdx + 2;
|
|
1086
|
+
} else if (atomicPath[i + 1] === "'" || /\d/.test(atomicPath[i + 1])) {
|
|
1087
|
+
const end = atomicPath.indexOf("]", i);
|
|
1088
|
+
if (end === -1) throw new Error(`Unterminated bracket in: ${atomicPath}`);
|
|
1089
|
+
result += atomicPath.slice(i, end + 1);
|
|
1090
|
+
i = end + 1;
|
|
1091
|
+
} else {
|
|
1092
|
+
const end = atomicPath.indexOf("]", i);
|
|
1093
|
+
if (end === -1) throw new Error(`Unterminated bracket in: ${atomicPath}`);
|
|
1094
|
+
const name = atomicPath.slice(i + 1, end);
|
|
1095
|
+
result += `['${name.replace(/'/g, "''")}']`;
|
|
1096
|
+
i = end + 1;
|
|
1097
|
+
}
|
|
1098
|
+
} else {
|
|
1099
|
+
throw new Error(`Unexpected character '${atomicPath[i]}' in atomic path: ${atomicPath}`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return result;
|
|
1103
|
+
}
|
|
1104
|
+
function atomPathToAtomicPath(atomPath) {
|
|
1105
|
+
if (!atomPath.startsWith("$")) {
|
|
1106
|
+
throw new Error(`Atom path must start with '$': ${atomPath}`);
|
|
1107
|
+
}
|
|
1108
|
+
if (atomPath === "$") {
|
|
1109
|
+
return "$.$root";
|
|
1110
|
+
}
|
|
1111
|
+
let result = "$";
|
|
1112
|
+
let i = 1;
|
|
1113
|
+
while (i < atomPath.length) {
|
|
1114
|
+
if (atomPath[i] === ".") {
|
|
1115
|
+
i += 1;
|
|
1116
|
+
const start = i;
|
|
1117
|
+
while (i < atomPath.length && /[a-zA-Z0-9_]/.test(atomPath[i])) {
|
|
1118
|
+
i += 1;
|
|
1119
|
+
}
|
|
1120
|
+
result += "." + atomPath.slice(start, i);
|
|
1121
|
+
} else if (atomPath[i] === "[") {
|
|
1122
|
+
if (atomPath[i + 1] === "?") {
|
|
1123
|
+
const closingIdx = findFilterClose(atomPath, i + 2);
|
|
1124
|
+
if (closingIdx === -1) throw new Error(`Unterminated filter in: ${atomPath}`);
|
|
1125
|
+
const inner = atomPath.slice(i + 3, closingIdx);
|
|
1126
|
+
result += canonicalizeFilterForV4(inner);
|
|
1127
|
+
i = closingIdx + 2;
|
|
1128
|
+
} else if (atomPath[i + 1] === "'") {
|
|
1129
|
+
const [key, endIdx] = extractQuotedString(atomPath, i + 2);
|
|
1130
|
+
if (atomPath[endIdx + 1] !== "]") throw new Error(`Expected ']' in: ${atomPath}`);
|
|
1131
|
+
result += `[${key}]`;
|
|
1132
|
+
i = endIdx + 2;
|
|
1133
|
+
} else if (/\d/.test(atomPath[i + 1])) {
|
|
1134
|
+
const end = atomPath.indexOf("]", i);
|
|
1135
|
+
if (end === -1) throw new Error(`Unterminated bracket in: ${atomPath}`);
|
|
1136
|
+
result += atomPath.slice(i, end + 1);
|
|
1137
|
+
i = end + 1;
|
|
1138
|
+
} else {
|
|
1139
|
+
throw new Error(`Unexpected character after '[' in: ${atomPath}`);
|
|
1140
|
+
}
|
|
1141
|
+
} else {
|
|
1142
|
+
throw new Error(`Unexpected character '${atomPath[i]}' in atom path: ${atomPath}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return result;
|
|
1146
|
+
}
|
|
1147
|
+
function normalizeFilterToStringLiterals(filter) {
|
|
1148
|
+
const eqIdx = filter.indexOf("==");
|
|
1149
|
+
if (eqIdx === -1) return filter;
|
|
1150
|
+
const literalStart = eqIdx + 2;
|
|
1151
|
+
const literalEnd = filter.length - 2;
|
|
1152
|
+
const literal = filter.slice(literalStart, literalEnd);
|
|
1153
|
+
if (literal.startsWith("'") && literal.endsWith("'")) {
|
|
1154
|
+
return filter;
|
|
1155
|
+
}
|
|
1156
|
+
const value = parseFilterLiteral(literal);
|
|
1157
|
+
const stringValue = String(value).replace(/'/g, "''");
|
|
1158
|
+
return filter.slice(0, literalStart) + `'${stringValue}'` + filter.slice(literalEnd);
|
|
1159
|
+
}
|
|
1160
|
+
function extractKeyFromAtomicPath(atomicPath) {
|
|
1161
|
+
if (atomicPath === "$.$root") return "$root";
|
|
1162
|
+
if (atomicPath.endsWith(")]")) {
|
|
1163
|
+
const filterStart = atomicPath.lastIndexOf("[?(");
|
|
1164
|
+
if (filterStart !== -1) {
|
|
1165
|
+
const inner = atomicPath.slice(filterStart + 3, atomicPath.length - 2);
|
|
1166
|
+
if (inner.startsWith("@==")) {
|
|
1167
|
+
const val = parseFilterLiteral(inner.slice(3));
|
|
1168
|
+
return String(val);
|
|
1169
|
+
}
|
|
1170
|
+
const eqIdx = inner.indexOf("==");
|
|
1171
|
+
if (eqIdx !== -1) {
|
|
1172
|
+
const val = parseFilterLiteral(inner.slice(eqIdx + 2));
|
|
1173
|
+
return String(val);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (atomicPath.endsWith("]")) {
|
|
1178
|
+
const bracketStart = atomicPath.lastIndexOf("[");
|
|
1179
|
+
if (bracketStart !== -1) {
|
|
1180
|
+
const inner = atomicPath.slice(bracketStart + 1, atomicPath.length - 1);
|
|
1181
|
+
if (/^\d+$/.test(inner)) return inner;
|
|
1182
|
+
return inner;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const lastDot = atomicPath.lastIndexOf(".");
|
|
1186
|
+
if (lastDot > 0) {
|
|
1187
|
+
return atomicPath.slice(lastDot + 1);
|
|
1188
|
+
}
|
|
1189
|
+
return atomicPath;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/jsonAtom.ts
|
|
1193
|
+
function validateAtom(atom) {
|
|
1194
|
+
const errors = [];
|
|
1195
|
+
if (typeof atom !== "object" || atom === null) {
|
|
1196
|
+
return { valid: false, errors: ["Atom must be a non-null object"] };
|
|
1197
|
+
}
|
|
1198
|
+
const d = atom;
|
|
1199
|
+
if (d.format !== "json-atom") {
|
|
1200
|
+
errors.push(`Invalid or missing format: expected 'json-atom', got '${d.format}'`);
|
|
1201
|
+
}
|
|
1202
|
+
if (typeof d.version !== "number") {
|
|
1203
|
+
errors.push(`Missing or invalid version: expected number, got '${typeof d.version}'`);
|
|
1204
|
+
}
|
|
1205
|
+
if (!Array.isArray(d.operations)) {
|
|
1206
|
+
errors.push("Missing or invalid operations: expected array");
|
|
1207
|
+
} else {
|
|
1208
|
+
for (let i = 0; i < d.operations.length; i++) {
|
|
1209
|
+
const op = d.operations[i];
|
|
1210
|
+
if (!op || typeof op !== "object") {
|
|
1211
|
+
errors.push(`operations[${i}]: must be an object`);
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (!["add", "remove", "replace"].includes(op.op)) {
|
|
1215
|
+
errors.push(`operations[${i}]: invalid op '${op.op}'`);
|
|
1216
|
+
}
|
|
1217
|
+
if (typeof op.path !== "string") {
|
|
1218
|
+
errors.push(`operations[${i}]: path must be a string`);
|
|
1219
|
+
}
|
|
1220
|
+
if (op.op === "add") {
|
|
1221
|
+
if (!("value" in op)) {
|
|
1222
|
+
errors.push(`operations[${i}]: add operation must have value`);
|
|
1223
|
+
}
|
|
1224
|
+
if ("oldValue" in op) {
|
|
1225
|
+
errors.push(`operations[${i}]: add operation must not have oldValue`);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (op.op === "remove" && "value" in op) {
|
|
1229
|
+
errors.push(`operations[${i}]: remove operation must not have value`);
|
|
1230
|
+
}
|
|
1231
|
+
if (op.op === "replace" && !("value" in op)) {
|
|
1232
|
+
errors.push(`operations[${i}]: replace operation must have value`);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return { valid: errors.length === 0, errors };
|
|
1237
|
+
}
|
|
1238
|
+
function diffAtom(oldObj, newObj, options = {}) {
|
|
1239
|
+
const changeset = diff(oldObj, newObj, {
|
|
1240
|
+
...options,
|
|
1241
|
+
treatTypeChangeAsReplace: true
|
|
1242
|
+
// Always true — merging REMOVE+ADD is more reliable (B.1)
|
|
1243
|
+
});
|
|
1244
|
+
const operations = [];
|
|
1245
|
+
walkChanges(changeset, "$", oldObj, newObj, operations, options);
|
|
1246
|
+
return {
|
|
1247
|
+
format: "json-atom",
|
|
1248
|
+
version: 1,
|
|
1249
|
+
operations
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function mergeTypeChangePairs(changes) {
|
|
1253
|
+
const result = [];
|
|
1254
|
+
let i = 0;
|
|
1255
|
+
while (i < changes.length) {
|
|
1256
|
+
if (i + 1 < changes.length && changes[i].type === "REMOVE" /* REMOVE */ && changes[i + 1].type === "ADD" /* ADD */ && changes[i].key === changes[i + 1].key) {
|
|
1257
|
+
result.push({
|
|
1258
|
+
...changes[i],
|
|
1259
|
+
isMergedReplace: true,
|
|
1260
|
+
removeValue: changes[i].value,
|
|
1261
|
+
addValue: changes[i + 1].value
|
|
1262
|
+
});
|
|
1263
|
+
i += 2;
|
|
1264
|
+
} else {
|
|
1265
|
+
result.push(changes[i]);
|
|
1266
|
+
i += 1;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return result;
|
|
1270
|
+
}
|
|
1271
|
+
var SIMPLE_PROPERTY_RE2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
1272
|
+
function appendCanonicalProperty(basePath, name) {
|
|
1273
|
+
if (SIMPLE_PROPERTY_RE2.test(name)) {
|
|
1274
|
+
return `${basePath}.${name}`;
|
|
1275
|
+
}
|
|
1276
|
+
return `${basePath}['${name.replace(/'/g, "''")}']`;
|
|
1277
|
+
}
|
|
1278
|
+
function walkChanges(changes, basePath, oldCtx, newCtx, ops, options) {
|
|
1279
|
+
const merged = mergeTypeChangePairs(changes);
|
|
1280
|
+
for (const change of merged) {
|
|
1281
|
+
if (change.isMergedReplace) {
|
|
1282
|
+
const mc = change;
|
|
1283
|
+
const path = mc.key === "$root" ? "$" : appendCanonicalProperty(basePath, mc.key);
|
|
1284
|
+
const op = { op: "replace", path, value: mc.addValue };
|
|
1285
|
+
if (options.reversible !== false) {
|
|
1286
|
+
op.oldValue = mc.removeValue;
|
|
1287
|
+
}
|
|
1288
|
+
ops.push(op);
|
|
1289
|
+
} else if (change.changes) {
|
|
1290
|
+
const childPath = change.key === "$root" ? "$" : appendCanonicalProperty(basePath, change.key);
|
|
1291
|
+
const childOld = change.key === "$root" ? oldCtx : oldCtx?.[change.key];
|
|
1292
|
+
const childNew = change.key === "$root" ? newCtx : newCtx?.[change.key];
|
|
1293
|
+
if (change.embeddedKey) {
|
|
1294
|
+
for (const childChange of change.changes) {
|
|
1295
|
+
const filterPath = buildCanonicalFilterPath(
|
|
1296
|
+
childPath,
|
|
1297
|
+
change.embeddedKey,
|
|
1298
|
+
childChange.key,
|
|
1299
|
+
childOld,
|
|
1300
|
+
childNew,
|
|
1301
|
+
childChange
|
|
1302
|
+
);
|
|
1303
|
+
if (childChange.changes) {
|
|
1304
|
+
const oldEl = findElement(childOld, change.embeddedKey, childChange.key);
|
|
1305
|
+
const newEl = findElement(childNew, change.embeddedKey, childChange.key);
|
|
1306
|
+
walkChanges(childChange.changes, filterPath, oldEl, newEl, ops, options);
|
|
1307
|
+
} else {
|
|
1308
|
+
emitLeafOp(childChange, filterPath, ops, options);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
} else {
|
|
1312
|
+
walkChanges(change.changes, childPath, childOld, childNew, ops, options);
|
|
1313
|
+
}
|
|
1314
|
+
} else {
|
|
1315
|
+
const path = change.key === "$root" ? "$" : appendCanonicalProperty(basePath, change.key);
|
|
1316
|
+
emitLeafOp(change, path, ops, options);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function emitLeafOp(change, path, ops, options) {
|
|
1321
|
+
switch (change.type) {
|
|
1322
|
+
case "ADD" /* ADD */: {
|
|
1323
|
+
ops.push({ op: "add", path, value: change.value });
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
case "REMOVE" /* REMOVE */: {
|
|
1327
|
+
const op = { op: "remove", path };
|
|
1328
|
+
if (options.reversible !== false) {
|
|
1329
|
+
op.oldValue = change.value;
|
|
1330
|
+
}
|
|
1331
|
+
ops.push(op);
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
case "UPDATE" /* UPDATE */: {
|
|
1335
|
+
const op = { op: "replace", path, value: change.value };
|
|
1336
|
+
if (options.reversible !== false) {
|
|
1337
|
+
op.oldValue = change.oldValue;
|
|
1338
|
+
}
|
|
1339
|
+
ops.push(op);
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function buildCanonicalFilterPath(basePath, embeddedKey, changeKey, oldArr, newArr, change) {
|
|
1345
|
+
if (embeddedKey === "$index") {
|
|
1346
|
+
return `${basePath}[${changeKey}]`;
|
|
1347
|
+
}
|
|
1348
|
+
if (embeddedKey === "$value") {
|
|
1349
|
+
const typedVal2 = findActualValue(oldArr, newArr, changeKey, change.type);
|
|
1350
|
+
return `${basePath}[?(@==${formatFilterLiteral(typedVal2)})]`;
|
|
1351
|
+
}
|
|
1352
|
+
if (typeof embeddedKey === "function") {
|
|
1353
|
+
const sample = oldArr && oldArr.length > 0 ? oldArr[0] : newArr?.[0];
|
|
1354
|
+
const keyName = sample ? embeddedKey(sample, true) : changeKey;
|
|
1355
|
+
const element2 = findElementByFn(oldArr, newArr, embeddedKey, changeKey, change.type);
|
|
1356
|
+
if (element2 && typeof keyName === "string") {
|
|
1357
|
+
const typedVal2 = element2[keyName];
|
|
1358
|
+
const memberAccess3 = SIMPLE_PROPERTY_RE2.test(keyName) ? `.${keyName}` : `['${keyName.replace(/'/g, "''")}']`;
|
|
1359
|
+
return `${basePath}[?(@${memberAccess3}==${formatFilterLiteral(typedVal2)})]`;
|
|
1360
|
+
}
|
|
1361
|
+
const memberAccess2 = typeof keyName === "string" && SIMPLE_PROPERTY_RE2.test(keyName) ? `.${keyName}` : `.${changeKey}`;
|
|
1362
|
+
return `${basePath}[?(@${memberAccess2}=='${changeKey}')]`;
|
|
1363
|
+
}
|
|
1364
|
+
const element = findElementByKey(oldArr, newArr, embeddedKey, changeKey, change.type);
|
|
1365
|
+
const typedVal = element ? element[embeddedKey] : changeKey;
|
|
1366
|
+
const memberAccess = SIMPLE_PROPERTY_RE2.test(embeddedKey) ? `.${embeddedKey}` : `['${embeddedKey.replace(/'/g, "''")}']`;
|
|
1367
|
+
return `${basePath}[?(@${memberAccess}==${formatFilterLiteral(typedVal)})]`;
|
|
1368
|
+
}
|
|
1369
|
+
function findActualValue(oldArr, newArr, stringKey, opType) {
|
|
1370
|
+
if (opType === "REMOVE" /* REMOVE */ && oldArr) {
|
|
1371
|
+
for (const item of oldArr) {
|
|
1372
|
+
if (String(item) === stringKey) return item;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (opType === "ADD" /* ADD */ && newArr) {
|
|
1376
|
+
for (const item of newArr) {
|
|
1377
|
+
if (String(item) === stringKey) return item;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (oldArr) {
|
|
1381
|
+
for (const item of oldArr) {
|
|
1382
|
+
if (String(item) === stringKey) return item;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (newArr) {
|
|
1386
|
+
for (const item of newArr) {
|
|
1387
|
+
if (String(item) === stringKey) return item;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return stringKey;
|
|
1391
|
+
}
|
|
1392
|
+
function findElement(arr, embeddedKey, changeKey) {
|
|
1393
|
+
if (!arr || !Array.isArray(arr)) return void 0;
|
|
1394
|
+
if (embeddedKey === "$index") {
|
|
1395
|
+
return arr[Number(changeKey)];
|
|
1396
|
+
}
|
|
1397
|
+
if (embeddedKey === "$value") {
|
|
1398
|
+
return arr.find((item) => String(item) === changeKey);
|
|
1399
|
+
}
|
|
1400
|
+
if (typeof embeddedKey === "function") {
|
|
1401
|
+
return arr.find((item) => String(embeddedKey(item)) === changeKey);
|
|
1402
|
+
}
|
|
1403
|
+
return arr.find((item) => item && String(item[embeddedKey]) === changeKey);
|
|
1404
|
+
}
|
|
1405
|
+
function findElementByKey(oldArr, newArr, embeddedKey, changeKey, opType) {
|
|
1406
|
+
if (opType === "REMOVE" /* REMOVE */ || opType === "UPDATE" /* UPDATE */) {
|
|
1407
|
+
const el = oldArr?.find((item) => item && String(item[embeddedKey]) === changeKey);
|
|
1408
|
+
if (el) return el;
|
|
1409
|
+
}
|
|
1410
|
+
if (opType === "ADD" /* ADD */ || opType === "UPDATE" /* UPDATE */) {
|
|
1411
|
+
const el = newArr?.find((item) => item && String(item[embeddedKey]) === changeKey);
|
|
1412
|
+
if (el) return el;
|
|
1413
|
+
}
|
|
1414
|
+
return void 0;
|
|
1415
|
+
}
|
|
1416
|
+
function findElementByFn(oldArr, newArr, fn, changeKey, opType) {
|
|
1417
|
+
if (opType === "REMOVE" /* REMOVE */ || opType === "UPDATE" /* UPDATE */) {
|
|
1418
|
+
const el = oldArr?.find((item) => String(fn(item)) === changeKey);
|
|
1419
|
+
if (el) return el;
|
|
1420
|
+
}
|
|
1421
|
+
if (opType === "ADD" /* ADD */ || opType === "UPDATE" /* UPDATE */) {
|
|
1422
|
+
const el = newArr?.find((item) => String(fn(item)) === changeKey);
|
|
1423
|
+
if (el) return el;
|
|
1424
|
+
}
|
|
1425
|
+
return void 0;
|
|
1426
|
+
}
|
|
1427
|
+
function toAtom(changeset, options = {}) {
|
|
1428
|
+
let atoms;
|
|
1429
|
+
if (changeset.length === 0) {
|
|
1430
|
+
return { format: "json-atom", version: 1, operations: [] };
|
|
1431
|
+
}
|
|
1432
|
+
if ("path" in changeset[0]) {
|
|
1433
|
+
atoms = changeset;
|
|
1434
|
+
} else {
|
|
1435
|
+
atoms = atomizeChangeset(changeset);
|
|
1436
|
+
}
|
|
1437
|
+
const rawOps = atoms.map((atom) => {
|
|
1438
|
+
const path = atomicPathToAtomPath(atom.path);
|
|
1439
|
+
switch (atom.type) {
|
|
1440
|
+
case "ADD" /* ADD */:
|
|
1441
|
+
return { op: "add", path, value: atom.value };
|
|
1442
|
+
case "REMOVE" /* REMOVE */: {
|
|
1443
|
+
const op = { op: "remove", path };
|
|
1444
|
+
if (options.reversible !== false && atom.value !== void 0) {
|
|
1445
|
+
op.oldValue = atom.value;
|
|
1446
|
+
}
|
|
1447
|
+
return op;
|
|
1448
|
+
}
|
|
1449
|
+
case "UPDATE" /* UPDATE */: {
|
|
1450
|
+
const op = { op: "replace", path, value: atom.value };
|
|
1451
|
+
if (options.reversible !== false && atom.oldValue !== void 0) {
|
|
1452
|
+
op.oldValue = atom.oldValue;
|
|
1453
|
+
}
|
|
1454
|
+
return op;
|
|
1455
|
+
}
|
|
1456
|
+
/* istanbul ignore next -- exhaustive switch */
|
|
1457
|
+
default:
|
|
1458
|
+
throw new Error(`Unknown operation type: ${atom.type}`);
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
const operations = mergeConsecutiveOps(rawOps);
|
|
1462
|
+
return { format: "json-atom", version: 1, operations };
|
|
1463
|
+
}
|
|
1464
|
+
function mergeConsecutiveOps(ops) {
|
|
1465
|
+
const result = [];
|
|
1466
|
+
let i = 0;
|
|
1467
|
+
while (i < ops.length) {
|
|
1468
|
+
if (i + 1 < ops.length && ops[i].op === "remove" && ops[i + 1].op === "add" && ops[i].path === ops[i + 1].path) {
|
|
1469
|
+
const merged = {
|
|
1470
|
+
op: "replace",
|
|
1471
|
+
path: ops[i].path,
|
|
1472
|
+
value: ops[i + 1].value
|
|
1473
|
+
};
|
|
1474
|
+
if (ops[i].oldValue !== void 0) {
|
|
1475
|
+
merged.oldValue = ops[i].oldValue;
|
|
1476
|
+
}
|
|
1477
|
+
result.push(merged);
|
|
1478
|
+
i += 2;
|
|
1479
|
+
} else {
|
|
1480
|
+
result.push(ops[i]);
|
|
1481
|
+
i += 1;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
return result;
|
|
1485
|
+
}
|
|
1486
|
+
function fromAtom(atom) {
|
|
1487
|
+
const validation = validateAtom(atom);
|
|
1488
|
+
if (!validation.valid) {
|
|
1489
|
+
throw new Error(`Invalid atom: ${validation.errors.join(", ")}`);
|
|
1490
|
+
}
|
|
1491
|
+
return atom.operations.map((op) => {
|
|
1492
|
+
const atomicPath = atomPathToAtomicPath(op.path);
|
|
1493
|
+
const key = extractKeyFromAtomicPath(atomicPath);
|
|
1494
|
+
switch (op.op) {
|
|
1495
|
+
case "add": {
|
|
1496
|
+
const valueType = getValueType(op.value);
|
|
1497
|
+
return { type: "ADD" /* ADD */, key, path: atomicPath, valueType, value: op.value };
|
|
1498
|
+
}
|
|
1499
|
+
case "remove": {
|
|
1500
|
+
const valueType = op.oldValue !== void 0 ? getValueType(op.oldValue) : null;
|
|
1501
|
+
return { type: "REMOVE" /* REMOVE */, key, path: atomicPath, valueType, value: op.oldValue };
|
|
1502
|
+
}
|
|
1503
|
+
case "replace": {
|
|
1504
|
+
const valueType = getValueType(op.value);
|
|
1505
|
+
const atom2 = { type: "UPDATE" /* UPDATE */, key, path: atomicPath, valueType, value: op.value };
|
|
1506
|
+
if (op.oldValue !== void 0) {
|
|
1507
|
+
atom2.oldValue = op.oldValue;
|
|
1508
|
+
}
|
|
1509
|
+
return atom2;
|
|
1510
|
+
}
|
|
1511
|
+
/* istanbul ignore next -- exhaustive switch */
|
|
1512
|
+
default:
|
|
1513
|
+
throw new Error(`Unknown operation: ${op.op}`);
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
function getValueType(value) {
|
|
1518
|
+
if (value === void 0) return "undefined";
|
|
1519
|
+
if (value === null) return null;
|
|
1520
|
+
if (Array.isArray(value)) return "Array";
|
|
1521
|
+
const type = typeof value;
|
|
1522
|
+
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
1523
|
+
}
|
|
1524
|
+
function invertAtom(atom) {
|
|
1525
|
+
const validation = validateAtom(atom);
|
|
1526
|
+
if (!validation.valid) {
|
|
1527
|
+
throw new Error(`Invalid atom: ${validation.errors.join(", ")}`);
|
|
1528
|
+
}
|
|
1529
|
+
for (let i = 0; i < atom.operations.length; i++) {
|
|
1530
|
+
const op = atom.operations[i];
|
|
1531
|
+
if (op.op === "replace" && !("oldValue" in op)) {
|
|
1532
|
+
throw new Error(`operations[${i}]: replace operation missing oldValue \u2014 atom is not reversible`);
|
|
1533
|
+
}
|
|
1534
|
+
if (op.op === "remove" && !("oldValue" in op)) {
|
|
1535
|
+
throw new Error(`operations[${i}]: remove operation missing oldValue \u2014 atom is not reversible`);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const invertedOps = [...atom.operations].reverse().map((op) => {
|
|
1539
|
+
const extensions = {};
|
|
1540
|
+
for (const key of Object.keys(op)) {
|
|
1541
|
+
if (!["op", "path", "value", "oldValue"].includes(key)) {
|
|
1542
|
+
extensions[key] = op[key];
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
switch (op.op) {
|
|
1546
|
+
case "add":
|
|
1547
|
+
return { op: "remove", path: op.path, oldValue: op.value, ...extensions };
|
|
1548
|
+
case "remove":
|
|
1549
|
+
return { op: "add", path: op.path, value: op.oldValue, ...extensions };
|
|
1550
|
+
case "replace":
|
|
1551
|
+
return { op: "replace", path: op.path, value: op.oldValue, oldValue: op.value, ...extensions };
|
|
1552
|
+
/* istanbul ignore next -- exhaustive switch */
|
|
1553
|
+
default:
|
|
1554
|
+
throw new Error(`Unknown operation: ${op.op}`);
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
const envelope = { format: "json-atom", version: atom.version, operations: invertedOps };
|
|
1558
|
+
for (const key of Object.keys(atom)) {
|
|
1559
|
+
if (!["format", "version", "operations"].includes(key)) {
|
|
1560
|
+
envelope[key] = atom[key];
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return envelope;
|
|
1564
|
+
}
|
|
1565
|
+
function applyAtom(obj, atom) {
|
|
1566
|
+
const validation = validateAtom(atom);
|
|
1567
|
+
if (!validation.valid) {
|
|
1568
|
+
throw new Error(`Invalid atom: ${validation.errors.join(", ")}`);
|
|
1569
|
+
}
|
|
1570
|
+
let result = obj;
|
|
1571
|
+
for (const op of atom.operations) {
|
|
1572
|
+
if (op.path === "$") {
|
|
1573
|
+
result = applyRootOp(result, op);
|
|
1574
|
+
} else {
|
|
1575
|
+
const atomicChange = atomOpToAtomicChange(op);
|
|
1576
|
+
const miniChangeset = unatomizeChangeset([atomicChange]);
|
|
1577
|
+
applyChangeset(result, miniChangeset);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return result;
|
|
1581
|
+
}
|
|
1582
|
+
function applyRootOp(obj, op) {
|
|
1583
|
+
switch (op.op) {
|
|
1584
|
+
case "add":
|
|
1585
|
+
return op.value;
|
|
1586
|
+
case "remove":
|
|
1587
|
+
return null;
|
|
1588
|
+
case "replace": {
|
|
1589
|
+
if (typeof obj === "object" && obj !== null && !Array.isArray(obj) && typeof op.value === "object" && op.value !== null && !Array.isArray(op.value)) {
|
|
1590
|
+
for (const key of Object.keys(obj)) {
|
|
1591
|
+
delete obj[key];
|
|
1592
|
+
}
|
|
1593
|
+
Object.assign(obj, op.value);
|
|
1594
|
+
return obj;
|
|
1595
|
+
}
|
|
1596
|
+
return op.value;
|
|
1597
|
+
}
|
|
1598
|
+
/* istanbul ignore next -- exhaustive switch */
|
|
1599
|
+
default:
|
|
1600
|
+
throw new Error(`Unknown operation: ${op.op}`);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
function atomOpToAtomicChange(op) {
|
|
1604
|
+
const atomicPath = atomPathToAtomicPath(op.path);
|
|
1605
|
+
const key = extractKeyFromAtomicPath(atomicPath);
|
|
1606
|
+
switch (op.op) {
|
|
1607
|
+
case "add":
|
|
1608
|
+
return { type: "ADD" /* ADD */, key, path: atomicPath, valueType: getValueType(op.value), value: op.value };
|
|
1609
|
+
case "remove":
|
|
1610
|
+
return { type: "REMOVE" /* REMOVE */, key, path: atomicPath, valueType: getValueType(op.oldValue), value: op.oldValue };
|
|
1611
|
+
case "replace":
|
|
1612
|
+
return {
|
|
1613
|
+
type: "UPDATE" /* UPDATE */,
|
|
1614
|
+
key,
|
|
1615
|
+
path: atomicPath,
|
|
1616
|
+
valueType: getValueType(op.value),
|
|
1617
|
+
value: op.value,
|
|
1618
|
+
oldValue: op.oldValue
|
|
1619
|
+
};
|
|
1620
|
+
/* istanbul ignore next -- exhaustive switch */
|
|
1621
|
+
default:
|
|
1622
|
+
throw new Error(`Unknown operation: ${op.op}`);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
function revertAtom(obj, atom) {
|
|
1626
|
+
const inverse = invertAtom(atom);
|
|
1627
|
+
return applyAtom(obj, inverse);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/atomHelpers.ts
|
|
1631
|
+
var OP_SPEC_KEYS = /* @__PURE__ */ new Set(["op", "path", "value", "oldValue"]);
|
|
1632
|
+
var ATOM_SPEC_KEYS = /* @__PURE__ */ new Set(["format", "version", "operations"]);
|
|
1633
|
+
function operationSpecDict(op) {
|
|
1634
|
+
const result = { op: op.op, path: op.path };
|
|
1635
|
+
if ("value" in op) result.value = op.value;
|
|
1636
|
+
if ("oldValue" in op) result.oldValue = op.oldValue;
|
|
1637
|
+
return result;
|
|
1638
|
+
}
|
|
1639
|
+
function operationExtensions(op) {
|
|
1640
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
1641
|
+
for (const key of Object.keys(op)) {
|
|
1642
|
+
if (!OP_SPEC_KEYS.has(key)) {
|
|
1643
|
+
result[key] = op[key];
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return result;
|
|
1647
|
+
}
|
|
1648
|
+
function leafProperty(op) {
|
|
1649
|
+
const segments = parseAtomPath(op.path);
|
|
1650
|
+
if (segments.length === 0) return null;
|
|
1651
|
+
const last = segments[segments.length - 1];
|
|
1652
|
+
return last.type === "property" ? last.name : null;
|
|
1653
|
+
}
|
|
1654
|
+
function atomSpecDict(atom) {
|
|
1655
|
+
return {
|
|
1656
|
+
format: atom.format,
|
|
1657
|
+
version: atom.version,
|
|
1658
|
+
operations: atom.operations.map(operationSpecDict)
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
function atomExtensions(atom) {
|
|
1662
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
1663
|
+
for (const key of Object.keys(atom)) {
|
|
1664
|
+
if (!ATOM_SPEC_KEYS.has(key)) {
|
|
1665
|
+
result[key] = atom[key];
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return result;
|
|
1669
|
+
}
|
|
1670
|
+
function atomMap(atom, fn) {
|
|
1671
|
+
return { ...atom, operations: atom.operations.map((op, i) => fn(op, i)) };
|
|
1672
|
+
}
|
|
1673
|
+
function atomStamp(atom, extensions) {
|
|
1674
|
+
return atomMap(atom, (op) => ({ ...op, ...extensions }));
|
|
1675
|
+
}
|
|
1676
|
+
function atomGroupBy(atom, keyFn) {
|
|
1677
|
+
const groups = /* @__PURE__ */ Object.create(null);
|
|
1678
|
+
for (const op of atom.operations) {
|
|
1679
|
+
const k = keyFn(op);
|
|
1680
|
+
if (!groups[k]) groups[k] = [];
|
|
1681
|
+
groups[k].push(op);
|
|
1682
|
+
}
|
|
1683
|
+
const envelope = /* @__PURE__ */ Object.create(null);
|
|
1684
|
+
for (const key of Object.keys(atom)) {
|
|
1685
|
+
if (key !== "operations") {
|
|
1686
|
+
envelope[key] = atom[key];
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
1690
|
+
for (const [k, ops] of Object.entries(groups)) {
|
|
1691
|
+
result[k] = { ...envelope, operations: ops };
|
|
1692
|
+
}
|
|
1693
|
+
return result;
|
|
1694
|
+
}
|
|
1695
|
+
function deepClone(obj) {
|
|
1696
|
+
return JSON.parse(JSON.stringify(obj));
|
|
1697
|
+
}
|
|
1698
|
+
function squashAtoms(source, atoms, options = {}) {
|
|
1699
|
+
const { target, verifyTarget = true, ...diffOptions } = options;
|
|
1700
|
+
let final;
|
|
1701
|
+
if (target !== void 0 && atoms.length > 0 && verifyTarget) {
|
|
1702
|
+
let computed = deepClone(source);
|
|
1703
|
+
for (const d of atoms) {
|
|
1704
|
+
computed = applyAtom(computed, d);
|
|
1705
|
+
}
|
|
1706
|
+
const verification = diffAtom(computed, target, diffOptions);
|
|
1707
|
+
if (verification.operations.length > 0) {
|
|
1708
|
+
throw new Error(
|
|
1709
|
+
"squashAtoms: provided target does not match sequential application of atoms to source"
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
final = target;
|
|
1713
|
+
} else if (target !== void 0) {
|
|
1714
|
+
final = target;
|
|
1715
|
+
} else {
|
|
1716
|
+
final = deepClone(source);
|
|
1717
|
+
for (const d of atoms) {
|
|
1718
|
+
final = applyAtom(final, d);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
const result = diffAtom(source, final, diffOptions);
|
|
1722
|
+
for (const d of atoms) {
|
|
1723
|
+
for (const key of Object.keys(d)) {
|
|
1724
|
+
if (!ATOM_SPEC_KEYS.has(key)) {
|
|
1725
|
+
Object.defineProperty(result, key, {
|
|
1726
|
+
value: d[key],
|
|
1727
|
+
writable: true,
|
|
1728
|
+
enumerable: true,
|
|
1729
|
+
configurable: true
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return result;
|
|
1735
|
+
}
|
|
821
1736
|
// Annotate the CommonJS export names for ESM import in node:
|
|
822
1737
|
0 && (module.exports = {
|
|
823
1738
|
CompareOperation,
|
|
824
1739
|
Operation,
|
|
1740
|
+
applyAtom,
|
|
825
1741
|
applyChangelist,
|
|
826
1742
|
applyChangeset,
|
|
1743
|
+
atomExtensions,
|
|
1744
|
+
atomGroupBy,
|
|
1745
|
+
atomMap,
|
|
1746
|
+
atomSpecDict,
|
|
1747
|
+
atomStamp,
|
|
827
1748
|
atomizeChangeset,
|
|
1749
|
+
buildAtomPath,
|
|
828
1750
|
compare,
|
|
1751
|
+
comparisonToDict,
|
|
1752
|
+
comparisonToFlatList,
|
|
829
1753
|
createContainer,
|
|
830
1754
|
createValue,
|
|
831
1755
|
diff,
|
|
1756
|
+
diffAtom,
|
|
832
1757
|
enrich,
|
|
1758
|
+
formatFilterLiteral,
|
|
1759
|
+
fromAtom,
|
|
833
1760
|
getTypeOfObj,
|
|
1761
|
+
invertAtom,
|
|
1762
|
+
leafProperty,
|
|
1763
|
+
operationExtensions,
|
|
1764
|
+
operationSpecDict,
|
|
1765
|
+
parseAtomPath,
|
|
1766
|
+
parseFilterLiteral,
|
|
1767
|
+
revertAtom,
|
|
834
1768
|
revertChangeset,
|
|
835
|
-
|
|
1769
|
+
squashAtoms,
|
|
1770
|
+
toAtom,
|
|
1771
|
+
unatomizeChangeset,
|
|
1772
|
+
validateAtom
|
|
836
1773
|
});
|
|
837
1774
|
//# sourceMappingURL=index.cjs.map
|