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