json-diff-ts 5.0.0-alpha.8 → 5.0.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 +41 -7
- package/dist/index.cjs +1082 -1032
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -7
- package/dist/index.d.ts +6 -7
- package/dist/index.js +1082 -1032
- package/dist/index.js.map +1 -1
- package/package.json +16 -2
package/dist/index.js
CHANGED
|
@@ -1,328 +1,619 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
function
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const char = path[i];
|
|
9
|
-
if (char === "'" && path[i - 1] !== "\\") {
|
|
10
|
-
inSingleQuotes = !inSingleQuotes;
|
|
11
|
-
} else if (char === "[" && !inSingleQuotes) {
|
|
12
|
-
inBrackets++;
|
|
13
|
-
} else if (char === "]" && !inSingleQuotes) {
|
|
14
|
-
inBrackets--;
|
|
15
|
-
}
|
|
16
|
-
if (char === "." && !inSingleQuotes && inBrackets === 0) {
|
|
17
|
-
parts.push(currentPart);
|
|
18
|
-
currentPart = "";
|
|
19
|
-
} else {
|
|
20
|
-
currentPart += char;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
if (currentPart !== "") {
|
|
24
|
-
parts.push(currentPart);
|
|
25
|
-
}
|
|
26
|
-
return parts;
|
|
27
|
-
}
|
|
28
|
-
function arrayDifference(first, second) {
|
|
29
|
-
const secondSet = new Set(second);
|
|
30
|
-
return first.filter((item) => !secondSet.has(item));
|
|
31
|
-
}
|
|
32
|
-
function arrayIntersection(first, second) {
|
|
33
|
-
const secondSet = new Set(second);
|
|
34
|
-
return first.filter((item) => secondSet.has(item));
|
|
35
|
-
}
|
|
36
|
-
function keyBy(arr, getKey2) {
|
|
37
|
-
const result = {};
|
|
38
|
-
for (const [index, item] of Object.entries(arr)) {
|
|
39
|
-
result[String(getKey2(item, false, Number(index)))] = item;
|
|
1
|
+
// src/atomPath.ts
|
|
2
|
+
function formatFilterLiteral(value) {
|
|
3
|
+
if (value === null) return "null";
|
|
4
|
+
if (typeof value === "boolean") return String(value);
|
|
5
|
+
if (typeof value === "number") {
|
|
6
|
+
if (!Number.isFinite(value)) throw new Error(`Cannot format non-finite number as filter literal: ${value}`);
|
|
7
|
+
return String(value);
|
|
40
8
|
}
|
|
41
|
-
return
|
|
9
|
+
if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
|
|
10
|
+
throw new Error(`Cannot format filter literal for type ${typeof value}`);
|
|
42
11
|
}
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
current = current[part];
|
|
12
|
+
function parseFilterLiteral(s) {
|
|
13
|
+
if (s === "true") return true;
|
|
14
|
+
if (s === "false") return false;
|
|
15
|
+
if (s === "null") return null;
|
|
16
|
+
if (s.startsWith("'") && s.endsWith("'")) {
|
|
17
|
+
const [value, end] = extractQuotedString(s, 1);
|
|
18
|
+
if (end !== s.length - 1) throw new Error(`Invalid filter literal: ${s}`);
|
|
19
|
+
return value;
|
|
52
20
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
var Operation = /* @__PURE__ */ ((Operation2) => {
|
|
58
|
-
Operation2["REMOVE"] = "REMOVE";
|
|
59
|
-
Operation2["ADD"] = "ADD";
|
|
60
|
-
Operation2["UPDATE"] = "UPDATE";
|
|
61
|
-
return Operation2;
|
|
62
|
-
})(Operation || {});
|
|
63
|
-
function diff(oldObj, newObj, options = {}) {
|
|
64
|
-
let embeddedObjKeys = options.arrayIdentityKeys ?? options.embeddedObjKeys;
|
|
65
|
-
const { keysToSkip, treatTypeChangeAsReplace } = options;
|
|
66
|
-
if (embeddedObjKeys instanceof Map) {
|
|
67
|
-
embeddedObjKeys = new Map(
|
|
68
|
-
Array.from(embeddedObjKeys.entries()).map(([key, value]) => [
|
|
69
|
-
key instanceof RegExp ? key : key.replace(/^\./, ""),
|
|
70
|
-
value
|
|
71
|
-
])
|
|
72
|
-
);
|
|
73
|
-
} else if (embeddedObjKeys) {
|
|
74
|
-
embeddedObjKeys = Object.fromEntries(
|
|
75
|
-
Object.entries(embeddedObjKeys).map(([key, value]) => [key.replace(/^\./, ""), value])
|
|
76
|
-
);
|
|
21
|
+
if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(s)) {
|
|
22
|
+
const value = Number(s);
|
|
23
|
+
if (!Number.isFinite(value)) throw new Error(`Invalid non-finite filter literal: ${s}`);
|
|
24
|
+
return value;
|
|
77
25
|
}
|
|
78
|
-
|
|
79
|
-
embeddedObjKeys,
|
|
80
|
-
keysToSkip: keysToSkip ?? [],
|
|
81
|
-
treatTypeChangeAsReplace: treatTypeChangeAsReplace ?? true
|
|
82
|
-
});
|
|
26
|
+
throw new Error(`Invalid filter literal: ${s}`);
|
|
83
27
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
return obj;
|
|
96
|
-
};
|
|
97
|
-
var revertChangeset = (obj, changeset) => {
|
|
98
|
-
if (changeset) {
|
|
99
|
-
changeset.reverse().forEach((change) => {
|
|
100
|
-
const { value, type } = change;
|
|
101
|
-
if (!change.changes || value === null && type === "REMOVE" /* REMOVE */) {
|
|
102
|
-
revertLeafChange(obj, change);
|
|
28
|
+
function extractQuotedString(s, start) {
|
|
29
|
+
const result = [];
|
|
30
|
+
let i = start;
|
|
31
|
+
while (i < s.length) {
|
|
32
|
+
if (s[i] === "'") {
|
|
33
|
+
if (i + 1 < s.length && s[i + 1] === "'") {
|
|
34
|
+
result.push("'");
|
|
35
|
+
i += 2;
|
|
103
36
|
} else {
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
return obj;
|
|
109
|
-
};
|
|
110
|
-
var atomizeChangeset = (obj, path = "$", embeddedKey, embeddedKeyIsPath) => {
|
|
111
|
-
if (Array.isArray(obj)) {
|
|
112
|
-
return handleArray(obj, path, embeddedKey, embeddedKeyIsPath);
|
|
113
|
-
} else if (obj.changes || embeddedKey) {
|
|
114
|
-
if (embeddedKey) {
|
|
115
|
-
const [updatedPath, atomicChange] = handleEmbeddedKey(embeddedKey, obj, path, embeddedKeyIsPath);
|
|
116
|
-
path = updatedPath;
|
|
117
|
-
if (atomicChange) {
|
|
118
|
-
return atomicChange;
|
|
37
|
+
return [result.join(""), i];
|
|
119
38
|
}
|
|
120
39
|
} else {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return atomizeChangeset(obj.changes || obj, path, obj.embeddedKey, obj.embeddedKeyIsPath);
|
|
124
|
-
} else {
|
|
125
|
-
const valueType = getTypeOfObj(obj.value);
|
|
126
|
-
let finalPath = path;
|
|
127
|
-
if (!finalPath.endsWith(`[${obj.key}]`)) {
|
|
128
|
-
let endsWithFilterValue = false;
|
|
129
|
-
const filterEndIdx = path.lastIndexOf(")]");
|
|
130
|
-
if (filterEndIdx !== -1) {
|
|
131
|
-
const filterStartIdx = path.lastIndexOf("==", filterEndIdx);
|
|
132
|
-
if (filterStartIdx !== -1) {
|
|
133
|
-
const filterValue = path.slice(filterStartIdx + 2, filterEndIdx).replaceAll(/(^'|'$)/g, "");
|
|
134
|
-
endsWithFilterValue = filterValue === String(obj.key);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
if (!endsWithFilterValue) {
|
|
138
|
-
finalPath = append(path, obj.key);
|
|
139
|
-
}
|
|
40
|
+
result.push(s[i]);
|
|
41
|
+
i += 1;
|
|
140
42
|
}
|
|
141
|
-
return [
|
|
142
|
-
{
|
|
143
|
-
...obj,
|
|
144
|
-
path: finalPath,
|
|
145
|
-
valueType
|
|
146
|
-
}
|
|
147
|
-
];
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
function handleEmbeddedKey(embeddedKey, obj, path, isPath) {
|
|
151
|
-
if (embeddedKey === "$index") {
|
|
152
|
-
path = `${path}[${obj.key}]`;
|
|
153
|
-
return [path];
|
|
154
|
-
} else if (embeddedKey === "$value") {
|
|
155
|
-
path = `${path}[?(@=='${obj.key}')]`;
|
|
156
|
-
const valueType = getTypeOfObj(obj.value);
|
|
157
|
-
return [
|
|
158
|
-
path,
|
|
159
|
-
[
|
|
160
|
-
{
|
|
161
|
-
...obj,
|
|
162
|
-
path,
|
|
163
|
-
valueType
|
|
164
|
-
}
|
|
165
|
-
]
|
|
166
|
-
];
|
|
167
|
-
} else {
|
|
168
|
-
path = filterExpression(path, embeddedKey, obj.key, isPath);
|
|
169
|
-
return [path];
|
|
170
43
|
}
|
|
44
|
+
throw new Error("Unterminated quoted string");
|
|
171
45
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const obj = {};
|
|
182
|
-
let ptr = obj;
|
|
183
|
-
const segments = splitJSONPath(change.path);
|
|
184
|
-
if (segments.length === 1) {
|
|
185
|
-
ptr.key = change.key;
|
|
186
|
-
ptr.type = change.type;
|
|
187
|
-
ptr.value = change.value;
|
|
188
|
-
ptr.oldValue = change.oldValue;
|
|
189
|
-
changesArr.push(ptr);
|
|
190
|
-
} else {
|
|
191
|
-
for (let i = 1; i < segments.length; i++) {
|
|
192
|
-
const segment = segments[i];
|
|
193
|
-
const result = /^([^[\]]+)\[\?\(@(?:\.?([^=[]*)|(?:\['([^']*(?:''[^']*)*)'\]))=+'([^']*(?:''[^']*)*)'\)\]$|^(.+)\[(\d+)\]$/.exec(segment);
|
|
194
|
-
if (result) {
|
|
195
|
-
let key;
|
|
196
|
-
let embeddedKey;
|
|
197
|
-
let arrKey;
|
|
198
|
-
let isPath;
|
|
199
|
-
if (result[1]) {
|
|
200
|
-
key = result[1];
|
|
201
|
-
embeddedKey = result[3]?.replace(/''/g, "'") || result[2] || "$value";
|
|
202
|
-
isPath = !result[3] && !!result[2] && result[2].includes(".") && NESTED_PATH_RE.test(result[2]) ? true : void 0;
|
|
203
|
-
arrKey = result[4]?.replace(/''/g, "'");
|
|
204
|
-
} else {
|
|
205
|
-
key = result[5];
|
|
206
|
-
embeddedKey = "$index";
|
|
207
|
-
arrKey = Number(result[6]);
|
|
208
|
-
}
|
|
209
|
-
if (i === segments.length - 1) {
|
|
210
|
-
ptr.key = key;
|
|
211
|
-
ptr.embeddedKey = embeddedKey;
|
|
212
|
-
if (isPath) ptr.embeddedKeyIsPath = true;
|
|
213
|
-
ptr.type = "UPDATE" /* UPDATE */;
|
|
214
|
-
ptr.changes = [
|
|
215
|
-
{
|
|
216
|
-
type: change.type,
|
|
217
|
-
key: arrKey,
|
|
218
|
-
value: change.value,
|
|
219
|
-
oldValue: change.oldValue
|
|
220
|
-
}
|
|
221
|
-
];
|
|
46
|
+
function findFilterClose(s, from) {
|
|
47
|
+
let i = from;
|
|
48
|
+
while (i < s.length) {
|
|
49
|
+
if (s[i] === "'") {
|
|
50
|
+
i += 1;
|
|
51
|
+
while (i < s.length) {
|
|
52
|
+
if (s[i] === "'") {
|
|
53
|
+
if (i + 1 < s.length && s[i + 1] === "'") {
|
|
54
|
+
i += 2;
|
|
222
55
|
} else {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (isPath) ptr.embeddedKeyIsPath = true;
|
|
226
|
-
ptr.type = "UPDATE" /* UPDATE */;
|
|
227
|
-
const newPtr = {};
|
|
228
|
-
ptr.changes = [
|
|
229
|
-
{
|
|
230
|
-
type: "UPDATE" /* UPDATE */,
|
|
231
|
-
key: arrKey,
|
|
232
|
-
changes: [newPtr]
|
|
233
|
-
}
|
|
234
|
-
];
|
|
235
|
-
ptr = newPtr;
|
|
56
|
+
i += 1;
|
|
57
|
+
break;
|
|
236
58
|
}
|
|
237
59
|
} else {
|
|
238
|
-
|
|
239
|
-
ptr.key = segment;
|
|
240
|
-
ptr.type = change.type;
|
|
241
|
-
ptr.value = change.value;
|
|
242
|
-
ptr.oldValue = change.oldValue;
|
|
243
|
-
} else {
|
|
244
|
-
ptr.key = segment;
|
|
245
|
-
ptr.type = "UPDATE" /* UPDATE */;
|
|
246
|
-
const newPtr = {};
|
|
247
|
-
ptr.changes = [newPtr];
|
|
248
|
-
ptr = newPtr;
|
|
249
|
-
}
|
|
60
|
+
i += 1;
|
|
250
61
|
}
|
|
251
62
|
}
|
|
252
|
-
|
|
63
|
+
} else if (s[i] === ")" && i + 1 < s.length && s[i + 1] === "]") {
|
|
64
|
+
return i;
|
|
65
|
+
} else {
|
|
66
|
+
i += 1;
|
|
253
67
|
}
|
|
254
|
-
});
|
|
255
|
-
return changesArr;
|
|
256
|
-
};
|
|
257
|
-
var getTypeOfObj = (obj) => {
|
|
258
|
-
if (obj === void 0) {
|
|
259
|
-
return "undefined";
|
|
260
|
-
}
|
|
261
|
-
if (obj === null) {
|
|
262
|
-
return null;
|
|
263
68
|
}
|
|
264
|
-
return
|
|
265
|
-
}
|
|
266
|
-
var
|
|
267
|
-
|
|
268
|
-
return
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
let
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (skipPath.includes(".") && skipPath.startsWith(currentPath + ".")) {
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
if (skipPath.includes(".")) {
|
|
281
|
-
const skipParts = skipPath.split(".");
|
|
282
|
-
const currentParts = currentPath.split(".");
|
|
283
|
-
if (currentParts.length >= skipParts.length) {
|
|
284
|
-
for (let i = 0; i < skipParts.length; i++) {
|
|
285
|
-
if (skipParts[i] !== currentParts[i]) {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return true;
|
|
69
|
+
return -1;
|
|
70
|
+
}
|
|
71
|
+
var NESTED_PATH_RE = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
72
|
+
function parseFilter(inner) {
|
|
73
|
+
if (inner.startsWith("@==")) return { type: "valueFilter", value: parseFilterLiteral(inner.slice(3)) };
|
|
74
|
+
if (!inner.startsWith("@.") && !inner.startsWith("@['")) throw new Error(`Invalid filter expression: ${inner}`);
|
|
75
|
+
let quoted = false;
|
|
76
|
+
let eq = -1;
|
|
77
|
+
for (let i = 1; i < inner.length; i++) {
|
|
78
|
+
if (inner[i] === "'") {
|
|
79
|
+
if (quoted && inner[i + 1] === "'") {
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
290
82
|
}
|
|
83
|
+
quoted = !quoted;
|
|
84
|
+
} else if (!quoted && inner.slice(i, i + 2) === "==") {
|
|
85
|
+
eq = i;
|
|
86
|
+
break;
|
|
291
87
|
}
|
|
292
|
-
return false;
|
|
293
|
-
})) {
|
|
294
|
-
return changes;
|
|
295
|
-
}
|
|
296
|
-
const typeOfOldObj = getTypeOfObj(oldObj);
|
|
297
|
-
const typeOfNewObj = getTypeOfObj(newObj);
|
|
298
|
-
if (options.treatTypeChangeAsReplace && typeOfOldObj !== typeOfNewObj) {
|
|
299
|
-
if (typeOfOldObj !== "undefined") {
|
|
300
|
-
changes.push({ type: "REMOVE" /* REMOVE */, key: getKey(path), value: oldObj });
|
|
301
|
-
}
|
|
302
|
-
const lastPathSegment = path[path.length - 1];
|
|
303
|
-
const isArrayElement = path.length > 0 && (typeof lastPathSegment === "number" || typeof lastPathSegment === "string" && /^\d+$/.test(lastPathSegment));
|
|
304
|
-
if (typeOfNewObj !== "undefined" || isArrayElement) {
|
|
305
|
-
changes.push({ type: "ADD" /* ADD */, key: getKey(path), value: newObj });
|
|
306
|
-
}
|
|
307
|
-
return changes;
|
|
308
|
-
}
|
|
309
|
-
if (typeOfNewObj === "undefined" && typeOfOldObj !== "undefined") {
|
|
310
|
-
const lastPathSegment = path[path.length - 1];
|
|
311
|
-
const isArrayElement = path.length > 0 && (typeof lastPathSegment === "number" || typeof lastPathSegment === "string" && /^\d+$/.test(lastPathSegment));
|
|
312
|
-
if (isArrayElement) {
|
|
313
|
-
changes.push({ type: "UPDATE" /* UPDATE */, key: getKey(path), value: newObj, oldValue: oldObj });
|
|
314
|
-
} else {
|
|
315
|
-
changes.push({ type: "REMOVE" /* REMOVE */, key: getKey(path), value: oldObj });
|
|
316
|
-
}
|
|
317
|
-
return changes;
|
|
318
88
|
}
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
89
|
+
if (eq === -1) throw new Error(`Invalid filter: missing '==' in ${inner}`);
|
|
90
|
+
const access = inner.slice(1, eq);
|
|
91
|
+
let members;
|
|
92
|
+
try {
|
|
93
|
+
members = parseAtomPath("$" + access).slice(1);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error(`Invalid property name in filter: ${inner}`);
|
|
96
|
+
}
|
|
97
|
+
if (!members.length || members.some((member) => member.type !== "property")) throw new Error(`Invalid property name in filter: ${inner}`);
|
|
98
|
+
const names = members.map((member) => member.name);
|
|
99
|
+
const property = names.join(".");
|
|
100
|
+
const value = parseFilterLiteral(inner.slice(eq + 2));
|
|
101
|
+
if (names.length === 1 && access.startsWith("[")) return { type: "keyFilter", property, value, literalKey: true };
|
|
102
|
+
if (names.some((name) => !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name))) return { type: "keyFilter", property, value, memberPath: names };
|
|
103
|
+
return { type: "keyFilter", property, value };
|
|
104
|
+
}
|
|
105
|
+
function parseAtomPath(path) {
|
|
106
|
+
if (!path.startsWith("$")) {
|
|
107
|
+
throw new Error(`Path must start with '$': ${path}`);
|
|
322
108
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
109
|
+
const segments = [{ type: "root" }];
|
|
110
|
+
let i = 1;
|
|
111
|
+
while (i < path.length) {
|
|
112
|
+
if (path[i] === ".") {
|
|
113
|
+
i += 1;
|
|
114
|
+
const start = i;
|
|
115
|
+
while (i < path.length && /[a-zA-Z0-9_]/.test(path[i])) {
|
|
116
|
+
i += 1;
|
|
117
|
+
}
|
|
118
|
+
if (i === start) throw new Error(`Empty property name at position ${i} in: ${path}`);
|
|
119
|
+
if (!/^[a-zA-Z_]/.test(path.slice(start, i))) throw new Error(`Invalid dot property in: ${path}`);
|
|
120
|
+
segments.push({ type: "property", name: path.slice(start, i) });
|
|
121
|
+
} else if (path[i] === "[") {
|
|
122
|
+
if (i + 1 >= path.length) throw new Error(`Unexpected end of path after '[': ${path}`);
|
|
123
|
+
if (path[i + 1] === "?") {
|
|
124
|
+
const closingIdx = findFilterClose(path, i + 2);
|
|
125
|
+
if (closingIdx === -1) throw new Error(`Unterminated filter expression in: ${path}`);
|
|
126
|
+
const inner = path.slice(i + 3, closingIdx);
|
|
127
|
+
segments.push(parseFilter(inner));
|
|
128
|
+
i = closingIdx + 2;
|
|
129
|
+
} else if (path[i + 1] === "'") {
|
|
130
|
+
const [key, endIdx] = extractQuotedString(path, i + 2);
|
|
131
|
+
if (path[endIdx + 1] !== "]") throw new Error(`Expected ']' after bracket property in: ${path}`);
|
|
132
|
+
segments.push({ type: "property", name: key });
|
|
133
|
+
i = endIdx + 2;
|
|
134
|
+
} else if (/\d/.test(path[i + 1])) {
|
|
135
|
+
const end = path.indexOf("]", i);
|
|
136
|
+
if (end === -1) throw new Error(`Unterminated array index in: ${path}`);
|
|
137
|
+
const indexStr = path.slice(i + 1, end);
|
|
138
|
+
if (indexStr.length > 1 && indexStr[0] === "0") {
|
|
139
|
+
throw new Error(`Leading zeros not allowed in array index: [${indexStr}]`);
|
|
140
|
+
}
|
|
141
|
+
if (!/^(0|[1-9]\d*)$/.test(indexStr) || !Number.isSafeInteger(Number(indexStr))) {
|
|
142
|
+
throw new Error(`Invalid array index: [${indexStr}]`);
|
|
143
|
+
}
|
|
144
|
+
segments.push({ type: "index", index: Number(indexStr) });
|
|
145
|
+
i = end + 1;
|
|
146
|
+
} else {
|
|
147
|
+
throw new Error(`Unexpected character after '[': '${path[i + 1]}' in: ${path}`);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
throw new Error(`Unexpected character '${path[i]}' at position ${i} in: ${path}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return segments;
|
|
154
|
+
}
|
|
155
|
+
var SIMPLE_PROPERTY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
156
|
+
function formatMemberAccess(name) {
|
|
157
|
+
if (SIMPLE_PROPERTY_RE.test(name)) {
|
|
158
|
+
return `.${name}`;
|
|
159
|
+
}
|
|
160
|
+
return `['${name.replace(/'/g, "''")}']`;
|
|
161
|
+
}
|
|
162
|
+
function buildAtomPath(segments) {
|
|
163
|
+
let result = "";
|
|
164
|
+
for (const seg of segments) {
|
|
165
|
+
switch (seg.type) {
|
|
166
|
+
case "root":
|
|
167
|
+
result += "$";
|
|
168
|
+
break;
|
|
169
|
+
case "property":
|
|
170
|
+
result += formatMemberAccess(seg.name);
|
|
171
|
+
break;
|
|
172
|
+
case "index":
|
|
173
|
+
result += `[${seg.index}]`;
|
|
174
|
+
break;
|
|
175
|
+
case "keyFilter": {
|
|
176
|
+
let memberAccess;
|
|
177
|
+
if (seg.memberPath) {
|
|
178
|
+
memberAccess = seg.memberPath.map(formatMemberAccess).join("");
|
|
179
|
+
} else if (seg.literalKey) {
|
|
180
|
+
memberAccess = `['${seg.property.replace(/'/g, "''")}']`;
|
|
181
|
+
} else if (NESTED_PATH_RE.test(seg.property)) {
|
|
182
|
+
memberAccess = `.${seg.property}`;
|
|
183
|
+
} else {
|
|
184
|
+
memberAccess = `['${seg.property.replace(/'/g, "''")}']`;
|
|
185
|
+
}
|
|
186
|
+
result += `[?(@${memberAccess}==${formatFilterLiteral(seg.value)})]`;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case "valueFilter":
|
|
190
|
+
result += `[?(@==${formatFilterLiteral(seg.value)})]`;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
function normalizeLegacyFilterOperator(inner) {
|
|
197
|
+
let quoted = false;
|
|
198
|
+
for (let i = 0; i < inner.length; i++) {
|
|
199
|
+
if (inner[i] === "'") {
|
|
200
|
+
if (quoted && inner[i + 1] === "'") {
|
|
201
|
+
i++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
quoted = !quoted;
|
|
205
|
+
} else if (inner[i] === "=" && !quoted) {
|
|
206
|
+
let end = i + 1;
|
|
207
|
+
while (inner[end] === "=") end++;
|
|
208
|
+
inner = inner.slice(0, i) + "==" + inner.slice(end);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return inner;
|
|
213
|
+
}
|
|
214
|
+
function canonicalizeFilterForAtom(inner) {
|
|
215
|
+
inner = normalizeLegacyFilterOperator(inner);
|
|
216
|
+
if (inner.startsWith("@.")) {
|
|
217
|
+
const eqIdx = inner.indexOf("==");
|
|
218
|
+
if (eqIdx === -1) return `[?(${inner})]`;
|
|
219
|
+
const key = inner.slice(2, eqIdx);
|
|
220
|
+
const valuePart = inner.slice(eqIdx);
|
|
221
|
+
if (NESTED_PATH_RE.test(key)) {
|
|
222
|
+
return `[?(@.${key}${valuePart})]`;
|
|
223
|
+
}
|
|
224
|
+
return `[?(@['${key.replace(/'/g, "''")}']${valuePart})]`;
|
|
225
|
+
}
|
|
226
|
+
return `[?(${inner})]`;
|
|
227
|
+
}
|
|
228
|
+
function canonicalizeFilterForV4(inner) {
|
|
229
|
+
if (inner.startsWith("@['")) {
|
|
230
|
+
const [key, endIdx] = extractQuotedString(inner, 3);
|
|
231
|
+
const rest = inner.slice(endIdx + 2);
|
|
232
|
+
if (rest.startsWith("==")) {
|
|
233
|
+
const literal = rest.slice(2);
|
|
234
|
+
const normalizedLiteral = normalizeToStringQuoted(literal);
|
|
235
|
+
return `[?(@['${key.replace(/'/g, "''")}']==${normalizedLiteral})]`;
|
|
236
|
+
}
|
|
237
|
+
return `[?(${inner})]`;
|
|
238
|
+
}
|
|
239
|
+
return normalizeFilterToStringLiterals(`[?(${inner})]`);
|
|
240
|
+
}
|
|
241
|
+
function normalizeToStringQuoted(literal) {
|
|
242
|
+
if (literal.startsWith("'") && literal.endsWith("'")) {
|
|
243
|
+
return literal;
|
|
244
|
+
}
|
|
245
|
+
const value = parseFilterLiteral(literal);
|
|
246
|
+
const stringValue = String(value).replace(/'/g, "''");
|
|
247
|
+
return `'${stringValue}'`;
|
|
248
|
+
}
|
|
249
|
+
function atomicPathToAtomPath(atomicPath) {
|
|
250
|
+
if (atomicPath === "$.$root") return "$";
|
|
251
|
+
if (atomicPath.startsWith("$.$root.") || atomicPath.startsWith("$.$root[")) {
|
|
252
|
+
atomicPath = "$" + atomicPath.slice(7);
|
|
253
|
+
}
|
|
254
|
+
if (!atomicPath.startsWith("$")) {
|
|
255
|
+
throw new Error(`Atomic path must start with '$': ${atomicPath}`);
|
|
256
|
+
}
|
|
257
|
+
let result = "$";
|
|
258
|
+
let i = 1;
|
|
259
|
+
while (i < atomicPath.length) {
|
|
260
|
+
if (atomicPath[i] === ".") {
|
|
261
|
+
i += 1;
|
|
262
|
+
const start = i;
|
|
263
|
+
while (i < atomicPath.length && atomicPath[i] !== "." && atomicPath[i] !== "[") {
|
|
264
|
+
i += 1;
|
|
265
|
+
}
|
|
266
|
+
const name = atomicPath.slice(start, i);
|
|
267
|
+
result += formatMemberAccess(name);
|
|
268
|
+
} else if (atomicPath[i] === "[") {
|
|
269
|
+
if (atomicPath[i + 1] === "?") {
|
|
270
|
+
const closingIdx = findFilterClose(atomicPath, i + 2);
|
|
271
|
+
if (closingIdx === -1) throw new Error(`Unterminated filter in: ${atomicPath}`);
|
|
272
|
+
const inner = atomicPath.slice(i + 3, closingIdx);
|
|
273
|
+
result += canonicalizeFilterForAtom(inner);
|
|
274
|
+
i = closingIdx + 2;
|
|
275
|
+
} else if (atomicPath[i + 1] === "'") {
|
|
276
|
+
const [key, endIdx] = extractQuotedString(atomicPath, i + 2);
|
|
277
|
+
if (atomicPath[endIdx + 1] !== "]") throw new Error(`Expected ']' in: ${atomicPath}`);
|
|
278
|
+
result += formatMemberAccess(key);
|
|
279
|
+
i = endIdx + 2;
|
|
280
|
+
} else if (/\d/.test(atomicPath[i + 1])) {
|
|
281
|
+
const end = atomicPath.indexOf("]", i);
|
|
282
|
+
if (end === -1) throw new Error(`Unterminated bracket in: ${atomicPath}`);
|
|
283
|
+
result += atomicPath.slice(i, end + 1);
|
|
284
|
+
i = end + 1;
|
|
285
|
+
} else {
|
|
286
|
+
const end = atomicPath.indexOf("]", i);
|
|
287
|
+
if (end === -1) throw new Error(`Unterminated bracket in: ${atomicPath}`);
|
|
288
|
+
const name = atomicPath.slice(i + 1, end);
|
|
289
|
+
result += `['${name.replace(/'/g, "''")}']`;
|
|
290
|
+
i = end + 1;
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
throw new Error(`Unexpected character '${atomicPath[i]}' in atomic path: ${atomicPath}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
function atomPathToAtomicPath(atomPath) {
|
|
299
|
+
if (!atomPath.startsWith("$")) {
|
|
300
|
+
throw new Error(`Atom path must start with '$': ${atomPath}`);
|
|
301
|
+
}
|
|
302
|
+
if (atomPath === "$") {
|
|
303
|
+
return "$.$root";
|
|
304
|
+
}
|
|
305
|
+
let result = "$";
|
|
306
|
+
let i = 1;
|
|
307
|
+
while (i < atomPath.length) {
|
|
308
|
+
if (atomPath[i] === ".") {
|
|
309
|
+
i += 1;
|
|
310
|
+
const start = i;
|
|
311
|
+
while (i < atomPath.length && /[a-zA-Z0-9_]/.test(atomPath[i])) {
|
|
312
|
+
i += 1;
|
|
313
|
+
}
|
|
314
|
+
result += "." + atomPath.slice(start, i);
|
|
315
|
+
} else if (atomPath[i] === "[") {
|
|
316
|
+
if (atomPath[i + 1] === "?") {
|
|
317
|
+
const closingIdx = findFilterClose(atomPath, i + 2);
|
|
318
|
+
if (closingIdx === -1) throw new Error(`Unterminated filter in: ${atomPath}`);
|
|
319
|
+
const inner = atomPath.slice(i + 3, closingIdx);
|
|
320
|
+
result += canonicalizeFilterForV4(inner);
|
|
321
|
+
i = closingIdx + 2;
|
|
322
|
+
} else if (atomPath[i + 1] === "'") {
|
|
323
|
+
const [key, endIdx] = extractQuotedString(atomPath, i + 2);
|
|
324
|
+
if (atomPath[endIdx + 1] !== "]") throw new Error(`Expected ']' in: ${atomPath}`);
|
|
325
|
+
result += key.includes(".") && !/[[\]'\\]/.test(key) && !/^\d/.test(key) ? `[${key}]` : `['${key.replace(/'/g, "''")}']`;
|
|
326
|
+
i = endIdx + 2;
|
|
327
|
+
} else if (/\d/.test(atomPath[i + 1])) {
|
|
328
|
+
const end = atomPath.indexOf("]", i);
|
|
329
|
+
if (end === -1) throw new Error(`Unterminated bracket in: ${atomPath}`);
|
|
330
|
+
result += atomPath.slice(i, end + 1);
|
|
331
|
+
i = end + 1;
|
|
332
|
+
} else {
|
|
333
|
+
throw new Error(`Unexpected character after '[' in: ${atomPath}`);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
throw new Error(`Unexpected character '${atomPath[i]}' in atom path: ${atomPath}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
function normalizeFilterToStringLiterals(filter) {
|
|
342
|
+
const eqIdx = filter.indexOf("==");
|
|
343
|
+
if (eqIdx === -1) return filter;
|
|
344
|
+
const literalStart = eqIdx + 2;
|
|
345
|
+
const literalEnd = filter.length - 2;
|
|
346
|
+
const literal = filter.slice(literalStart, literalEnd);
|
|
347
|
+
if (literal.startsWith("'") && literal.endsWith("'")) {
|
|
348
|
+
return filter;
|
|
349
|
+
}
|
|
350
|
+
const value = parseFilterLiteral(literal);
|
|
351
|
+
const stringValue = String(value).replace(/'/g, "''");
|
|
352
|
+
return filter.slice(0, literalStart) + `'${stringValue}'` + filter.slice(literalEnd);
|
|
353
|
+
}
|
|
354
|
+
function extractKeyFromAtomicPath(atomicPath) {
|
|
355
|
+
if (atomicPath === "$.$root") return "$root";
|
|
356
|
+
const last = parseAtomPath(atomicPathToAtomPath(atomicPath)).at(-1);
|
|
357
|
+
switch (last.type) {
|
|
358
|
+
case "property":
|
|
359
|
+
return last.name;
|
|
360
|
+
case "index":
|
|
361
|
+
return String(last.index);
|
|
362
|
+
case "keyFilter":
|
|
363
|
+
case "valueFilter":
|
|
364
|
+
return String(last.value);
|
|
365
|
+
default:
|
|
366
|
+
return atomicPath;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/helpers.ts
|
|
371
|
+
function splitJSONPath(path) {
|
|
372
|
+
const parts = [];
|
|
373
|
+
let currentPart = "";
|
|
374
|
+
let inSingleQuotes = false;
|
|
375
|
+
let inBrackets = 0;
|
|
376
|
+
for (let i = 0; i < path.length; i++) {
|
|
377
|
+
const char = path[i];
|
|
378
|
+
if (char === "'" && path[i - 1] !== "\\") {
|
|
379
|
+
inSingleQuotes = !inSingleQuotes;
|
|
380
|
+
} else if (char === "[" && !inSingleQuotes) {
|
|
381
|
+
inBrackets++;
|
|
382
|
+
} else if (char === "]" && !inSingleQuotes) {
|
|
383
|
+
inBrackets--;
|
|
384
|
+
}
|
|
385
|
+
if (char === "." && !inSingleQuotes && inBrackets === 0) {
|
|
386
|
+
parts.push(currentPart);
|
|
387
|
+
currentPart = "";
|
|
388
|
+
} else {
|
|
389
|
+
currentPart += char;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (currentPart !== "") {
|
|
393
|
+
parts.push(currentPart);
|
|
394
|
+
}
|
|
395
|
+
return parts;
|
|
396
|
+
}
|
|
397
|
+
function arrayDifference(first, second) {
|
|
398
|
+
const secondSet = new Set(second);
|
|
399
|
+
return first.filter((item) => !secondSet.has(item));
|
|
400
|
+
}
|
|
401
|
+
function arrayIntersection(first, second) {
|
|
402
|
+
const secondSet = new Set(second);
|
|
403
|
+
return first.filter((item) => secondSet.has(item));
|
|
404
|
+
}
|
|
405
|
+
function keyBy(arr, getKey2) {
|
|
406
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
407
|
+
for (const [index, item] of Object.entries(arr)) {
|
|
408
|
+
const key = String(getKey2(item, false, Number(index)));
|
|
409
|
+
if (Object.prototype.hasOwnProperty.call(result, key)) throw new Error(`Duplicate array identity: ${key}`);
|
|
410
|
+
result[key] = item;
|
|
411
|
+
}
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
function setByPath(obj, path, value) {
|
|
415
|
+
const parts = path.replaceAll(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
416
|
+
let current = obj;
|
|
417
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
418
|
+
const part = parts[i];
|
|
419
|
+
if (!(part in current)) {
|
|
420
|
+
current[part] = /^\d+$/.test(parts[i + 1]) ? [] : {};
|
|
421
|
+
}
|
|
422
|
+
current = current[part];
|
|
423
|
+
}
|
|
424
|
+
current[parts.at(-1)] = value;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/jsonDiff.ts
|
|
428
|
+
var Operation = /* @__PURE__ */ ((Operation2) => {
|
|
429
|
+
Operation2["REMOVE"] = "REMOVE";
|
|
430
|
+
Operation2["ADD"] = "ADD";
|
|
431
|
+
Operation2["UPDATE"] = "UPDATE";
|
|
432
|
+
return Operation2;
|
|
433
|
+
})(Operation || {});
|
|
434
|
+
function diff(oldObj, newObj, options = {}) {
|
|
435
|
+
let embeddedObjKeys = options.arrayIdentityKeys ?? options.embeddedObjKeys;
|
|
436
|
+
const { keysToSkip, keysToInclude, treatTypeChangeAsReplace } = options;
|
|
437
|
+
if (embeddedObjKeys instanceof Map) {
|
|
438
|
+
embeddedObjKeys = new Map(
|
|
439
|
+
Array.from(embeddedObjKeys.entries()).map(([key, value]) => [
|
|
440
|
+
key instanceof RegExp ? key : key.replace(/^\./, ""),
|
|
441
|
+
value
|
|
442
|
+
])
|
|
443
|
+
);
|
|
444
|
+
} else if (embeddedObjKeys) {
|
|
445
|
+
embeddedObjKeys = Object.fromEntries(
|
|
446
|
+
Object.entries(embeddedObjKeys).map(([key, value]) => [key.replace(/^\./, ""), value])
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return compare(oldObj, newObj, [], [], {
|
|
450
|
+
embeddedObjKeys,
|
|
451
|
+
keysToSkip: keysToSkip ?? [],
|
|
452
|
+
keysToInclude,
|
|
453
|
+
treatTypeChangeAsReplace: treatTypeChangeAsReplace ?? true
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
var applyChangeset = (obj, changeset) => {
|
|
457
|
+
if (changeset) {
|
|
458
|
+
changeset.forEach((change) => {
|
|
459
|
+
const { type, key, value, embeddedKey } = change;
|
|
460
|
+
if (value !== null && value !== void 0 || type === "REMOVE" /* REMOVE */ || value === null && type === "ADD" /* ADD */ || value === void 0 && type === "ADD" /* ADD */) {
|
|
461
|
+
applyLeafChange(obj, change, embeddedKey);
|
|
462
|
+
} else {
|
|
463
|
+
applyBranchChange(key === "$root" ? obj : obj[key], change);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return obj;
|
|
468
|
+
};
|
|
469
|
+
var revertChangeset = (obj, changeset) => {
|
|
470
|
+
if (changeset) {
|
|
471
|
+
[...changeset].reverse().forEach((change) => {
|
|
472
|
+
const { value, type } = change;
|
|
473
|
+
if (!change.changes || value === null && type === "REMOVE" /* REMOVE */) {
|
|
474
|
+
revertLeafChange(obj, change);
|
|
475
|
+
} else {
|
|
476
|
+
revertBranchChange(change.key === "$root" ? obj : obj[change.key], change);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return obj;
|
|
481
|
+
};
|
|
482
|
+
var atomizeChangeset = (obj, path = "$", embeddedKey, embeddedKeyIsPath) => {
|
|
483
|
+
if (Array.isArray(obj)) {
|
|
484
|
+
return handleArray(obj, path, embeddedKey, embeddedKeyIsPath);
|
|
485
|
+
} else if (obj.changes || embeddedKey) {
|
|
486
|
+
if (embeddedKey) {
|
|
487
|
+
const [updatedPath, atomicChange] = handleEmbeddedKey(embeddedKey, obj, path, embeddedKeyIsPath);
|
|
488
|
+
path = updatedPath;
|
|
489
|
+
if (atomicChange) {
|
|
490
|
+
return atomicChange;
|
|
491
|
+
}
|
|
492
|
+
if (!obj.changes) {
|
|
493
|
+
return [{ ...obj, path, valueType: getTypeOfObj(obj.value) }];
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
path = append(path, obj.key);
|
|
497
|
+
}
|
|
498
|
+
return atomizeChangeset(obj.changes || obj, path, obj.embeddedKey, obj.embeddedKeyIsPath);
|
|
499
|
+
} else {
|
|
500
|
+
const valueType = getTypeOfObj(obj.value);
|
|
501
|
+
const finalPath = append(path, obj.key);
|
|
502
|
+
return [
|
|
503
|
+
{
|
|
504
|
+
...obj,
|
|
505
|
+
path: finalPath,
|
|
506
|
+
valueType
|
|
507
|
+
}
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
function handleEmbeddedKey(embeddedKey, obj, path, isPath) {
|
|
512
|
+
if (embeddedKey === "$index") {
|
|
513
|
+
path = `${path}[${obj.key}]`;
|
|
514
|
+
return [path];
|
|
515
|
+
} else if (embeddedKey === "$value") {
|
|
516
|
+
path = `${path}[?(@=='${obj.key}')]`;
|
|
517
|
+
const valueType = getTypeOfObj(obj.value);
|
|
518
|
+
return [
|
|
519
|
+
path,
|
|
520
|
+
[
|
|
521
|
+
{
|
|
522
|
+
...obj,
|
|
523
|
+
path,
|
|
524
|
+
valueType
|
|
525
|
+
}
|
|
526
|
+
]
|
|
527
|
+
];
|
|
528
|
+
} else {
|
|
529
|
+
path = filterExpression(path, embeddedKey, obj.key, isPath);
|
|
530
|
+
return [path];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
var handleArray = (obj, path, embeddedKey, embeddedKeyIsPath) => {
|
|
534
|
+
return obj.reduce((memo, change) => [...memo, ...atomizeChangeset(change, path, embeddedKey, embeddedKeyIsPath)], []);
|
|
535
|
+
};
|
|
536
|
+
var unatomizeChangeset = (changes) => {
|
|
537
|
+
return (Array.isArray(changes) ? changes : [changes]).map((change) => {
|
|
538
|
+
const segments = parseAtomPath(atomicPathToAtomPath(change.path)).slice(1);
|
|
539
|
+
const buildChange = (key, offset) => {
|
|
540
|
+
const segment = segments[offset];
|
|
541
|
+
if (!segment) {
|
|
542
|
+
return { key, type: change.type, value: change.value, oldValue: change.oldValue };
|
|
543
|
+
}
|
|
544
|
+
if (segment.type === "property") {
|
|
545
|
+
return { key, type: "UPDATE" /* UPDATE */, changes: [buildChange(segment.name, offset + 1)] };
|
|
546
|
+
}
|
|
547
|
+
if (segment.type === "index" || segment.type === "keyFilter" || segment.type === "valueFilter") {
|
|
548
|
+
const embeddedKey = segment.type === "index" ? "$index" : segment.type === "valueFilter" ? "$value" : segment.property;
|
|
549
|
+
const childKey = segment.type === "index" ? String(segment.index) : String(segment.value);
|
|
550
|
+
const isPath = segment.type === "keyFilter" && !segment.literalKey && segment.property.includes(".");
|
|
551
|
+
return {
|
|
552
|
+
key,
|
|
553
|
+
type: "UPDATE" /* UPDATE */,
|
|
554
|
+
embeddedKey,
|
|
555
|
+
...isPath ? { embeddedKeyIsPath: true } : {},
|
|
556
|
+
changes: [buildChange(childKey, offset + 1)]
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
throw new Error(`Unexpected root segment in: ${change.path}`);
|
|
560
|
+
};
|
|
561
|
+
const first = segments[0];
|
|
562
|
+
return first?.type === "property" ? buildChange(first.name, 1) : buildChange(first ? "$root" : change.key, 0);
|
|
563
|
+
});
|
|
564
|
+
};
|
|
565
|
+
var getTypeOfObj = (obj) => {
|
|
566
|
+
if (obj === void 0) {
|
|
567
|
+
return "undefined";
|
|
568
|
+
}
|
|
569
|
+
if (obj === null) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
return Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1];
|
|
573
|
+
};
|
|
574
|
+
var getKey = (path) => {
|
|
575
|
+
const left = path.at(-1);
|
|
576
|
+
return left ?? "$root";
|
|
577
|
+
};
|
|
578
|
+
var shouldComparePath = (path, options) => {
|
|
579
|
+
if (options.keysToSkip?.some((skip) => path === skip || path.startsWith(skip + "."))) return false;
|
|
580
|
+
return options.keysToInclude === void 0 || options.keysToInclude.some(
|
|
581
|
+
(include) => path === "" || path === include || path.startsWith(include + ".") || include.startsWith(path + ".")
|
|
582
|
+
);
|
|
583
|
+
};
|
|
584
|
+
var compare = (oldObj, newObj, path, keyPath, options) => {
|
|
585
|
+
let changes = [];
|
|
586
|
+
if (!shouldComparePath(keyPath.join("."), options)) return changes;
|
|
587
|
+
const typeOfOldObj = getTypeOfObj(oldObj);
|
|
588
|
+
const typeOfNewObj = getTypeOfObj(newObj);
|
|
589
|
+
if (options.treatTypeChangeAsReplace && typeOfOldObj !== typeOfNewObj) {
|
|
590
|
+
if (typeOfOldObj !== "undefined") {
|
|
591
|
+
changes.push({ type: "REMOVE" /* REMOVE */, key: getKey(path), value: oldObj });
|
|
592
|
+
}
|
|
593
|
+
const lastPathSegment = path[path.length - 1];
|
|
594
|
+
const isArrayElement = path.length > 0 && (typeof lastPathSegment === "number" || typeof lastPathSegment === "string" && /^\d+$/.test(lastPathSegment));
|
|
595
|
+
if (typeOfNewObj !== "undefined" || isArrayElement) {
|
|
596
|
+
changes.push({ type: "ADD" /* ADD */, key: getKey(path), value: newObj });
|
|
597
|
+
}
|
|
598
|
+
return changes;
|
|
599
|
+
}
|
|
600
|
+
if (typeOfNewObj === "undefined" && typeOfOldObj !== "undefined") {
|
|
601
|
+
const lastPathSegment = path[path.length - 1];
|
|
602
|
+
const isArrayElement = path.length > 0 && (typeof lastPathSegment === "number" || typeof lastPathSegment === "string" && /^\d+$/.test(lastPathSegment));
|
|
603
|
+
if (isArrayElement) {
|
|
604
|
+
changes.push({ type: "UPDATE" /* UPDATE */, key: getKey(path), value: newObj, oldValue: oldObj });
|
|
605
|
+
} else {
|
|
606
|
+
changes.push({ type: "REMOVE" /* REMOVE */, key: getKey(path), value: oldObj });
|
|
607
|
+
}
|
|
608
|
+
return changes;
|
|
609
|
+
}
|
|
610
|
+
if (typeOfNewObj === "Object" && typeOfOldObj === "Array") {
|
|
611
|
+
changes.push({ type: "UPDATE" /* UPDATE */, key: getKey(path), value: newObj, oldValue: oldObj });
|
|
612
|
+
return changes;
|
|
613
|
+
}
|
|
614
|
+
if (typeOfNewObj === null) {
|
|
615
|
+
if (typeOfOldObj !== null) {
|
|
616
|
+
changes.push({ type: "UPDATE" /* UPDATE */, key: getKey(path), value: newObj, oldValue: oldObj });
|
|
326
617
|
}
|
|
327
618
|
return changes;
|
|
328
619
|
}
|
|
@@ -380,6 +671,7 @@ var compareObject = (oldObj, newObj, path, keyPath, skipPath = false, options =
|
|
|
380
671
|
for (k of intersectionKeys) {
|
|
381
672
|
newPath = path.concat([k]);
|
|
382
673
|
newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
|
|
674
|
+
if (!shouldComparePath(newKeyPath.join("."), options)) continue;
|
|
383
675
|
const diffs = compare(oldObj[k], newObj[k], newPath, newKeyPath, options);
|
|
384
676
|
if (diffs.length) {
|
|
385
677
|
changes = changes.concat(diffs);
|
|
@@ -390,7 +682,7 @@ var compareObject = (oldObj, newObj, path, keyPath, skipPath = false, options =
|
|
|
390
682
|
newPath = path.concat([k]);
|
|
391
683
|
newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
|
|
392
684
|
const currentPath = newKeyPath.join(".");
|
|
393
|
-
if (
|
|
685
|
+
if (!shouldComparePath(currentPath, options)) {
|
|
394
686
|
continue;
|
|
395
687
|
}
|
|
396
688
|
changes.push({
|
|
@@ -404,7 +696,7 @@ var compareObject = (oldObj, newObj, path, keyPath, skipPath = false, options =
|
|
|
404
696
|
newPath = path.concat([k]);
|
|
405
697
|
newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
|
|
406
698
|
const currentPath = newKeyPath.join(".");
|
|
407
|
-
if (
|
|
699
|
+
if (!shouldComparePath(currentPath, options)) {
|
|
408
700
|
continue;
|
|
409
701
|
}
|
|
410
702
|
changes.push({
|
|
@@ -428,7 +720,7 @@ var compareArray = (oldObj, newObj, path, keyPath, options) => {
|
|
|
428
720
|
if (diffs.length) {
|
|
429
721
|
const resolvedKey = isFunctionKey ? uniqKey(newObj[0] ?? oldObj[0], true) : uniqKey;
|
|
430
722
|
const sampleEl = newObj[0] ?? oldObj[0];
|
|
431
|
-
const isNestedPath = typeof resolvedKey === "string" && resolvedKey.includes(".") &&
|
|
723
|
+
const isNestedPath = typeof resolvedKey === "string" && resolvedKey.includes(".") && NESTED_PATH_RE2.test(resolvedKey) && (isFunctionKey || !(sampleEl != null && typeof sampleEl === "object" && resolvedKey in sampleEl));
|
|
432
724
|
return [
|
|
433
725
|
{
|
|
434
726
|
type: "UPDATE" /* UPDATE */,
|
|
@@ -464,9 +756,10 @@ var getObjectKey = (embeddedObjKeys, keyPath) => {
|
|
|
464
756
|
return void 0;
|
|
465
757
|
};
|
|
466
758
|
var convertArrayToObj = (arr, uniqKey) => {
|
|
467
|
-
let obj =
|
|
759
|
+
let obj = /* @__PURE__ */ Object.create(null);
|
|
468
760
|
if (uniqKey === "$value") {
|
|
469
761
|
arr.forEach((value) => {
|
|
762
|
+
if (Object.prototype.hasOwnProperty.call(obj, String(value))) throw new Error(`Duplicate array value identity: ${value}`);
|
|
470
763
|
obj[value] = value;
|
|
471
764
|
});
|
|
472
765
|
} else if (uniqKey === "$index") {
|
|
@@ -475,7 +768,7 @@ var convertArrayToObj = (arr, uniqKey) => {
|
|
|
475
768
|
obj[i] = value;
|
|
476
769
|
}
|
|
477
770
|
} else {
|
|
478
|
-
const maybeNestedPath = typeof uniqKey === "string" && uniqKey.includes(".") &&
|
|
771
|
+
const maybeNestedPath = typeof uniqKey === "string" && uniqKey.includes(".") && NESTED_PATH_RE2.test(uniqKey);
|
|
479
772
|
const keyFunction = typeof uniqKey === "string" ? maybeNestedPath ? (item) => item != null && typeof item === "object" && uniqKey in item ? item[uniqKey] : resolveProperty(item, uniqKey, true) : (item) => item[uniqKey] : uniqKey;
|
|
480
773
|
obj = keyBy(arr, keyFunction);
|
|
481
774
|
}
|
|
@@ -541,106 +834,36 @@ var addKeyValue = (obj, key, value, embeddedKey) => {
|
|
|
541
834
|
return obj ? obj[key] = value : null;
|
|
542
835
|
}
|
|
543
836
|
};
|
|
544
|
-
var applyLeafChange = (obj, change, embeddedKey, isPath) => {
|
|
545
|
-
const { type, key, value } = change;
|
|
546
|
-
switch (type) {
|
|
547
|
-
case "ADD" /* ADD */:
|
|
548
|
-
return addKeyValue(obj, key, value, embeddedKey);
|
|
549
|
-
case "UPDATE" /* UPDATE */:
|
|
550
|
-
return modifyKeyValue(obj, key, value);
|
|
551
|
-
case "REMOVE" /* REMOVE */:
|
|
552
|
-
return removeKey(obj, key, embeddedKey, isPath);
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
var applyArrayChange = (arr, change) => {
|
|
556
|
-
let changes = change.changes;
|
|
557
|
-
if (change.embeddedKey === "$index") {
|
|
558
|
-
changes = [...changes].sort((a, b) => {
|
|
559
|
-
if (a.type === "REMOVE" /* REMOVE */ && b.type === "REMOVE" /* REMOVE */) {
|
|
560
|
-
return Number(b.key) - Number(a.key);
|
|
561
|
-
}
|
|
562
|
-
if (a.type === "REMOVE" /* REMOVE */) return -1;
|
|
563
|
-
if (b.type === "REMOVE" /* REMOVE */) return 1;
|
|
564
|
-
return Number(a.key) - Number(b.key);
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
for (const subchange of changes) {
|
|
568
|
-
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 */) {
|
|
569
|
-
applyLeafChange(arr, subchange, change.embeddedKey, change.embeddedKeyIsPath);
|
|
570
|
-
} else {
|
|
571
|
-
let element;
|
|
572
|
-
if (change.embeddedKey === "$index") {
|
|
573
|
-
element = arr[subchange.key];
|
|
574
|
-
} else if (change.embeddedKey === "$value") {
|
|
575
|
-
const index = arr.indexOf(subchange.key);
|
|
576
|
-
if (index !== -1) {
|
|
577
|
-
element = arr[index];
|
|
578
|
-
}
|
|
579
|
-
} else {
|
|
580
|
-
element = arr.find((el) => {
|
|
581
|
-
const resolved = resolveProperty(el, change.embeddedKey, change.embeddedKeyIsPath);
|
|
582
|
-
return resolved !== void 0 && String(resolved) === String(subchange.key);
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
if (element) {
|
|
586
|
-
applyChangeset(element, subchange.changes);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
return arr;
|
|
591
|
-
};
|
|
592
|
-
var applyBranchChange = (obj, change) => {
|
|
593
|
-
if (Array.isArray(obj)) {
|
|
594
|
-
return applyArrayChange(obj, change);
|
|
595
|
-
} else {
|
|
596
|
-
return applyChangeset(obj, change.changes);
|
|
597
|
-
}
|
|
598
|
-
};
|
|
599
|
-
var revertLeafChange = (obj, change, embeddedKey = "$index", isPath) => {
|
|
600
|
-
const { type, key, value, oldValue } = change;
|
|
601
|
-
if (key === "$root") {
|
|
602
|
-
switch (type) {
|
|
603
|
-
case "ADD" /* ADD */:
|
|
604
|
-
for (const prop in obj) {
|
|
605
|
-
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
606
|
-
delete obj[prop];
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
return obj;
|
|
610
|
-
case "UPDATE" /* UPDATE */:
|
|
611
|
-
for (const prop in obj) {
|
|
612
|
-
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
613
|
-
delete obj[prop];
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
if (oldValue && typeof oldValue === "object") {
|
|
617
|
-
Object.assign(obj, oldValue);
|
|
618
|
-
}
|
|
619
|
-
return obj;
|
|
620
|
-
case "REMOVE" /* REMOVE */:
|
|
621
|
-
if (value && typeof value === "object") {
|
|
622
|
-
Object.assign(obj, value);
|
|
623
|
-
}
|
|
624
|
-
return obj;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
837
|
+
var applyLeafChange = (obj, change, embeddedKey, isPath) => {
|
|
838
|
+
const { type, key, value } = change;
|
|
627
839
|
switch (type) {
|
|
628
840
|
case "ADD" /* ADD */:
|
|
629
|
-
return
|
|
841
|
+
return addKeyValue(obj, key, value, embeddedKey);
|
|
630
842
|
case "UPDATE" /* UPDATE */:
|
|
631
|
-
return modifyKeyValue(obj, key,
|
|
843
|
+
return modifyKeyValue(obj, key, value);
|
|
632
844
|
case "REMOVE" /* REMOVE */:
|
|
633
|
-
return
|
|
845
|
+
return removeKey(obj, key, embeddedKey, isPath);
|
|
634
846
|
}
|
|
635
847
|
};
|
|
636
|
-
var
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
848
|
+
var applyArrayChange = (arr, change) => {
|
|
849
|
+
let changes = change.changes;
|
|
850
|
+
if (change.embeddedKey === "$index") {
|
|
851
|
+
changes = [...changes].sort((a, b) => {
|
|
852
|
+
if (a.type === "REMOVE" /* REMOVE */ && b.type === "REMOVE" /* REMOVE */) {
|
|
853
|
+
return Number(b.key) - Number(a.key);
|
|
854
|
+
}
|
|
855
|
+
if (a.type === "REMOVE" /* REMOVE */) return -1;
|
|
856
|
+
if (b.type === "REMOVE" /* REMOVE */) return 1;
|
|
857
|
+
return Number(a.key) - Number(b.key);
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
for (const subchange of changes) {
|
|
861
|
+
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 */) {
|
|
862
|
+
applyLeafChange(arr, subchange, change.embeddedKey, change.embeddedKeyIsPath);
|
|
640
863
|
} else {
|
|
641
864
|
let element;
|
|
642
865
|
if (change.embeddedKey === "$index") {
|
|
643
|
-
element = arr[
|
|
866
|
+
element = arr[subchange.key];
|
|
644
867
|
} else if (change.embeddedKey === "$value") {
|
|
645
868
|
const index = arr.indexOf(subchange.key);
|
|
646
869
|
if (index !== -1) {
|
|
@@ -653,519 +876,378 @@ var revertArrayChange = (arr, change) => {
|
|
|
653
876
|
});
|
|
654
877
|
}
|
|
655
878
|
if (element) {
|
|
656
|
-
|
|
879
|
+
applyChangeset(element, subchange.changes);
|
|
657
880
|
}
|
|
658
881
|
}
|
|
659
882
|
}
|
|
660
883
|
return arr;
|
|
661
884
|
};
|
|
662
|
-
var
|
|
885
|
+
var applyBranchChange = (obj, change) => {
|
|
663
886
|
if (Array.isArray(obj)) {
|
|
664
|
-
return
|
|
665
|
-
} else {
|
|
666
|
-
return revertChangeset(obj, change.changes);
|
|
667
|
-
}
|
|
668
|
-
};
|
|
669
|
-
function append(basePath, nextSegment) {
|
|
670
|
-
return nextSegment.includes(".") ? `${basePath}[${nextSegment}]` : `${basePath}.${nextSegment}`;
|
|
671
|
-
}
|
|
672
|
-
var IDENT_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
673
|
-
var NESTED_PATH_RE = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
674
|
-
function filterExpression(basePath, filterKey, filterValue, isPath) {
|
|
675
|
-
const escapedValue = `'${filterValue.replace(/'/g, "''")}'`;
|
|
676
|
-
let memberAccess;
|
|
677
|
-
if (isPath && NESTED_PATH_RE.test(filterKey)) {
|
|
678
|
-
memberAccess = "." + filterKey;
|
|
679
|
-
} else if (IDENT_RE.test(filterKey)) {
|
|
680
|
-
memberAccess = `.${filterKey}`;
|
|
887
|
+
return applyArrayChange(obj, change);
|
|
681
888
|
} else {
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
return `${basePath}[?(@${memberAccess}==${escapedValue})]`;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// src/jsonCompare.ts
|
|
688
|
-
var CompareOperation = /* @__PURE__ */ ((CompareOperation2) => {
|
|
689
|
-
CompareOperation2["CONTAINER"] = "CONTAINER";
|
|
690
|
-
CompareOperation2["UNCHANGED"] = "UNCHANGED";
|
|
691
|
-
return CompareOperation2;
|
|
692
|
-
})(CompareOperation || {});
|
|
693
|
-
var createValue = (value) => ({ type: "UNCHANGED" /* UNCHANGED */, value });
|
|
694
|
-
var createContainer = (value) => ({
|
|
695
|
-
type: "CONTAINER" /* CONTAINER */,
|
|
696
|
-
value
|
|
697
|
-
});
|
|
698
|
-
var enrich = (object) => {
|
|
699
|
-
const objectType = getTypeOfObj(object);
|
|
700
|
-
switch (objectType) {
|
|
701
|
-
case "Object":
|
|
702
|
-
return Object.keys(object).map((key) => ({ key, value: enrich(object[key]) })).reduce((accumulator, entry) => {
|
|
703
|
-
accumulator.value[entry.key] = entry.value;
|
|
704
|
-
return accumulator;
|
|
705
|
-
}, createContainer({}));
|
|
706
|
-
case "Array":
|
|
707
|
-
return object.map((value) => enrich(value)).reduce((accumulator, value) => {
|
|
708
|
-
accumulator.value.push(value);
|
|
709
|
-
return accumulator;
|
|
710
|
-
}, createContainer([]));
|
|
711
|
-
case "Function":
|
|
712
|
-
return void 0;
|
|
713
|
-
case "Date":
|
|
714
|
-
default:
|
|
715
|
-
return createValue(object);
|
|
716
|
-
}
|
|
717
|
-
};
|
|
718
|
-
var buildEnrichedPath = (atomicPath) => {
|
|
719
|
-
const segments = splitJSONPath(atomicPath);
|
|
720
|
-
let result = "value";
|
|
721
|
-
for (let i = 1; i < segments.length; i++) {
|
|
722
|
-
const seg = segments[i];
|
|
723
|
-
const isLast = i === segments.length - 1;
|
|
724
|
-
const arrayMatch = /^(.+?)\[(\d+)\]$/.exec(seg);
|
|
725
|
-
if (arrayMatch) {
|
|
726
|
-
const [, key, index] = arrayMatch;
|
|
727
|
-
result += `.${key}.value[${index}]`;
|
|
728
|
-
} else {
|
|
729
|
-
result += `.${seg}`;
|
|
730
|
-
}
|
|
731
|
-
if (!isLast) {
|
|
732
|
-
result += ".value";
|
|
733
|
-
}
|
|
889
|
+
return applyChangeset(obj, change.changes);
|
|
734
890
|
}
|
|
735
|
-
return result;
|
|
736
891
|
};
|
|
737
|
-
var
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
switch (
|
|
892
|
+
var revertLeafChange = (obj, change, embeddedKey = "$index", isPath) => {
|
|
893
|
+
const { type, key, value, oldValue } = change;
|
|
894
|
+
if (key === "$root") {
|
|
895
|
+
switch (type) {
|
|
741
896
|
case "ADD" /* ADD */:
|
|
897
|
+
for (const prop in obj) {
|
|
898
|
+
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
899
|
+
delete obj[prop];
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return obj;
|
|
742
903
|
case "UPDATE" /* UPDATE */:
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
setByPath(object, path, { type: entry.type, value: void 0, oldValue: entry.value });
|
|
747
|
-
break;
|
|
748
|
-
default:
|
|
749
|
-
throw new Error();
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
return object;
|
|
753
|
-
};
|
|
754
|
-
var ARRAY_WRAPPER_KEY = "_$arr";
|
|
755
|
-
var compare2 = (oldObject, newObject) => {
|
|
756
|
-
if (Array.isArray(oldObject) || Array.isArray(newObject)) {
|
|
757
|
-
const wrappedOld = { [ARRAY_WRAPPER_KEY]: oldObject };
|
|
758
|
-
const wrappedNew = { [ARRAY_WRAPPER_KEY]: newObject };
|
|
759
|
-
const enriched = enrich(wrappedOld);
|
|
760
|
-
const changes = atomizeChangeset(diff(wrappedOld, wrappedNew));
|
|
761
|
-
const result = applyChangelist(enriched, changes);
|
|
762
|
-
return result.value[ARRAY_WRAPPER_KEY];
|
|
763
|
-
}
|
|
764
|
-
return applyChangelist(enrich(oldObject), atomizeChangeset(diff(oldObject, newObject)));
|
|
765
|
-
};
|
|
766
|
-
var comparisonToDict = (node) => {
|
|
767
|
-
const result = { type: node.type };
|
|
768
|
-
if (node.type === "CONTAINER" /* CONTAINER */) {
|
|
769
|
-
if (Array.isArray(node.value)) {
|
|
770
|
-
const children = node.value;
|
|
771
|
-
const serialized = new Array(children.length);
|
|
772
|
-
for (let i = 0; i < children.length; i++) {
|
|
773
|
-
const child = children[i];
|
|
774
|
-
serialized[i] = child != null ? comparisonToDict(child) : null;
|
|
775
|
-
}
|
|
776
|
-
result.value = serialized;
|
|
777
|
-
} else if (node.value && typeof node.value === "object") {
|
|
778
|
-
const obj = /* @__PURE__ */ Object.create(null);
|
|
779
|
-
for (const [key, child] of Object.entries(
|
|
780
|
-
node.value
|
|
781
|
-
)) {
|
|
782
|
-
if (child == null) continue;
|
|
783
|
-
obj[key] = comparisonToDict(child);
|
|
784
|
-
}
|
|
785
|
-
result.value = obj;
|
|
786
|
-
}
|
|
787
|
-
} else {
|
|
788
|
-
if (node.type === "UNCHANGED" /* UNCHANGED */ || node.type === "ADD" /* ADD */ || node.type === "UPDATE" /* UPDATE */) {
|
|
789
|
-
result.value = node.value;
|
|
790
|
-
}
|
|
791
|
-
if (node.type === "REMOVE" /* REMOVE */ || node.type === "UPDATE" /* UPDATE */) {
|
|
792
|
-
result.oldValue = node.oldValue;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
return result;
|
|
796
|
-
};
|
|
797
|
-
var IDENT_RE2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
798
|
-
var comparisonToFlatList = (node, options = {}) => {
|
|
799
|
-
const results = [];
|
|
800
|
-
flattenNode(node, "$", options.includeUnchanged ?? false, results);
|
|
801
|
-
return results;
|
|
802
|
-
};
|
|
803
|
-
function flattenNode(node, path, includeUnchanged, results) {
|
|
804
|
-
if (node.type === "CONTAINER" /* CONTAINER */) {
|
|
805
|
-
if (Array.isArray(node.value)) {
|
|
806
|
-
for (let i = 0; i < node.value.length; i++) {
|
|
807
|
-
const child = node.value[i];
|
|
808
|
-
if (child == null) continue;
|
|
809
|
-
flattenNode(child, `${path}[${i}]`, includeUnchanged, results);
|
|
810
|
-
}
|
|
811
|
-
} else if (node.value && typeof node.value === "object") {
|
|
812
|
-
for (const [key, child] of Object.entries(
|
|
813
|
-
node.value
|
|
814
|
-
)) {
|
|
815
|
-
if (child == null) continue;
|
|
816
|
-
const childPath = IDENT_RE2.test(key) ? `${path}.${key}` : `${path}['${key.replace(/'/g, "''")}']`;
|
|
817
|
-
flattenNode(child, childPath, includeUnchanged, results);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
if (node.type === "UNCHANGED" /* UNCHANGED */ && !includeUnchanged) {
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
const entry = { path, type: node.type };
|
|
826
|
-
if (node.type === "UNCHANGED" /* UNCHANGED */ || node.type === "ADD" /* ADD */ || node.type === "UPDATE" /* UPDATE */) {
|
|
827
|
-
entry.value = node.value;
|
|
828
|
-
}
|
|
829
|
-
if (node.type === "REMOVE" /* REMOVE */ || node.type === "UPDATE" /* UPDATE */) {
|
|
830
|
-
entry.oldValue = node.oldValue;
|
|
831
|
-
}
|
|
832
|
-
results.push(entry);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// src/atomPath.ts
|
|
836
|
-
function formatFilterLiteral(value) {
|
|
837
|
-
if (value === null) return "null";
|
|
838
|
-
if (typeof value === "boolean") return String(value);
|
|
839
|
-
if (typeof value === "number") {
|
|
840
|
-
if (!Number.isFinite(value)) throw new Error(`Cannot format non-finite number as filter literal: ${value}`);
|
|
841
|
-
return String(value);
|
|
842
|
-
}
|
|
843
|
-
if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
|
|
844
|
-
throw new Error(`Cannot format filter literal for type ${typeof value}`);
|
|
845
|
-
}
|
|
846
|
-
function parseFilterLiteral(s) {
|
|
847
|
-
if (s === "true") return true;
|
|
848
|
-
if (s === "false") return false;
|
|
849
|
-
if (s === "null") return null;
|
|
850
|
-
if (s.startsWith("'") && s.endsWith("'")) {
|
|
851
|
-
return s.slice(1, -1).replace(/''/g, "'");
|
|
852
|
-
}
|
|
853
|
-
if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(s)) {
|
|
854
|
-
return Number(s);
|
|
855
|
-
}
|
|
856
|
-
throw new Error(`Invalid filter literal: ${s}`);
|
|
857
|
-
}
|
|
858
|
-
function extractQuotedString(s, start) {
|
|
859
|
-
const result = [];
|
|
860
|
-
let i = start;
|
|
861
|
-
while (i < s.length) {
|
|
862
|
-
if (s[i] === "'") {
|
|
863
|
-
if (i + 1 < s.length && s[i + 1] === "'") {
|
|
864
|
-
result.push("'");
|
|
865
|
-
i += 2;
|
|
866
|
-
} else {
|
|
867
|
-
return [result.join(""), i];
|
|
868
|
-
}
|
|
869
|
-
} else {
|
|
870
|
-
result.push(s[i]);
|
|
871
|
-
i += 1;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
throw new Error("Unterminated quoted string");
|
|
875
|
-
}
|
|
876
|
-
function findFilterClose(s, from) {
|
|
877
|
-
let i = from;
|
|
878
|
-
while (i < s.length) {
|
|
879
|
-
if (s[i] === "'") {
|
|
880
|
-
i += 1;
|
|
881
|
-
while (i < s.length) {
|
|
882
|
-
if (s[i] === "'") {
|
|
883
|
-
if (i + 1 < s.length && s[i + 1] === "'") {
|
|
884
|
-
i += 2;
|
|
885
|
-
} else {
|
|
886
|
-
i += 1;
|
|
887
|
-
break;
|
|
904
|
+
for (const prop in obj) {
|
|
905
|
+
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
906
|
+
delete obj[prop];
|
|
888
907
|
}
|
|
889
|
-
} else {
|
|
890
|
-
i += 1;
|
|
891
908
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
var NESTED_PATH_RE2 = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
902
|
-
function parseFilter(inner) {
|
|
903
|
-
if (inner.startsWith("@.")) {
|
|
904
|
-
const eq = inner.indexOf("==");
|
|
905
|
-
if (eq === -1) throw new Error(`Invalid filter: missing '==' in ${inner}`);
|
|
906
|
-
const key = inner.slice(2, eq);
|
|
907
|
-
if (!key || !NESTED_PATH_RE2.test(key)) {
|
|
908
|
-
throw new Error(`Invalid property name in filter: '${key}'. Use bracket notation for non-identifier keys: @['${key}']`);
|
|
909
|
+
if (oldValue && typeof oldValue === "object") {
|
|
910
|
+
Object.assign(obj, oldValue);
|
|
911
|
+
}
|
|
912
|
+
return obj;
|
|
913
|
+
case "REMOVE" /* REMOVE */:
|
|
914
|
+
if (value && typeof value === "object") {
|
|
915
|
+
Object.assign(obj, value);
|
|
916
|
+
}
|
|
917
|
+
return obj;
|
|
909
918
|
}
|
|
910
|
-
return { type: "keyFilter", property: key, value: parseFilterLiteral(inner.slice(eq + 2)) };
|
|
911
|
-
}
|
|
912
|
-
if (inner.startsWith("@['")) {
|
|
913
|
-
const [key, endIdx] = extractQuotedString(inner, 3);
|
|
914
|
-
const valStart = endIdx + 4;
|
|
915
|
-
return { type: "keyFilter", property: key, value: parseFilterLiteral(inner.slice(valStart)), literalKey: true };
|
|
916
|
-
}
|
|
917
|
-
if (inner.startsWith("@==")) {
|
|
918
|
-
return { type: "valueFilter", value: parseFilterLiteral(inner.slice(3)) };
|
|
919
919
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
920
|
+
switch (type) {
|
|
921
|
+
case "ADD" /* ADD */:
|
|
922
|
+
return removeKey(obj, key, embeddedKey, isPath);
|
|
923
|
+
case "UPDATE" /* UPDATE */:
|
|
924
|
+
return modifyKeyValue(obj, key, oldValue);
|
|
925
|
+
case "REMOVE" /* REMOVE */:
|
|
926
|
+
return addKeyValue(obj, key, value, embeddedKey);
|
|
925
927
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
i += 1;
|
|
928
|
+
};
|
|
929
|
+
var revertArrayChange = (arr, change) => {
|
|
930
|
+
let changes = change.changes;
|
|
931
|
+
if (change.embeddedKey === "$index") {
|
|
932
|
+
changes = [...changes].sort((a, b) => {
|
|
933
|
+
if (a.type === "ADD" /* ADD */ && b.type === "ADD" /* ADD */) {
|
|
934
|
+
return Number(b.key) - Number(a.key);
|
|
934
935
|
}
|
|
935
|
-
if (
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
const end = path.indexOf("]", i);
|
|
952
|
-
if (end === -1) throw new Error(`Unterminated array index in: ${path}`);
|
|
953
|
-
const indexStr = path.slice(i + 1, end);
|
|
954
|
-
if (indexStr.length > 1 && indexStr[0] === "0") {
|
|
955
|
-
throw new Error(`Leading zeros not allowed in array index: [${indexStr}]`);
|
|
936
|
+
if (a.type === "ADD" /* ADD */) return -1;
|
|
937
|
+
if (b.type === "ADD" /* ADD */) return 1;
|
|
938
|
+
return Number(a.key) - Number(b.key);
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
for (const subchange of changes) {
|
|
942
|
+
if (!subchange.changes) {
|
|
943
|
+
revertLeafChange(arr, subchange, change.embeddedKey, change.embeddedKeyIsPath);
|
|
944
|
+
} else {
|
|
945
|
+
let element;
|
|
946
|
+
if (change.embeddedKey === "$index") {
|
|
947
|
+
element = arr[+subchange.key];
|
|
948
|
+
} else if (change.embeddedKey === "$value") {
|
|
949
|
+
const index = arr.indexOf(subchange.key);
|
|
950
|
+
if (index !== -1) {
|
|
951
|
+
element = arr[index];
|
|
956
952
|
}
|
|
957
|
-
segments.push({ type: "index", index: Number(indexStr) });
|
|
958
|
-
i = end + 1;
|
|
959
953
|
} else {
|
|
960
|
-
|
|
954
|
+
element = arr.find((el) => {
|
|
955
|
+
const resolved = resolveProperty(el, change.embeddedKey, change.embeddedKeyIsPath);
|
|
956
|
+
return resolved !== void 0 && String(resolved) === String(subchange.key);
|
|
957
|
+
});
|
|
961
958
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
return segments;
|
|
967
|
-
}
|
|
968
|
-
var SIMPLE_PROPERTY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
969
|
-
function formatMemberAccess(name) {
|
|
970
|
-
if (SIMPLE_PROPERTY_RE.test(name)) {
|
|
971
|
-
return `.${name}`;
|
|
972
|
-
}
|
|
973
|
-
return `['${name.replace(/'/g, "''")}']`;
|
|
974
|
-
}
|
|
975
|
-
function buildAtomPath(segments) {
|
|
976
|
-
let result = "";
|
|
977
|
-
for (const seg of segments) {
|
|
978
|
-
switch (seg.type) {
|
|
979
|
-
case "root":
|
|
980
|
-
result += "$";
|
|
981
|
-
break;
|
|
982
|
-
case "property":
|
|
983
|
-
result += formatMemberAccess(seg.name);
|
|
984
|
-
break;
|
|
985
|
-
case "index":
|
|
986
|
-
result += `[${seg.index}]`;
|
|
987
|
-
break;
|
|
988
|
-
case "keyFilter": {
|
|
989
|
-
let memberAccess;
|
|
990
|
-
if (seg.literalKey) {
|
|
991
|
-
memberAccess = `['${seg.property.replace(/'/g, "''")}']`;
|
|
992
|
-
} else if (NESTED_PATH_RE2.test(seg.property)) {
|
|
993
|
-
memberAccess = `.${seg.property}`;
|
|
994
|
-
} else {
|
|
995
|
-
memberAccess = `['${seg.property.replace(/'/g, "''")}']`;
|
|
996
|
-
}
|
|
997
|
-
result += `[?(@${memberAccess}==${formatFilterLiteral(seg.value)})]`;
|
|
998
|
-
break;
|
|
959
|
+
if (element) {
|
|
960
|
+
revertChangeset(element, subchange.changes);
|
|
999
961
|
}
|
|
1000
|
-
case "valueFilter":
|
|
1001
|
-
result += `[?(@==${formatFilterLiteral(seg.value)})]`;
|
|
1002
|
-
break;
|
|
1003
962
|
}
|
|
1004
963
|
}
|
|
1005
|
-
return
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
if (
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const valuePart = inner.slice(eqIdx);
|
|
1013
|
-
if (NESTED_PATH_RE2.test(key)) {
|
|
1014
|
-
return `[?(@.${key}${valuePart})]`;
|
|
1015
|
-
}
|
|
1016
|
-
return `[?(@['${key.replace(/'/g, "''")}']${valuePart})]`;
|
|
964
|
+
return arr;
|
|
965
|
+
};
|
|
966
|
+
var revertBranchChange = (obj, change) => {
|
|
967
|
+
if (Array.isArray(obj)) {
|
|
968
|
+
return revertArrayChange(obj, change);
|
|
969
|
+
} else {
|
|
970
|
+
return revertChangeset(obj, change.changes);
|
|
1017
971
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
const [key, endIdx] = extractQuotedString(inner, 3);
|
|
1023
|
-
const rest = inner.slice(endIdx + 2);
|
|
1024
|
-
if (rest.startsWith("==")) {
|
|
1025
|
-
const literal = rest.slice(2);
|
|
1026
|
-
const normalizedLiteral = normalizeToStringQuoted(literal);
|
|
1027
|
-
return `[?(@.${key}==${normalizedLiteral})]`;
|
|
1028
|
-
}
|
|
1029
|
-
return `[?(${inner})]`;
|
|
972
|
+
};
|
|
973
|
+
function append(basePath, nextSegment) {
|
|
974
|
+
if (!nextSegment || /[[\]'\\]/.test(nextSegment) || /^\d/.test(nextSegment)) {
|
|
975
|
+
return `${basePath}['${nextSegment.replace(/'/g, "''")}']`;
|
|
1030
976
|
}
|
|
1031
|
-
return
|
|
977
|
+
return nextSegment.includes(".") ? `${basePath}[${nextSegment}]` : `${basePath}.${nextSegment}`;
|
|
1032
978
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
979
|
+
var IDENT_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
980
|
+
var NESTED_PATH_RE2 = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
981
|
+
function filterExpression(basePath, filterKey, filterValue2, isPath) {
|
|
982
|
+
const escapedValue = `'${filterValue2.replace(/'/g, "''")}'`;
|
|
983
|
+
let memberAccess;
|
|
984
|
+
if (isPath && NESTED_PATH_RE2.test(filterKey)) {
|
|
985
|
+
memberAccess = "." + filterKey;
|
|
986
|
+
} else if (IDENT_RE.test(filterKey)) {
|
|
987
|
+
memberAccess = `.${filterKey}`;
|
|
988
|
+
} else {
|
|
989
|
+
memberAccess = `['${filterKey.replace(/'/g, "''")}']`;
|
|
1036
990
|
}
|
|
1037
|
-
|
|
1038
|
-
const stringValue = String(value).replace(/'/g, "''");
|
|
1039
|
-
return `'${stringValue}'`;
|
|
991
|
+
return `${basePath}[?(@${memberAccess}==${escapedValue})]`;
|
|
1040
992
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
993
|
+
|
|
994
|
+
// src/jsonCompare.ts
|
|
995
|
+
var CompareOperation = /* @__PURE__ */ ((CompareOperation2) => {
|
|
996
|
+
CompareOperation2["CONTAINER"] = "CONTAINER";
|
|
997
|
+
CompareOperation2["UNCHANGED"] = "UNCHANGED";
|
|
998
|
+
return CompareOperation2;
|
|
999
|
+
})(CompareOperation || {});
|
|
1000
|
+
var createValue = (value) => ({ type: "UNCHANGED" /* UNCHANGED */, value });
|
|
1001
|
+
var createContainer = (value) => ({
|
|
1002
|
+
type: "CONTAINER" /* CONTAINER */,
|
|
1003
|
+
value
|
|
1004
|
+
});
|
|
1005
|
+
var enrich = (object) => {
|
|
1006
|
+
const objectType = getTypeOfObj(object);
|
|
1007
|
+
switch (objectType) {
|
|
1008
|
+
case "Object":
|
|
1009
|
+
return Object.keys(object).map((key) => ({ key, value: enrich(object[key]) })).reduce((accumulator, entry) => {
|
|
1010
|
+
accumulator.value[entry.key] = entry.value;
|
|
1011
|
+
return accumulator;
|
|
1012
|
+
}, createContainer({}));
|
|
1013
|
+
case "Array":
|
|
1014
|
+
return object.map((value) => enrich(value)).reduce((accumulator, value) => {
|
|
1015
|
+
accumulator.value.push(value);
|
|
1016
|
+
return accumulator;
|
|
1017
|
+
}, createContainer([]));
|
|
1018
|
+
case "Function":
|
|
1019
|
+
return void 0;
|
|
1020
|
+
case "Date":
|
|
1021
|
+
default:
|
|
1022
|
+
return createValue(object);
|
|
1046
1023
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const
|
|
1057
|
-
result +=
|
|
1058
|
-
} else if (atomicPath[i] === "[") {
|
|
1059
|
-
if (atomicPath[i + 1] === "?") {
|
|
1060
|
-
const closingIdx = findFilterClose(atomicPath, i + 2);
|
|
1061
|
-
if (closingIdx === -1) throw new Error(`Unterminated filter in: ${atomicPath}`);
|
|
1062
|
-
const inner = atomicPath.slice(i + 3, closingIdx);
|
|
1063
|
-
result += canonicalizeFilterForAtom(inner);
|
|
1064
|
-
i = closingIdx + 2;
|
|
1065
|
-
} else if (atomicPath[i + 1] === "'" || /\d/.test(atomicPath[i + 1])) {
|
|
1066
|
-
const end = atomicPath.indexOf("]", i);
|
|
1067
|
-
if (end === -1) throw new Error(`Unterminated bracket in: ${atomicPath}`);
|
|
1068
|
-
result += atomicPath.slice(i, end + 1);
|
|
1069
|
-
i = end + 1;
|
|
1070
|
-
} else {
|
|
1071
|
-
const end = atomicPath.indexOf("]", i);
|
|
1072
|
-
if (end === -1) throw new Error(`Unterminated bracket in: ${atomicPath}`);
|
|
1073
|
-
const name = atomicPath.slice(i + 1, end);
|
|
1074
|
-
result += `['${name.replace(/'/g, "''")}']`;
|
|
1075
|
-
i = end + 1;
|
|
1076
|
-
}
|
|
1024
|
+
};
|
|
1025
|
+
var buildEnrichedPath = (atomicPath) => {
|
|
1026
|
+
const segments = splitJSONPath(atomicPath);
|
|
1027
|
+
let result = "value";
|
|
1028
|
+
for (let i = 1; i < segments.length; i++) {
|
|
1029
|
+
const seg = segments[i];
|
|
1030
|
+
const isLast = i === segments.length - 1;
|
|
1031
|
+
const arrayMatch = /^(.+?)\[(\d+)\]$/.exec(seg);
|
|
1032
|
+
if (arrayMatch) {
|
|
1033
|
+
const [, key, index] = arrayMatch;
|
|
1034
|
+
result += `.${key}.value[${index}]`;
|
|
1077
1035
|
} else {
|
|
1078
|
-
|
|
1036
|
+
result += `.${seg}`;
|
|
1037
|
+
}
|
|
1038
|
+
if (!isLast) {
|
|
1039
|
+
result += ".value";
|
|
1079
1040
|
}
|
|
1080
1041
|
}
|
|
1081
1042
|
return result;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1043
|
+
};
|
|
1044
|
+
var applyChangelist = (object, changelist) => {
|
|
1045
|
+
changelist.forEach((entry) => {
|
|
1046
|
+
const path = buildEnrichedPath(entry.path);
|
|
1047
|
+
switch (entry.type) {
|
|
1048
|
+
case "ADD" /* ADD */:
|
|
1049
|
+
case "UPDATE" /* UPDATE */:
|
|
1050
|
+
setByPath(object, path, { type: entry.type, value: entry.value, oldValue: entry.oldValue });
|
|
1051
|
+
break;
|
|
1052
|
+
case "REMOVE" /* REMOVE */:
|
|
1053
|
+
setByPath(object, path, { type: entry.type, value: void 0, oldValue: entry.value });
|
|
1054
|
+
break;
|
|
1055
|
+
default:
|
|
1056
|
+
throw new Error();
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
return object;
|
|
1060
|
+
};
|
|
1061
|
+
var ARRAY_WRAPPER_KEY = "_$arr";
|
|
1062
|
+
var compare2 = (oldObject, newObject) => {
|
|
1063
|
+
if (Array.isArray(oldObject) || Array.isArray(newObject)) {
|
|
1064
|
+
const wrappedOld = { [ARRAY_WRAPPER_KEY]: oldObject };
|
|
1065
|
+
const wrappedNew = { [ARRAY_WRAPPER_KEY]: newObject };
|
|
1066
|
+
const enriched = enrich(wrappedOld);
|
|
1067
|
+
const changes = atomizeChangeset(diff(wrappedOld, wrappedNew));
|
|
1068
|
+
const result = applyChangelist(enriched, changes);
|
|
1069
|
+
return result.value[ARRAY_WRAPPER_KEY];
|
|
1089
1070
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1071
|
+
return applyChangelist(enrich(oldObject), atomizeChangeset(diff(oldObject, newObject)));
|
|
1072
|
+
};
|
|
1073
|
+
var comparisonToDict = (node) => {
|
|
1074
|
+
const result = { type: node.type };
|
|
1075
|
+
if (node.type === "CONTAINER" /* CONTAINER */) {
|
|
1076
|
+
if (Array.isArray(node.value)) {
|
|
1077
|
+
const children = node.value;
|
|
1078
|
+
const serialized = new Array(children.length);
|
|
1079
|
+
for (let i = 0; i < children.length; i++) {
|
|
1080
|
+
const child = children[i];
|
|
1081
|
+
serialized[i] = child != null ? comparisonToDict(child) : null;
|
|
1098
1082
|
}
|
|
1099
|
-
result
|
|
1100
|
-
} else if (
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
} else if (atomPath[i + 1] === "'") {
|
|
1108
|
-
const [key, endIdx] = extractQuotedString(atomPath, i + 2);
|
|
1109
|
-
if (atomPath[endIdx + 1] !== "]") throw new Error(`Expected ']' in: ${atomPath}`);
|
|
1110
|
-
result += `[${key}]`;
|
|
1111
|
-
i = endIdx + 2;
|
|
1112
|
-
} else if (/\d/.test(atomPath[i + 1])) {
|
|
1113
|
-
const end = atomPath.indexOf("]", i);
|
|
1114
|
-
if (end === -1) throw new Error(`Unterminated bracket in: ${atomPath}`);
|
|
1115
|
-
result += atomPath.slice(i, end + 1);
|
|
1116
|
-
i = end + 1;
|
|
1117
|
-
} else {
|
|
1118
|
-
throw new Error(`Unexpected character after '[' in: ${atomPath}`);
|
|
1083
|
+
result.value = serialized;
|
|
1084
|
+
} else if (node.value && typeof node.value === "object") {
|
|
1085
|
+
const obj = /* @__PURE__ */ Object.create(null);
|
|
1086
|
+
for (const [key, child] of Object.entries(
|
|
1087
|
+
node.value
|
|
1088
|
+
)) {
|
|
1089
|
+
if (child == null) continue;
|
|
1090
|
+
obj[key] = comparisonToDict(child);
|
|
1119
1091
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1092
|
+
result.value = obj;
|
|
1093
|
+
}
|
|
1094
|
+
} else {
|
|
1095
|
+
if (node.type === "UNCHANGED" /* UNCHANGED */ || node.type === "ADD" /* ADD */ || node.type === "UPDATE" /* UPDATE */) {
|
|
1096
|
+
result.value = node.value;
|
|
1097
|
+
}
|
|
1098
|
+
if (node.type === "REMOVE" /* REMOVE */ || node.type === "UPDATE" /* UPDATE */) {
|
|
1099
|
+
result.oldValue = node.oldValue;
|
|
1122
1100
|
}
|
|
1123
1101
|
}
|
|
1124
1102
|
return result;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
function extractKeyFromAtomicPath(atomicPath) {
|
|
1140
|
-
if (atomicPath === "$.$root") return "$root";
|
|
1141
|
-
if (atomicPath.endsWith(")]")) {
|
|
1142
|
-
const filterStart = atomicPath.lastIndexOf("[?(");
|
|
1143
|
-
if (filterStart !== -1) {
|
|
1144
|
-
const inner = atomicPath.slice(filterStart + 3, atomicPath.length - 2);
|
|
1145
|
-
if (inner.startsWith("@==")) {
|
|
1146
|
-
const val = parseFilterLiteral(inner.slice(3));
|
|
1147
|
-
return String(val);
|
|
1103
|
+
};
|
|
1104
|
+
var IDENT_RE2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
1105
|
+
var comparisonToFlatList = (node, options = {}) => {
|
|
1106
|
+
const results = [];
|
|
1107
|
+
flattenNode(node, "$", options.includeUnchanged ?? false, results);
|
|
1108
|
+
return results;
|
|
1109
|
+
};
|
|
1110
|
+
function flattenNode(node, path, includeUnchanged, results) {
|
|
1111
|
+
if (node.type === "CONTAINER" /* CONTAINER */) {
|
|
1112
|
+
if (Array.isArray(node.value)) {
|
|
1113
|
+
for (let i = 0; i < node.value.length; i++) {
|
|
1114
|
+
const child = node.value[i];
|
|
1115
|
+
if (child == null) continue;
|
|
1116
|
+
flattenNode(child, `${path}[${i}]`, includeUnchanged, results);
|
|
1148
1117
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1118
|
+
} else if (node.value && typeof node.value === "object") {
|
|
1119
|
+
for (const [key, child] of Object.entries(
|
|
1120
|
+
node.value
|
|
1121
|
+
)) {
|
|
1122
|
+
if (child == null) continue;
|
|
1123
|
+
const childPath = IDENT_RE2.test(key) ? `${path}.${key}` : `${path}['${key.replace(/'/g, "''")}']`;
|
|
1124
|
+
flattenNode(child, childPath, includeUnchanged, results);
|
|
1153
1125
|
}
|
|
1154
1126
|
}
|
|
1127
|
+
return;
|
|
1155
1128
|
}
|
|
1156
|
-
if (
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1129
|
+
if (node.type === "UNCHANGED" /* UNCHANGED */ && !includeUnchanged) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const entry = { path, type: node.type };
|
|
1133
|
+
if (node.type === "UNCHANGED" /* UNCHANGED */ || node.type === "ADD" /* ADD */ || node.type === "UPDATE" /* UPDATE */) {
|
|
1134
|
+
entry.value = node.value;
|
|
1135
|
+
}
|
|
1136
|
+
if (node.type === "REMOVE" /* REMOVE */ || node.type === "UPDATE" /* UPDATE */) {
|
|
1137
|
+
entry.oldValue = node.oldValue;
|
|
1138
|
+
}
|
|
1139
|
+
results.push(entry);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/atomValidation.ts
|
|
1143
|
+
function isJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
1144
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
1145
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
1146
|
+
if (typeof value !== "object" || ancestors.has(value)) return false;
|
|
1147
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1148
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
|
|
1149
|
+
if (Object.getOwnPropertySymbols(value).length) return false;
|
|
1150
|
+
if (Array.isArray(value) && Object.keys(value).length !== value.length) return false;
|
|
1151
|
+
ancestors.add(value);
|
|
1152
|
+
const valid = Object.values(value).every((child) => isJsonValue(child, ancestors));
|
|
1153
|
+
ancestors.delete(value);
|
|
1154
|
+
return valid;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/atomApply.ts
|
|
1158
|
+
var owns = (obj, key) => obj !== null && typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, key);
|
|
1159
|
+
function filterValue(item, filter) {
|
|
1160
|
+
if (filter.type === "valueFilter") return item;
|
|
1161
|
+
const keys = filter.memberPath ?? (filter.literalKey ? [filter.property] : filter.property.split("."));
|
|
1162
|
+
return keys.reduce((value, key) => owns(value, key) ? value[key] : void 0, item);
|
|
1163
|
+
}
|
|
1164
|
+
function locate(parent, segment, adding = false) {
|
|
1165
|
+
if (segment.type === "property") {
|
|
1166
|
+
if (parent === null || typeof parent !== "object" || Array.isArray(parent)) {
|
|
1167
|
+
throw new Error(`Cannot access property '${segment.name}' on non-object`);
|
|
1168
|
+
}
|
|
1169
|
+
const exists = owns(parent, segment.name);
|
|
1170
|
+
if (adding ? exists : !exists) throw new Error(`Property '${segment.name}' ${exists ? "already exists" : "does not exist"}`);
|
|
1171
|
+
return segment.name;
|
|
1172
|
+
}
|
|
1173
|
+
if (!Array.isArray(parent)) {
|
|
1174
|
+
const names = { index: "access index", keyFilter: "apply key filter", valueFilter: "apply value filter" };
|
|
1175
|
+
throw new Error(`Cannot ${names[segment.type]} on non-array`);
|
|
1176
|
+
}
|
|
1177
|
+
if (segment.type === "index") {
|
|
1178
|
+
const limit = adding ? parent.length : parent.length - 1;
|
|
1179
|
+
if (segment.index > limit) throw new Error(`Array index ${segment.index} is out of bounds`);
|
|
1180
|
+
return segment.index;
|
|
1181
|
+
}
|
|
1182
|
+
const matches = parent.flatMap((item, index) => filterValue(item, segment) === segment.value ? [index] : []);
|
|
1183
|
+
if (matches.length > 1) throw new Error("Array filter matches multiple elements");
|
|
1184
|
+
if (adding) {
|
|
1185
|
+
if (matches.length) throw new Error("Array filter target already exists");
|
|
1186
|
+
return parent.length;
|
|
1187
|
+
}
|
|
1188
|
+
if (!matches.length) throw new Error("Array filter does not match an element");
|
|
1189
|
+
return matches[0];
|
|
1190
|
+
}
|
|
1191
|
+
function resolve(obj, segments) {
|
|
1192
|
+
return segments.reduce((current, segment) => current[locate(current, segment)], obj);
|
|
1193
|
+
}
|
|
1194
|
+
function selectors(path) {
|
|
1195
|
+
return parseAtomPath(path).slice(1);
|
|
1196
|
+
}
|
|
1197
|
+
function setOwn(obj, key, value) {
|
|
1198
|
+
Object.defineProperty(obj, key, { value, writable: true, enumerable: true, configurable: true });
|
|
1199
|
+
}
|
|
1200
|
+
function applyValue(obj, op) {
|
|
1201
|
+
const segments = selectors(op.path);
|
|
1202
|
+
if (segments.length === 0) {
|
|
1203
|
+
if (op.op === "add" && obj !== null) throw new Error("Root add requires a null source");
|
|
1204
|
+
if (op.op !== "add" && obj === null) throw new Error("Root remove/replace requires a non-null source");
|
|
1205
|
+
if (op.op === "remove") return null;
|
|
1206
|
+
const value = structuredClone(op.value);
|
|
1207
|
+
if (op.op === "replace" && obj && value && typeof obj === "object" && typeof value === "object" && !Array.isArray(obj) && !Array.isArray(value)) {
|
|
1208
|
+
for (const key2 of Object.keys(obj)) delete obj[key2];
|
|
1209
|
+
for (const key2 of Object.keys(value)) setOwn(obj, key2, value[key2]);
|
|
1210
|
+
return obj;
|
|
1211
|
+
}
|
|
1212
|
+
return value;
|
|
1213
|
+
}
|
|
1214
|
+
const last = segments.at(-1);
|
|
1215
|
+
const parent = resolve(obj, segments.slice(0, -1));
|
|
1216
|
+
const key = locate(parent, last, op.op === "add");
|
|
1217
|
+
if (op.op !== "remove" && last.type === "keyFilter" && filterValue(op.value, last) !== last.value) {
|
|
1218
|
+
throw new Error("Keyed-array value does not match its identity filter");
|
|
1219
|
+
}
|
|
1220
|
+
if (op.op === "add" && last.type === "valueFilter" && op.value !== last.value) {
|
|
1221
|
+
throw new Error("Added value does not match its value filter");
|
|
1222
|
+
}
|
|
1223
|
+
if (op.op === "remove") {
|
|
1224
|
+
if (Array.isArray(parent)) parent.splice(Number(key), 1);
|
|
1225
|
+
else delete parent[key];
|
|
1226
|
+
} else {
|
|
1227
|
+
const value = structuredClone(op.value);
|
|
1228
|
+
if (Array.isArray(parent) && op.op === "add") parent.splice(Number(key), 0, value);
|
|
1229
|
+
else setOwn(parent, key, value);
|
|
1163
1230
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1231
|
+
return obj;
|
|
1232
|
+
}
|
|
1233
|
+
function applyAtomOperations(obj, atom) {
|
|
1234
|
+
let result = obj;
|
|
1235
|
+
for (const op of atom.operations) {
|
|
1236
|
+
if (op.op === "move" || op.op === "copy") {
|
|
1237
|
+
const value = resolve(result, selectors(op.from));
|
|
1238
|
+
const add = { op: "add", path: op.path, value };
|
|
1239
|
+
if (op.op === "move") {
|
|
1240
|
+
const remove = { op: "remove", path: op.from };
|
|
1241
|
+
const probe = applyValue(structuredClone(result), remove);
|
|
1242
|
+
applyValue(probe, add);
|
|
1243
|
+
result = applyValue(result, remove);
|
|
1244
|
+
}
|
|
1245
|
+
result = applyValue(result, add);
|
|
1246
|
+
} else {
|
|
1247
|
+
result = applyValue(result, op);
|
|
1248
|
+
}
|
|
1167
1249
|
}
|
|
1168
|
-
return
|
|
1250
|
+
return result;
|
|
1169
1251
|
}
|
|
1170
1252
|
|
|
1171
1253
|
// src/jsonAtom.ts
|
|
@@ -1178,8 +1260,8 @@ function validateAtom(atom) {
|
|
|
1178
1260
|
if (d.format !== "json-atom") {
|
|
1179
1261
|
errors.push(`Invalid or missing format: expected 'json-atom', got '${d.format}'`);
|
|
1180
1262
|
}
|
|
1181
|
-
if (
|
|
1182
|
-
errors.push(`Missing or invalid version: expected
|
|
1263
|
+
if (d.version !== 1) {
|
|
1264
|
+
errors.push(`Missing or invalid version: expected 1, got '${d.version}'`);
|
|
1183
1265
|
}
|
|
1184
1266
|
if (!Array.isArray(d.operations)) {
|
|
1185
1267
|
errors.push("Missing or invalid operations: expected array");
|
|
@@ -1196,6 +1278,18 @@ function validateAtom(atom) {
|
|
|
1196
1278
|
if (typeof op.path !== "string") {
|
|
1197
1279
|
errors.push(`operations[${i}]: path must be a string`);
|
|
1198
1280
|
}
|
|
1281
|
+
for (const field of ["value", "oldValue"]) {
|
|
1282
|
+
if (field in op && !isJsonValue(op[field])) errors.push(`operations[${i}]: ${field} must be a JSON value`);
|
|
1283
|
+
}
|
|
1284
|
+
for (const field of ["path", ...["move", "copy"].includes(op.op) ? ["from"] : []]) {
|
|
1285
|
+
if (typeof op[field] === "string") {
|
|
1286
|
+
try {
|
|
1287
|
+
parseAtomPath(op[field]);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
errors.push(`operations[${i}]: invalid ${field}: ${error.message}`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1199
1293
|
if (op.op === "add") {
|
|
1200
1294
|
if (!("value" in op)) {
|
|
1201
1295
|
errors.push(`operations[${i}]: add operation must have value`);
|
|
@@ -1220,11 +1314,13 @@ function validateAtom(atom) {
|
|
|
1220
1314
|
if ("oldValue" in op) {
|
|
1221
1315
|
errors.push(`operations[${i}]: move operation must not have oldValue`);
|
|
1222
1316
|
}
|
|
1223
|
-
if (typeof op.from === "string" && typeof op.path === "string") {
|
|
1224
|
-
|
|
1317
|
+
if (typeof op.from === "string" && typeof op.path === "string" && !errors.length) {
|
|
1318
|
+
const from = buildAtomPath(parseAtomPath(op.from));
|
|
1319
|
+
const path = buildAtomPath(parseAtomPath(op.path));
|
|
1320
|
+
if (from === path) {
|
|
1225
1321
|
errors.push(`operations[${i}]: move operation from must not equal path (self-move)`);
|
|
1226
1322
|
}
|
|
1227
|
-
if (
|
|
1323
|
+
if (path.startsWith(from + ".") || path.startsWith(from + "[")) {
|
|
1228
1324
|
errors.push(`operations[${i}]: move operation path must not be a subtree of from`);
|
|
1229
1325
|
}
|
|
1230
1326
|
}
|
|
@@ -1249,11 +1345,16 @@ function diffAtom(oldObj, newObj, options = {}) {
|
|
|
1249
1345
|
});
|
|
1250
1346
|
const operations = [];
|
|
1251
1347
|
walkChanges(changeset, "$", oldObj, newObj, operations, options);
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1348
|
+
if (operations.length === 1 && operations[0].path === "$") {
|
|
1349
|
+
if (oldObj === null && newObj !== null) operations[0] = { op: "add", path: "$", value: newObj };
|
|
1350
|
+
if (newObj === null && oldObj !== null) {
|
|
1351
|
+
operations[0] = { op: "remove", path: "$", ...options.reversible === false ? {} : { oldValue: oldObj } };
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
const atom = { format: "json-atom", version: 1, operations };
|
|
1355
|
+
const validation = validateAtom(atom);
|
|
1356
|
+
if (!validation.valid) throw new Error(`Cannot produce JSON Atom: ${validation.errors.join(", ")}`);
|
|
1357
|
+
return atom;
|
|
1257
1358
|
}
|
|
1258
1359
|
function mergeTypeChangePairs(changes) {
|
|
1259
1360
|
const result = [];
|
|
@@ -1274,6 +1375,7 @@ function mergeTypeChangePairs(changes) {
|
|
|
1274
1375
|
}
|
|
1275
1376
|
return result;
|
|
1276
1377
|
}
|
|
1378
|
+
var NESTED_PATH_RE3 = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
1277
1379
|
var SIMPLE_PROPERTY_RE2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
1278
1380
|
function appendCanonicalProperty(basePath, name) {
|
|
1279
1381
|
if (SIMPLE_PROPERTY_RE2.test(name)) {
|
|
@@ -1284,46 +1386,91 @@ function appendCanonicalProperty(basePath, name) {
|
|
|
1284
1386
|
function walkChanges(changes, basePath, oldCtx, newCtx, ops, options) {
|
|
1285
1387
|
const merged = mergeTypeChangePairs(changes);
|
|
1286
1388
|
for (const change of merged) {
|
|
1389
|
+
const isRoot = change.key === "$root" && basePath === "$" && (oldCtx === null || newCtx === null || typeof oldCtx !== "object" || typeof newCtx !== "object" || Array.isArray(oldCtx) || Array.isArray(newCtx));
|
|
1287
1390
|
if (change.isMergedReplace) {
|
|
1288
1391
|
const mc = change;
|
|
1289
|
-
const path =
|
|
1392
|
+
const path = isRoot ? "$" : appendCanonicalProperty(basePath, mc.key);
|
|
1290
1393
|
const op = { op: "replace", path, value: mc.addValue };
|
|
1291
1394
|
if (options.reversible !== false) {
|
|
1292
1395
|
op.oldValue = mc.removeValue;
|
|
1293
1396
|
}
|
|
1294
1397
|
ops.push(op);
|
|
1295
1398
|
} else if (change.changes) {
|
|
1296
|
-
const childPath =
|
|
1297
|
-
const childOld =
|
|
1298
|
-
const childNew =
|
|
1399
|
+
const childPath = isRoot ? "$" : appendCanonicalProperty(basePath, change.key);
|
|
1400
|
+
const childOld = isRoot ? oldCtx : oldCtx?.[change.key];
|
|
1401
|
+
const childNew = isRoot ? newCtx : newCtx?.[change.key];
|
|
1299
1402
|
if (change.embeddedKey) {
|
|
1300
|
-
|
|
1301
|
-
const filterPath = buildCanonicalFilterPath(
|
|
1302
|
-
childPath,
|
|
1303
|
-
change.embeddedKey,
|
|
1304
|
-
childChange.key,
|
|
1305
|
-
childOld,
|
|
1306
|
-
childNew,
|
|
1307
|
-
childChange,
|
|
1308
|
-
change.embeddedKeyIsPath
|
|
1309
|
-
);
|
|
1310
|
-
if (childChange.changes) {
|
|
1311
|
-
const oldEl = findElement(childOld, change.embeddedKey, childChange.key);
|
|
1312
|
-
const newEl = findElement(childNew, change.embeddedKey, childChange.key);
|
|
1313
|
-
walkChanges(childChange.changes, filterPath, oldEl, newEl, ops, options);
|
|
1314
|
-
} else {
|
|
1315
|
-
emitLeafOp(childChange, filterPath, ops, options);
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1403
|
+
walkArrayChanges(change, childPath, childOld, childNew, ops, options);
|
|
1318
1404
|
} else {
|
|
1319
1405
|
walkChanges(change.changes, childPath, childOld, childNew, ops, options);
|
|
1320
1406
|
}
|
|
1321
1407
|
} else {
|
|
1322
|
-
const path =
|
|
1408
|
+
const path = isRoot ? "$" : appendCanonicalProperty(basePath, change.key);
|
|
1323
1409
|
emitLeafOp(change, path, ops, options);
|
|
1324
1410
|
}
|
|
1325
1411
|
}
|
|
1326
1412
|
}
|
|
1413
|
+
function walkArrayChanges(change, path, oldArr, newArr, ops, options) {
|
|
1414
|
+
const ordered = orderArrayChildChanges(change.changes, change.embeddedKey);
|
|
1415
|
+
for (const child of mergeTypeChangePairs(ordered)) {
|
|
1416
|
+
const childPath = buildCanonicalFilterPath(path, change.embeddedKey, child.key, oldArr, newArr, child, change.embeddedKeyIsPath);
|
|
1417
|
+
if (child.isMergedReplace) {
|
|
1418
|
+
ops.push({
|
|
1419
|
+
op: "replace",
|
|
1420
|
+
path: childPath,
|
|
1421
|
+
value: child.addValue,
|
|
1422
|
+
...options.reversible === false ? {} : { oldValue: child.removeValue }
|
|
1423
|
+
});
|
|
1424
|
+
} else if (child.changes) {
|
|
1425
|
+
const oldEl = findElement(oldArr, change.embeddedKey, child.key, change.embeddedKeyIsPath);
|
|
1426
|
+
const newEl = findElement(newArr, change.embeddedKey, child.key, change.embeddedKeyIsPath);
|
|
1427
|
+
if (child.embeddedKey) walkArrayChanges(child, childPath, oldEl, newEl, ops, options);
|
|
1428
|
+
else walkChanges(child.changes, childPath, oldEl, newEl, ops, options);
|
|
1429
|
+
} else {
|
|
1430
|
+
emitLeafOp(child, childPath, ops, options);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
function orderArrayChildChanges(changes, embeddedKey) {
|
|
1435
|
+
if (embeddedKey !== "$index") {
|
|
1436
|
+
return changes;
|
|
1437
|
+
}
|
|
1438
|
+
const groups = [];
|
|
1439
|
+
const pureRemoves = [];
|
|
1440
|
+
for (let i = 0; i < changes.length; i++) {
|
|
1441
|
+
const current = changes[i];
|
|
1442
|
+
const next = changes[i + 1];
|
|
1443
|
+
if (current.type === "REMOVE" /* REMOVE */ && next && next.type === "ADD" /* ADD */ && String(current.key) === String(next.key)) {
|
|
1444
|
+
groups.push({ kind: "preserved", changes: [current, next] });
|
|
1445
|
+
i++;
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
if (current.type === "REMOVE" /* REMOVE */) {
|
|
1449
|
+
pureRemoves.push(current);
|
|
1450
|
+
groups.push({ kind: "pure-remove" });
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
groups.push({ kind: "preserved", changes: [current] });
|
|
1454
|
+
}
|
|
1455
|
+
if (pureRemoves.length < 2) {
|
|
1456
|
+
return changes;
|
|
1457
|
+
}
|
|
1458
|
+
const removeIndices = pureRemoves.map((change) => Number(change.key));
|
|
1459
|
+
if (removeIndices.some((idx) => !Number.isInteger(idx))) {
|
|
1460
|
+
return changes;
|
|
1461
|
+
}
|
|
1462
|
+
pureRemoves.sort((a, b) => Number(b.key) - Number(a.key));
|
|
1463
|
+
const ordered = [];
|
|
1464
|
+
let removeIndex = 0;
|
|
1465
|
+
for (const group of groups) {
|
|
1466
|
+
if (group.kind === "pure-remove") {
|
|
1467
|
+
ordered.push(pureRemoves[removeIndex++]);
|
|
1468
|
+
} else {
|
|
1469
|
+
ordered.push(...group.changes);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
return ordered;
|
|
1473
|
+
}
|
|
1327
1474
|
function emitLeafOp(change, path, ops, options) {
|
|
1328
1475
|
switch (change.type) {
|
|
1329
1476
|
case "ADD" /* ADD */: {
|
|
@@ -1398,7 +1545,7 @@ function findActualValue(oldArr, newArr, stringKey, opType) {
|
|
|
1398
1545
|
}
|
|
1399
1546
|
return stringKey;
|
|
1400
1547
|
}
|
|
1401
|
-
function findElement(arr, embeddedKey, changeKey) {
|
|
1548
|
+
function findElement(arr, embeddedKey, changeKey, isPath = false) {
|
|
1402
1549
|
if (!arr || !Array.isArray(arr)) return void 0;
|
|
1403
1550
|
if (embeddedKey === "$index") {
|
|
1404
1551
|
return arr[Number(changeKey)];
|
|
@@ -1409,7 +1556,7 @@ function findElement(arr, embeddedKey, changeKey) {
|
|
|
1409
1556
|
if (typeof embeddedKey === "function") {
|
|
1410
1557
|
return arr.find((item) => String(embeddedKey(item)) === changeKey);
|
|
1411
1558
|
}
|
|
1412
|
-
return arr.find((item) => item && String(item
|
|
1559
|
+
return arr.find((item) => item && String(resolveNestedKey(item, embeddedKey, isPath)) === changeKey);
|
|
1413
1560
|
}
|
|
1414
1561
|
function resolveNestedKey(item, key, isPath) {
|
|
1415
1562
|
if (isPath) {
|
|
@@ -1474,9 +1621,30 @@ function toAtom(changeset, options = {}) {
|
|
|
1474
1621
|
throw new Error(`Unknown operation type: ${atom.type}`);
|
|
1475
1622
|
}
|
|
1476
1623
|
});
|
|
1477
|
-
const operations = mergeConsecutiveOps(rawOps);
|
|
1624
|
+
const operations = orderLegacyIndexRemoves(mergeConsecutiveOps(rawOps));
|
|
1478
1625
|
return { format: "json-atom", version: 1, operations };
|
|
1479
1626
|
}
|
|
1627
|
+
function orderLegacyIndexRemoves(operations) {
|
|
1628
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1629
|
+
operations.forEach((operation, slot) => {
|
|
1630
|
+
if (operation.op !== "remove") return;
|
|
1631
|
+
const segments = parseAtomPath(operation.path);
|
|
1632
|
+
const last = segments.at(-1);
|
|
1633
|
+
if (last.type !== "index") return;
|
|
1634
|
+
const parent = buildAtomPath(segments.slice(0, -1));
|
|
1635
|
+
const group = groups.get(parent) ?? [];
|
|
1636
|
+
group.push({ slot, index: last.index, operation });
|
|
1637
|
+
groups.set(parent, group);
|
|
1638
|
+
});
|
|
1639
|
+
const result = [...operations];
|
|
1640
|
+
for (const group of groups.values()) {
|
|
1641
|
+
const ordered = [...group].sort((a, b) => b.index - a.index);
|
|
1642
|
+
group.forEach((entry, index) => {
|
|
1643
|
+
result[entry.slot] = ordered[index].operation;
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
return result;
|
|
1647
|
+
}
|
|
1480
1648
|
function mergeConsecutiveOps(ops) {
|
|
1481
1649
|
const result = [];
|
|
1482
1650
|
let i = 0;
|
|
@@ -1569,8 +1737,13 @@ function invertAtom(atom) {
|
|
|
1569
1737
|
return { op: "remove", path: op.path, oldValue: op.value, ...extensions };
|
|
1570
1738
|
case "remove":
|
|
1571
1739
|
return { op: "add", path: op.path, value: op.oldValue, ...extensions };
|
|
1572
|
-
case "replace":
|
|
1573
|
-
return { op: "
|
|
1740
|
+
case "replace": {
|
|
1741
|
+
if (op.path === "$" && op.value === null) return { op: "add", path: "$", value: op.oldValue, ...extensions };
|
|
1742
|
+
const segments = parseAtomPath(op.path);
|
|
1743
|
+
const last = segments.at(-1);
|
|
1744
|
+
if (last.type === "valueFilter") last.value = op.value;
|
|
1745
|
+
return { op: "replace", path: buildAtomPath(segments), value: op.oldValue, oldValue: op.value, ...extensions };
|
|
1746
|
+
}
|
|
1574
1747
|
case "move":
|
|
1575
1748
|
return { op: "move", from: op.path, path: op.from, ...extensions };
|
|
1576
1749
|
case "copy":
|
|
@@ -1588,134 +1761,10 @@ function invertAtom(atom) {
|
|
|
1588
1761
|
}
|
|
1589
1762
|
return envelope;
|
|
1590
1763
|
}
|
|
1591
|
-
var NESTED_PATH_RE3 = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
1592
|
-
function resolveValueAtPath(obj, atomPath) {
|
|
1593
|
-
const segments = parseAtomPath(atomPath);
|
|
1594
|
-
let current = obj;
|
|
1595
|
-
for (const seg of segments) {
|
|
1596
|
-
switch (seg.type) {
|
|
1597
|
-
case "root":
|
|
1598
|
-
break;
|
|
1599
|
-
case "property":
|
|
1600
|
-
if (current == null || typeof current !== "object") {
|
|
1601
|
-
throw new Error(`Cannot access property '${seg.name}' on ${current === null ? "null" : typeof current} at path: ${atomPath}`);
|
|
1602
|
-
}
|
|
1603
|
-
current = current[seg.name];
|
|
1604
|
-
break;
|
|
1605
|
-
case "index":
|
|
1606
|
-
if (!Array.isArray(current)) {
|
|
1607
|
-
throw new Error(`Cannot access index ${seg.index} on non-array at path: ${atomPath}`);
|
|
1608
|
-
}
|
|
1609
|
-
current = current[seg.index];
|
|
1610
|
-
break;
|
|
1611
|
-
case "keyFilter": {
|
|
1612
|
-
if (!Array.isArray(current)) {
|
|
1613
|
-
throw new Error(`Cannot apply key filter on non-array at path: ${atomPath}`);
|
|
1614
|
-
}
|
|
1615
|
-
const prop = seg.property;
|
|
1616
|
-
const isPath = !seg.literalKey && prop.includes(".") && NESTED_PATH_RE3.test(prop);
|
|
1617
|
-
current = current.find((el) => {
|
|
1618
|
-
const resolved = isPath ? prop.split(".").reduce((c, s) => c?.[s], el) : el[prop];
|
|
1619
|
-
return JSON.stringify(resolved) === JSON.stringify(seg.value);
|
|
1620
|
-
});
|
|
1621
|
-
break;
|
|
1622
|
-
}
|
|
1623
|
-
case "valueFilter": {
|
|
1624
|
-
if (!Array.isArray(current)) {
|
|
1625
|
-
throw new Error(`Cannot apply value filter on non-array at path: ${atomPath}`);
|
|
1626
|
-
}
|
|
1627
|
-
current = current.find((el) => JSON.stringify(el) === JSON.stringify(seg.value));
|
|
1628
|
-
break;
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
return current;
|
|
1633
|
-
}
|
|
1634
1764
|
function applyAtom(obj, atom) {
|
|
1635
1765
|
const validation = validateAtom(atom);
|
|
1636
|
-
if (!validation.valid) {
|
|
1637
|
-
|
|
1638
|
-
}
|
|
1639
|
-
let result = obj;
|
|
1640
|
-
for (const op of atom.operations) {
|
|
1641
|
-
if (op.op === "move") {
|
|
1642
|
-
const value = resolveValueAtPath(result, op.from);
|
|
1643
|
-
const removeOp = { op: "remove", path: op.from, oldValue: value };
|
|
1644
|
-
if (removeOp.path === "$") {
|
|
1645
|
-
result = applyRootOp(result, removeOp);
|
|
1646
|
-
} else {
|
|
1647
|
-
const removeChange = atomOpToAtomicChange(removeOp);
|
|
1648
|
-
applyChangeset(result, unatomizeChangeset([removeChange]));
|
|
1649
|
-
}
|
|
1650
|
-
const addOp = { op: "add", path: op.path, value };
|
|
1651
|
-
if (addOp.path === "$") {
|
|
1652
|
-
result = applyRootOp(result, addOp);
|
|
1653
|
-
} else {
|
|
1654
|
-
const addChange = atomOpToAtomicChange(addOp);
|
|
1655
|
-
applyChangeset(result, unatomizeChangeset([addChange]));
|
|
1656
|
-
}
|
|
1657
|
-
} else if (op.op === "copy") {
|
|
1658
|
-
const source = resolveValueAtPath(result, op.from);
|
|
1659
|
-
const value = source === void 0 ? void 0 : JSON.parse(JSON.stringify(source));
|
|
1660
|
-
const addOp = { op: "add", path: op.path, value };
|
|
1661
|
-
if (addOp.path === "$") {
|
|
1662
|
-
result = applyRootOp(result, addOp);
|
|
1663
|
-
} else {
|
|
1664
|
-
const addChange = atomOpToAtomicChange(addOp);
|
|
1665
|
-
applyChangeset(result, unatomizeChangeset([addChange]));
|
|
1666
|
-
}
|
|
1667
|
-
} else if (op.path === "$") {
|
|
1668
|
-
result = applyRootOp(result, op);
|
|
1669
|
-
} else {
|
|
1670
|
-
const atomicChange = atomOpToAtomicChange(op);
|
|
1671
|
-
const miniChangeset = unatomizeChangeset([atomicChange]);
|
|
1672
|
-
applyChangeset(result, miniChangeset);
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
return result;
|
|
1676
|
-
}
|
|
1677
|
-
function applyRootOp(obj, op) {
|
|
1678
|
-
switch (op.op) {
|
|
1679
|
-
case "add":
|
|
1680
|
-
return op.value;
|
|
1681
|
-
case "remove":
|
|
1682
|
-
return null;
|
|
1683
|
-
case "replace": {
|
|
1684
|
-
if (typeof obj === "object" && obj !== null && !Array.isArray(obj) && typeof op.value === "object" && op.value !== null && !Array.isArray(op.value)) {
|
|
1685
|
-
for (const key of Object.keys(obj)) {
|
|
1686
|
-
delete obj[key];
|
|
1687
|
-
}
|
|
1688
|
-
Object.assign(obj, op.value);
|
|
1689
|
-
return obj;
|
|
1690
|
-
}
|
|
1691
|
-
return op.value;
|
|
1692
|
-
}
|
|
1693
|
-
/* istanbul ignore next -- exhaustive switch */
|
|
1694
|
-
default:
|
|
1695
|
-
throw new Error(`Unknown operation: ${op.op}`);
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
function atomOpToAtomicChange(op) {
|
|
1699
|
-
const atomicPath = atomPathToAtomicPath(op.path);
|
|
1700
|
-
const key = extractKeyFromAtomicPath(atomicPath);
|
|
1701
|
-
switch (op.op) {
|
|
1702
|
-
case "add":
|
|
1703
|
-
return { type: "ADD" /* ADD */, key, path: atomicPath, valueType: getValueType(op.value), value: op.value };
|
|
1704
|
-
case "remove":
|
|
1705
|
-
return { type: "REMOVE" /* REMOVE */, key, path: atomicPath, valueType: getValueType(op.oldValue), value: op.oldValue };
|
|
1706
|
-
case "replace":
|
|
1707
|
-
return {
|
|
1708
|
-
type: "UPDATE" /* UPDATE */,
|
|
1709
|
-
key,
|
|
1710
|
-
path: atomicPath,
|
|
1711
|
-
valueType: getValueType(op.value),
|
|
1712
|
-
value: op.value,
|
|
1713
|
-
oldValue: op.oldValue
|
|
1714
|
-
};
|
|
1715
|
-
/* istanbul ignore next -- exhaustive switch */
|
|
1716
|
-
default:
|
|
1717
|
-
throw new Error(`Unknown operation: ${op.op}`);
|
|
1718
|
-
}
|
|
1766
|
+
if (!validation.valid) throw new Error(`Invalid atom: ${validation.errors.join(", ")}`);
|
|
1767
|
+
return applyAtomOperations(obj, atom);
|
|
1719
1768
|
}
|
|
1720
1769
|
function revertAtom(obj, atom) {
|
|
1721
1770
|
const inverse = invertAtom(atom);
|
|
@@ -1723,10 +1772,11 @@ function revertAtom(obj, atom) {
|
|
|
1723
1772
|
}
|
|
1724
1773
|
|
|
1725
1774
|
// src/atomHelpers.ts
|
|
1726
|
-
var OP_SPEC_KEYS = /* @__PURE__ */ new Set(["op", "path", "value", "oldValue"]);
|
|
1775
|
+
var OP_SPEC_KEYS = /* @__PURE__ */ new Set(["op", "path", "from", "value", "oldValue"]);
|
|
1727
1776
|
var ATOM_SPEC_KEYS = /* @__PURE__ */ new Set(["format", "version", "operations"]);
|
|
1728
1777
|
function operationSpecDict(op) {
|
|
1729
1778
|
const result = { op: op.op, path: op.path };
|
|
1779
|
+
if ("from" in op) result.from = op.from;
|
|
1730
1780
|
if ("value" in op) result.value = op.value;
|
|
1731
1781
|
if ("oldValue" in op) result.oldValue = op.oldValue;
|
|
1732
1782
|
return result;
|