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.cjs CHANGED
@@ -58,6 +58,375 @@ __export(index_exports, {
58
58
  });
59
59
  module.exports = __toCommonJS(index_exports);
60
60
 
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);
68
+ }
69
+ if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
70
+ throw new Error(`Cannot format filter literal for type ${typeof value}`);
71
+ }
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;
80
+ }
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;
85
+ }
86
+ throw new Error(`Invalid filter literal: ${s}`);
87
+ }
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;
96
+ } else {
97
+ return [result.join(""), i];
98
+ }
99
+ } else {
100
+ result.push(s[i]);
101
+ i += 1;
102
+ }
103
+ }
104
+ throw new Error("Unterminated quoted string");
105
+ }
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;
115
+ } else {
116
+ i += 1;
117
+ break;
118
+ }
119
+ } else {
120
+ i += 1;
121
+ }
122
+ }
123
+ } else if (s[i] === ")" && i + 1 < s.length && s[i + 1] === "]") {
124
+ return i;
125
+ } else {
126
+ i += 1;
127
+ }
128
+ }
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;
142
+ }
143
+ quoted = !quoted;
144
+ } else if (!quoted && inner.slice(i, i + 2) === "==") {
145
+ eq = i;
146
+ break;
147
+ }
148
+ }
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}`);
168
+ }
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
+
61
430
  // src/helpers.ts
62
431
  function splitJSONPath(path) {
63
432
  const parts = [];
@@ -94,9 +463,11 @@ function arrayIntersection(first, second) {
94
463
  return first.filter((item) => secondSet.has(item));
95
464
  }
96
465
  function keyBy(arr, getKey2) {
97
- const result = {};
466
+ const result = /* @__PURE__ */ Object.create(null);
98
467
  for (const [index, item] of Object.entries(arr)) {
99
- result[String(getKey2(item, false, Number(index)))] = item;
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;
100
471
  }
101
472
  return result;
102
473
  }
@@ -122,7 +493,7 @@ var Operation = /* @__PURE__ */ ((Operation2) => {
122
493
  })(Operation || {});
123
494
  function diff(oldObj, newObj, options = {}) {
124
495
  let embeddedObjKeys = options.arrayIdentityKeys ?? options.embeddedObjKeys;
125
- const { keysToSkip, treatTypeChangeAsReplace } = options;
496
+ const { keysToSkip, keysToInclude, treatTypeChangeAsReplace } = options;
126
497
  if (embeddedObjKeys instanceof Map) {
127
498
  embeddedObjKeys = new Map(
128
499
  Array.from(embeddedObjKeys.entries()).map(([key, value]) => [
@@ -138,6 +509,7 @@ function diff(oldObj, newObj, options = {}) {
138
509
  return compare(oldObj, newObj, [], [], {
139
510
  embeddedObjKeys,
140
511
  keysToSkip: keysToSkip ?? [],
512
+ keysToInclude,
141
513
  treatTypeChangeAsReplace: treatTypeChangeAsReplace ?? true
142
514
  });
143
515
  }
@@ -156,7 +528,7 @@ var applyChangeset = (obj, changeset) => {
156
528
  };
157
529
  var revertChangeset = (obj, changeset) => {
158
530
  if (changeset) {
159
- changeset.reverse().forEach((change) => {
531
+ [...changeset].reverse().forEach((change) => {
160
532
  const { value, type } = change;
161
533
  if (!change.changes || value === null && type === "REMOVE" /* REMOVE */) {
162
534
  revertLeafChange(obj, change);
@@ -177,27 +549,16 @@ var atomizeChangeset = (obj, path = "$", embeddedKey, embeddedKeyIsPath) => {
177
549
  if (atomicChange) {
178
550
  return atomicChange;
179
551
  }
552
+ if (!obj.changes) {
553
+ return [{ ...obj, path, valueType: getTypeOfObj(obj.value) }];
554
+ }
180
555
  } else {
181
556
  path = append(path, obj.key);
182
557
  }
183
558
  return atomizeChangeset(obj.changes || obj, path, obj.embeddedKey, obj.embeddedKeyIsPath);
184
559
  } else {
185
560
  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
- }
200
- }
561
+ const finalPath = append(path, obj.key);
201
562
  return [
202
563
  {
203
564
  ...obj,
@@ -233,86 +594,33 @@ var handleArray = (obj, path, embeddedKey, embeddedKeyIsPath) => {
233
594
  return obj.reduce((memo, change) => [...memo, ...atomizeChangeset(change, path, embeddedKey, embeddedKeyIsPath)], []);
234
595
  };
235
596
  var unatomizeChangeset = (changes) => {
236
- if (!Array.isArray(changes)) {
237
- changes = [changes];
238
- }
239
- const changesArr = [];
240
- changes.forEach((change) => {
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
- ];
282
- } else {
283
- ptr.key = key;
284
- ptr.embeddedKey = embeddedKey;
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;
296
- }
297
- } else {
298
- if (i === segments.length - 1) {
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
- }
310
- }
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 };
311
603
  }
312
- changesArr.push(obj);
313
- }
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);
314
623
  });
315
- return changesArr;
316
624
  };
317
625
  var getTypeOfObj = (obj) => {
318
626
  if (obj === void 0) {
@@ -327,32 +635,15 @@ var getKey = (path) => {
327
635
  const left = path.at(-1);
328
636
  return left ?? "$root";
329
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
+ };
330
644
  var compare = (oldObj, newObj, path, keyPath, options) => {
331
645
  let changes = [];
332
- const currentPath = keyPath.join(".");
333
- if (options.keysToSkip?.some((skipPath) => {
334
- if (currentPath === skipPath) {
335
- return true;
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;
350
- }
351
- }
352
- return false;
353
- })) {
354
- return changes;
355
- }
646
+ if (!shouldComparePath(keyPath.join("."), options)) return changes;
356
647
  const typeOfOldObj = getTypeOfObj(oldObj);
357
648
  const typeOfNewObj = getTypeOfObj(newObj);
358
649
  if (options.treatTypeChangeAsReplace && typeOfOldObj !== typeOfNewObj) {
@@ -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 (options.keysToSkip?.some((skipPath2) => currentPath === skipPath2 || currentPath.startsWith(skipPath2 + "."))) {
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 (options.keysToSkip?.some((skipPath2) => currentPath === skipPath2 || currentPath.startsWith(skipPath2 + "."))) {
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(".") && NESTED_PATH_RE.test(resolvedKey) && (isFunctionKey || !(sampleEl != null && typeof sampleEl === "object" && resolvedKey in sampleEl));
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(".") && NESTED_PATH_RE.test(uniqKey);
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
  }
@@ -690,12 +983,23 @@ var revertLeafChange = (obj, change, embeddedKey = "$index", isPath) => {
690
983
  case "UPDATE" /* UPDATE */:
691
984
  return modifyKeyValue(obj, key, oldValue);
692
985
  case "REMOVE" /* REMOVE */:
693
- return addKeyValue(obj, key, value);
986
+ return addKeyValue(obj, key, value, embeddedKey);
694
987
  }
695
988
  };
696
989
  var revertArrayChange = (arr, change) => {
697
- for (const subchange of change.changes) {
698
- if (subchange.value != null || subchange.type === "REMOVE" /* REMOVE */) {
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);
995
+ }
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) {
699
1003
  revertLeafChange(arr, subchange, change.embeddedKey, change.embeddedKeyIsPath);
700
1004
  } else {
701
1005
  let element;
@@ -727,14 +1031,17 @@ var revertBranchChange = (obj, change) => {
727
1031
  }
728
1032
  };
729
1033
  function append(basePath, nextSegment) {
1034
+ if (!nextSegment || /[[\]'\\]/.test(nextSegment) || /^\d/.test(nextSegment)) {
1035
+ return `${basePath}['${nextSegment.replace(/'/g, "''")}']`;
1036
+ }
730
1037
  return nextSegment.includes(".") ? `${basePath}[${nextSegment}]` : `${basePath}.${nextSegment}`;
