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