731
1038
  }
732
1039
  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, "''")}'`;
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, "''")}'`;
736
1043
  let memberAccess;
737
- if (isPath && NESTED_PATH_RE.test(filterKey)) {
1044
+ if (isPath && NESTED_PATH_RE2.test(filterKey)) {
738
1045
  memberAccess = "." + filterKey;
739
1046
  } else if (IDENT_RE.test(filterKey)) {
740
1047
  memberAccess = `.${filterKey}`;
@@ -838,394 +1145,169 @@ var comparisonToDict = (node) => {
838
1145
  const obj = /* @__PURE__ */ Object.create(null);
839
1146
  for (const [key, child] of Object.entries(
840
1147
  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;
948
- }
949
- } else {
950
- i += 1;
951
- }
952
- }
953
- } else if (s[i] === ")" && i + 1 < s.length && s[i + 1] === "]") {
954
- return i;
955
- } else {
956
- i += 1;
957
- }
958
- }
959
- return -1;
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
- }
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
- }
980
- throw new Error(`Invalid filter expression: ${inner}`);
981
- }
982
- function parseAtomPath(path) {
983
- if (!path.startsWith("$")) {
984
- throw new Error(`Path must start with '$': ${path}`);
985
- }
986
- const segments = [{ type: "root" }];
987
- let i = 1;
988
- while (i < path.length) {
989
- if (path[i] === ".") {
990
- i += 1;
991
- const start = i;
992
- while (i < path.length && /[a-zA-Z0-9_]/.test(path[i])) {
993
- i += 1;
994
- }
995
- if (i === start) throw new Error(`Empty property name at position ${i} in: ${path}`);
996
- segments.push({ type: "property", name: path.slice(start, i) });
997
- } else if (path[i] === "[") {
998
- if (i + 1 >= path.length) throw new Error(`Unexpected end of path after '[': ${path}`);
999
- if (path[i + 1] === "?") {
1000
- const closingIdx = findFilterClose(path, i + 2);
1001
- if (closingIdx === -1) throw new Error(`Unterminated filter expression in: ${path}`);
1002
- const inner = path.slice(i + 3, closingIdx);
1003
- segments.push(parseFilter(inner));
1004
- i = closingIdx + 2;
1005
- } else if (path[i + 1] === "'") {
1006
- const [key, endIdx] = extractQuotedString(path, i + 2);
1007
- if (path[endIdx + 1] !== "]") throw new Error(`Expected ']' after bracket property in: ${path}`);
1008
- segments.push({ type: "property", name: key });
1009
- i = endIdx + 2;
1010
- } else if (/\d/.test(path[i + 1])) {
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}]`);
1016
- }
1017
- segments.push({ type: "index", index: Number(indexStr) });
1018
- i = end + 1;
1019
- } else {
1020
- throw new Error(`Unexpected character after '[': '${path[i + 1]}' in: ${path}`);
1021
- }
1022
- } else {
1023
- throw new Error(`Unexpected character '${path[i]}' at position ${i} in: ${path}`);
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;
1148
+ )) {
1149
+ if (child == null) continue;
1150
+ obj[key] = comparisonToDict(child);
1059
1151
  }
1060
- case "valueFilter":
1061
- result += `[?(@==${formatFilterLiteral(seg.value)})]`;
1062
- break;
1152
+ result.value = obj;
1063
1153
  }
1064
- }
1065
- return result;
1066
- }
1067
- function canonicalizeFilterForAtom(inner) {
1068
- if (inner.startsWith("@.")) {
1069
- const eqIdx = inner.indexOf("==");
1070
- if (eqIdx === -1) return `[?(${inner})]`;
1071
- const key = inner.slice(2, eqIdx);
1072
- const valuePart = inner.slice(eqIdx);
1073
- if (NESTED_PATH_RE2.test(key)) {
1074
- return `[?(@.${key}${valuePart})]`;
1154
+ } else {
1155
+ if (node.type === "UNCHANGED" /* UNCHANGED */ || node.type === "ADD" /* ADD */ || node.type === "UPDATE" /* UPDATE */) {
1156
+ result.value = node.value;
1075
1157
  }
1076
- return `[?(@['${key.replace(/'/g, "''")}']${valuePart})]`;
1077
- }
1078
- return `[?(${inner})]`;
1079
- }
1080
- function canonicalizeFilterForV4(inner) {
1081
- if (inner.startsWith("@['")) {
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})]`;
1158
+ if (node.type === "REMOVE" /* REMOVE */ || node.type === "UPDATE" /* UPDATE */) {
1159
+ result.oldValue = node.oldValue;
1088
1160
  }
1089
- return `[?(${inner})]`;
1090
- }
1091
- return normalizeFilterToStringLiterals(`[?(${inner})]`);
1092
- }
1093
- function normalizeToStringQuoted(literal) {
1094
- if (literal.startsWith("'") && literal.endsWith("'")) {
1095
- return literal;
1096
- }
1097
- const value = parseFilterLiteral(literal);
1098
- const stringValue = String(value).replace(/'/g, "''");
1099
- return `'${stringValue}'`;
1100
- }
1101
- function atomicPathToAtomPath(atomicPath) {
1102
- if (atomicPath === "$.$root") return "$";
1103
- if (atomicPath.startsWith("$.$root.")) return "$" + atomicPath.slice(7);
1104
- if (!atomicPath.startsWith("$")) {
1105
- throw new Error(`Atomic path must start with '$': ${atomicPath}`);
1106
1161
  }
1107
- let result = "$";
1108
- let i = 1;
1109
- while (i < atomicPath.length) {
1110
- if (atomicPath[i] === ".") {
1111
- i += 1;
1112
- const start = i;
1113
- while (i < atomicPath.length && atomicPath[i] !== "." && atomicPath[i] !== "[") {
1114
- i += 1;
1162
+ return result;
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);
1115
1177
  }
1116
- const name = atomicPath.slice(start, i);
1117
- result += formatMemberAccess(name);
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;
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);
1136
1185
  }
1137
- } else {
1138
- throw new Error(`Unexpected character '${atomicPath[i]}' in atomic path: ${atomicPath}`);
1139
1186
  }
1187
+ return;
1140
1188
  }
1141
- return result;
1142
- }
1143
- function atomPathToAtomicPath(atomPath) {
1144
- if (!atomPath.startsWith("$")) {
1145
- throw new Error(`Atom path must start with '$': ${atomPath}`);
1189
+ if (node.type === "UNCHANGED" /* UNCHANGED */ && !includeUnchanged) {
1190
+ return;
1146
1191
  }
1147
- if (atomPath === "$") {
1148
- return "$.$root";
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;
1149
1195
  }
1150
- let result = "$";
1151
- let i = 1;
1152
- while (i < atomPath.length) {
1153
- if (atomPath[i] === ".") {
1154
- i += 1;
1155
- const start = i;
1156
- while (i < atomPath.length && /[a-zA-Z0-9_]/.test(atomPath[i])) {
1157
- i += 1;
1158
- }
1159
- result += "." + atomPath.slice(start, i);
1160
- } else if (atomPath[i] === "[") {
1161
- if (atomPath[i + 1] === "?") {
1162
- const closingIdx = findFilterClose(atomPath, i + 2);
1163
- if (closingIdx === -1) throw new Error(`Unterminated filter in: ${atomPath}`);
1164
- const inner = atomPath.slice(i + 3, closingIdx);
1165
- result += canonicalizeFilterForV4(inner);
1166
- i = closingIdx + 2;
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}`);
1179
- }
1180
- } else {
1181
- throw new Error(`Unexpected character '${atomPath[i]}' in atom path: ${atomPath}`);
1182
- }
1196
+ if (node.type === "REMOVE" /* REMOVE */ || node.type === "UPDATE" /* UPDATE */) {
1197
+ entry.oldValue = node.oldValue;
1183
1198
  }
1184
- return result;
1199
+ results.push(entry);
1185
1200
  }
1186
- function normalizeFilterToStringLiterals(filter) {
1187
- const eqIdx = filter.indexOf("==");
1188
- if (eqIdx === -1) return filter;
1189
- const literalStart = eqIdx + 2;
1190
- const literalEnd = filter.length - 2;
1191
- const literal = filter.slice(literalStart, literalEnd);
1192
- if (literal.startsWith("'") && literal.endsWith("'")) {
1193
- return filter;
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);
1194
1290
  }
1195
- const value = parseFilterLiteral(literal);
1196
- const stringValue = String(value).replace(/'/g, "''");
1197
- return filter.slice(0, literalStart) + `'${stringValue}'` + filter.slice(literalEnd);
1291
+ return obj;
1198
1292
  }
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);
1208
- }
1209
- const eqIdx = inner.indexOf("==");
1210
- if (eqIdx !== -1) {
1211
- const val = parseFilterLiteral(inner.slice(eqIdx + 2));
1212
- return String(val);
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);
1213
1304
  }
1305
+ result = applyValue(result, add);
1306
+ } else {
1307
+ result = applyValue(result, op);
1214
1308
  }
1215
1309
  }
1216
- if (atomicPath.endsWith("]")) {
1217
- const bracketStart = atomicPath.lastIndexOf("[");
1218
- if (bracketStart !== -1) {
1219
- const inner = atomicPath.slice(bracketStart + 1, atomicPath.length - 1);
1220
- if (/^\d+$/.test(inner)) return inner;
1221
- return inner;
1222
- }
1223
- }
1224
- const lastDot = atomicPath.lastIndexOf(".");
1225
- if (lastDot > 0) {
1226
- return atomicPath.slice(lastDot + 1);
1227
- }
1228
- return atomicPath;
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 (typeof d.version !== "number") {
1242
- errors.push(`Missing or invalid version: expected number, got '${typeof d.version}'`);
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
- if (op.from === op.path) {
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 (op.from !== "$" && (op.path.startsWith(op.from + ".") || op.path.startsWith(op.from + "["))) {
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
- return {
1313
- format: "json-atom",
1314
- version: 1,
1315
- operations
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,47 +1446,51 @@ 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 = mc.key === "$root" ? "$" : appendCanonicalProperty(basePath, mc.key);
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 = change.key === "$root" ? "$" : appendCanonicalProperty(basePath, change.key);
1357
- const childOld = change.key === "$root" ? oldCtx : oldCtx?.[change.key];
1358
- const childNew = change.key === "$root" ? newCtx : newCtx?.[change.key];
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
- const orderedChildChanges = orderArrayChildChanges(change.changes, change.embeddedKey);
1361
- for (const childChange of orderedChildChanges) {
1362
- const filterPath = buildCanonicalFilterPath(
1363
- childPath,
1364
- change.embeddedKey,
1365
- childChange.key,
1366
- childOld,
1367
- childNew,
1368
- childChange,
1369
- change.embeddedKeyIsPath
1370
- );
1371
- if (childChange.changes) {
1372
- const oldEl = findElement(childOld, change.embeddedKey, childChange.key);
1373
- const newEl = findElement(childNew, change.embeddedKey, childChange.key);
1374
- walkChanges(childChange.changes, filterPath, oldEl, newEl, ops, options);
1375
- } else {
1376
- emitLeafOp(childChange, filterPath, ops, options);
1377
- }
1378
- }
1463
+ walkArrayChanges(change, childPath, childOld, childNew, ops, options);
1379
1464
  } else {
1380
1465
  walkChanges(change.changes, childPath, childOld, childNew, ops, options);
1381
1466
  }
1382
1467
  } else {
1383
- const path = change.key === "$root" ? "$" : appendCanonicalProperty(basePath, change.key);
1468
+ const path = isRoot ? "$" : appendCanonicalProperty(basePath, change.key);
1384
1469
  emitLeafOp(change, path, ops, options);
1385
1470
  }
1386
1471
  }
1387
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
+ }
1388
1494
  function orderArrayChildChanges(changes, embeddedKey) {
1389
1495
  if (embeddedKey !== "$index") {
1390
1496
  return changes;
@@ -1499,7 +1605,7 @@ function findActualValue(oldArr, newArr, stringKey, opType) {
1499
1605
  }
1500
1606
  return stringKey;
1501
1607
  }
1502
- function findElement(arr, embeddedKey, changeKey) {
1608
+ function findElement(arr, embeddedKey, changeKey, isPath = false) {
1503
1609
  if (!arr || !Array.isArray(arr)) return void 0;
1504
1610
  if (embeddedKey === "$index") {
1505
1611
  return arr[Number(changeKey)];
@@ -1510,7 +1616,7 @@ function findElement(arr, embeddedKey, changeKey) {
1510
1616
  if (typeof embeddedKey === "function") {
1511
1617
  return arr.find((item) => String(embeddedKey(item)) === changeKey);
1512
1618
  }
1513
- return arr.find((item) => item && String(item[embeddedKey]) === changeKey);
1619
+ return arr.find((item) => item && String(resolveNestedKey(item, embeddedKey, isPath)) === changeKey);
1514
1620
  }
1515
1621
  function resolveNestedKey(item, key, isPath) {
1516
1622
  if (isPath) {
@@ -1575,9 +1681,30 @@ function toAtom(changeset, options = {}) {
1575
1681
  throw new Error(`Unknown operation type: ${atom.type}`);
1576
1682
  }
1577
1683
  });
1578
- const operations = mergeConsecutiveOps(rawOps);
1684
+ const operations = orderLegacyIndexRemoves(mergeConsecutiveOps(rawOps));
1579
1685
  return { format: "json-atom", version: 1, operations };
1580
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
+ }
1581
1708
  function mergeConsecutiveOps(ops) {
1582
1709
  const result = [];
1583
1710
  let i = 0;
@@ -1670,8 +1797,13 @@ function invertAtom(atom) {
1670
1797
  return { op: "remove", path: op.path, oldValue: op.value, ...extensions };
1671
1798
  case "remove":
1672
1799
  return { op: "add", path: op.path, value: op.oldValue, ...extensions };
1673
- case "replace":
1674
- return { op: "replace", path: op.path, value: op.oldValue, oldValue: op.value, ...extensions };
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
+ }
1675
1807
  case "move":
1676
1808
  return { op: "move", from: op.path, path: op.from, ...extensions };
1677
1809
  case "copy":
@@ -1689,134 +1821,10 @@ function invertAtom(atom) {
1689
1821
  }
1690
1822
  return envelope;
1691
1823
  }
1692
- var NESTED_PATH_RE3 = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
1693
- function resolveValueAtPath(obj, atomPath) {
1694
- const segments = parseAtomPath(atomPath);
1695
- let current = obj;
1696
- for (const seg of segments) {
1697
- switch (seg.type) {
1698
- case "root":
1699
- break;
1700
- case "property":
1701
- if (current == null || typeof current !== "object") {
1702
- throw new Error(`Cannot access property '${seg.name}' on ${current === null ? "null" : typeof current} at path: ${atomPath}`);
1703
- }
1704
- current = current[seg.name];
1705
- break;
1706
- case "index":
1707
- if (!Array.isArray(current)) {
1708
- throw new Error(`Cannot access index ${seg.index} on non-array at path: ${atomPath}`);
1709
- }
1710
- current = current[seg.index];
1711
- break;
1712
- case "keyFilter": {
1713
- if (!Array.isArray(current)) {
1714
- throw new Error(`Cannot apply key filter on non-array at path: ${atomPath}`);
1715
- }
1716
- const prop = seg.property;
1717
- const isPath = !seg.literalKey && prop.includes(".") && NESTED_PATH_RE3.test(prop);
1718
- current = current.find((el) => {
1719
- const resolved = isPath ? prop.split(".").reduce((c, s) => c?.[s], el) : el[prop];
1720
- return JSON.stringify(resolved) === JSON.stringify(seg.value);
1721
- });
1722
- break;
1723
- }
1724
- case "valueFilter": {
1725
- if (!Array.isArray(current)) {
1726
- throw new Error(`Cannot apply value filter on non-array at path: ${atomPath}`);
1727
- }
1728
- current = current.find((el) => JSON.stringify(el) === JSON.stringify(seg.value));
1729
- break;
1730
- }
1731
- }
1732
- }
1733
- return current;
1734
- }
1735
1824
  function applyAtom(obj, atom) {
1736
1825
  const validation = validateAtom(atom);
1737
- if (!validation.valid) {
1738
- throw new Error(`Invalid atom: ${validation.errors.join(", ")}`);
1739
- }
1740
- let result = obj;
1741
- for (const op of atom.operations) {
1742
- if (op.op === "move") {
1743
- const value = resolveValueAtPath(result, op.from);
1744
- const removeOp = { op: "remove", path: op.from, oldValue: value };
1745
- if (removeOp.path === "$") {
1746
- result = applyRootOp(result, removeOp);
1747
- } else {
1748
- const removeChange = atomOpToAtomicChange(removeOp);
1749
- applyChangeset(result, unatomizeChangeset([removeChange]));
1750
- }
1751
- const addOp = { op: "add", path: op.path, value };
1752
- if (addOp.path === "$") {
1753
- result = applyRootOp(result, addOp);
1754
- } else {
1755
- const addChange = atomOpToAtomicChange(addOp);
1756
- applyChangeset(result, unatomizeChangeset([addChange]));
1757
- }
1758
- } else if (op.op === "copy") {
1759
- const source = resolveValueAtPath(result, op.from);
1760
- const value = source === void 0 ? void 0 : JSON.parse(JSON.stringify(source));
1761
- const addOp = { op: "add", path: op.path, value };
1762
- if (addOp.path === "$") {
1763
- result = applyRootOp(result, addOp);
1764
- } else {
1765
- const addChange = atomOpToAtomicChange(addOp);
1766
- applyChangeset(result, unatomizeChangeset([addChange]));
1767
- }
1768
- } else if (op.path === "$") {
1769
- result = applyRootOp(result, op);
1770
- } else {
1771
- const atomicChange = atomOpToAtomicChange(op);
1772
- const miniChangeset = unatomizeChangeset([atomicChange]);
1773
- applyChangeset(result, miniChangeset);
1774
- }
1775
- }
1776
- return result;
1777
- }
1778
- function applyRootOp(obj, op) {
1779
- switch (op.op) {
1780
- case "add":
1781
- return op.value;
1782
- case "remove":
1783
- return null;
1784
- case "replace": {
1785
- if (typeof obj === "object" && obj !== null && !Array.isArray(obj) && typeof op.value === "object" && op.value !== null && !Array.isArray(op.value)) {
1786
- for (const key of Object.keys(obj)) {
1787
- delete obj[key];
1788
- }
1789
- Object.assign(obj, op.value);
1790
- return obj;
1791
- }
1792
- return op.value;
1793
- }
1794
- /* istanbul ignore next -- exhaustive switch */
1795
- default:
1796
- throw new Error(`Unknown operation: ${op.op}`);
1797
- }
1798
- }
1799
- function atomOpToAtomicChange(op) {
1800
- const atomicPath = atomPathToAtomicPath(op.path);
1801
- const key = extractKeyFromAtomicPath(atomicPath);
1802
- switch (op.op) {
1803
- case "add":
1804
- return { type: "ADD" /* ADD */, key, path: atomicPath, valueType: getValueType(op.value), value: op.value };
1805
- case "remove":
1806
- return { type: "REMOVE" /* REMOVE */, key, path: atomicPath, valueType: getValueType(op.oldValue), value: op.oldValue };
1807
- case "replace":
1808
- return {
1809
- type: "UPDATE" /* UPDATE */,
1810
- key,
1811
- path: atomicPath,
1812
- valueType: getValueType(op.value),
1813
- value: op.value,
1814
- oldValue: op.oldValue
1815
- };
1816
- /* istanbul ignore next -- exhaustive switch */
1817
- default:
1818
- throw new Error(`Unknown operation: ${op.op}`);
1819
- }
1826
+ if (!validation.valid) throw new Error(`Invalid atom: ${validation.errors.join(", ")}`);
1827
+ return applyAtomOperations(obj, atom);
1820
1828
  }
1821
1829
  function revertAtom(obj, atom) {
1822
1830
  const inverse = invertAtom(atom);
@@ -1824,10 +1832,11 @@ function revertAtom(obj, atom) {
1824
1832
  }
1825
1833
 
1826
1834
  // src/atomHelpers.ts
1827
- var OP_SPEC_KEYS = /* @__PURE__ */ new Set(["op", "path", "value", "oldValue"]);
1835
+ var OP_SPEC_KEYS = /* @__PURE__ */ new Set(["op", "path", "from", "value", "oldValue"]);
1828
1836
  var ATOM_SPEC_KEYS = /* @__PURE__ */ new Set(["format", "version", "operations"]);
1829
1837
  function operationSpecDict(op) {
1830
1838
  const result = { op: op.op, path: op.path };
1839
+ if ("from" in op) result.from = op.from;
1831
1840
  if ("value" in op) result.value = op.value;
1832
1841
  if ("oldValue" in op) result.oldValue = op.oldValue;
1833
1842
  return result;