json-diff-ts 5.0.0-alpha.6 → 5.0.0-alpha.7

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
@@ -1247,7 +1247,7 @@ function validateAtom(atom) {
1247
1247
  errors.push(`operations[${i}]: must be an object`);
1248
1248
  continue;
1249
1249
  }
1250
- if (!["add", "remove", "replace"].includes(op.op)) {
1250
+ if (!["add", "remove", "replace", "move", "copy"].includes(op.op)) {
1251
1251
  errors.push(`operations[${i}]: invalid op '${op.op}'`);
1252
1252
  }
1253
1253
  if (typeof op.path !== "string") {
@@ -1267,6 +1267,33 @@ function validateAtom(atom) {
1267
1267
  if (op.op === "replace" && !("value" in op)) {
1268
1268
  errors.push(`operations[${i}]: replace operation must have value`);
1269
1269
  }
1270
+ if (op.op === "move") {
1271
+ if (typeof op.from !== "string") {
1272
+ errors.push(`operations[${i}]: move operation must have from (string)`);
1273
+ }
1274
+ if ("value" in op) {
1275
+ errors.push(`operations[${i}]: move operation must not have value`);
1276
+ }
1277
+ if ("oldValue" in op) {
1278
+ errors.push(`operations[${i}]: move operation must not have oldValue`);
1279
+ }
1280
+ if (typeof op.from === "string" && typeof op.path === "string") {
1281
+ if (op.from === op.path) {
1282
+ errors.push(`operations[${i}]: move operation from must not equal path (self-move)`);
1283
+ }
1284
+ if (op.from !== "$" && (op.path.startsWith(op.from + ".") || op.path.startsWith(op.from + "["))) {
1285
+ errors.push(`operations[${i}]: move operation path must not be a subtree of from`);
1286
+ }
1287
+ }
1288
+ }
1289
+ if (op.op === "copy") {
1290
+ if (typeof op.from !== "string") {
1291
+ errors.push(`operations[${i}]: copy operation must have from (string)`);
1292
+ }
1293
+ if ("oldValue" in op) {
1294
+ errors.push(`operations[${i}]: copy operation must not have oldValue`);
1295
+ }
1296
+ }
1270
1297
  }
1271
1298
  }
1272
1299
  return { valid: errors.length === 0, errors };
@@ -1525,6 +1552,9 @@ function fromAtom(atom) {
1525
1552
  throw new Error(`Invalid atom: ${validation.errors.join(", ")}`);
1526
1553
  }
1527
1554
  return atom.operations.map((op) => {
1555
+ if (op.op === "move" || op.op === "copy") {
1556
+ throw new Error(`${op.op} operations cannot be converted to v4 atomic changes`);
1557
+ }
1528
1558
  const atomicPath = atomPathToAtomicPath(op.path);
1529
1559
  const key = extractKeyFromAtomicPath(atomicPath);
1530
1560
  switch (op.op) {
@@ -1570,11 +1600,14 @@ function invertAtom(atom) {
1570
1600
  if (op.op === "remove" && !("oldValue" in op)) {
1571
1601
  throw new Error(`operations[${i}]: remove operation missing oldValue \u2014 atom is not reversible`);
1572
1602
  }
1603
+ if (op.op === "copy" && !("value" in op)) {
1604
+ throw new Error(`operations[${i}]: copy operation missing value \u2014 atom is not reversible`);
1605
+ }
1573
1606
  }
1574
1607
  const invertedOps = [...atom.operations].reverse().map((op) => {
1575
1608
  const extensions = {};
1576
1609
  for (const key of Object.keys(op)) {
1577
- if (!["op", "path", "value", "oldValue"].includes(key)) {
1610
+ if (!["op", "path", "from", "value", "oldValue"].includes(key)) {
1578
1611
  extensions[key] = op[key];
1579
1612
  }
1580
1613
  }
@@ -1585,6 +1618,10 @@ function invertAtom(atom) {
1585
1618
  return { op: "add", path: op.path, value: op.oldValue, ...extensions };
1586
1619
  case "replace":
1587
1620
  return { op: "replace", path: op.path, value: op.oldValue, oldValue: op.value, ...extensions };
1621
+ case "move":
1622
+ return { op: "move", from: op.path, path: op.from, ...extensions };
1623
+ case "copy":
1624
+ return { op: "remove", path: op.path, oldValue: op.value, ...extensions };
1588
1625
  /* istanbul ignore next -- exhaustive switch */
1589
1626
  default:
1590
1627
  throw new Error(`Unknown operation: ${op.op}`);
@@ -1598,6 +1635,49 @@ function invertAtom(atom) {
1598
1635
  }
1599
1636
  return envelope;
1600
1637
  }
1638
+ var NESTED_PATH_RE3 = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
1639
+ function resolveValueAtPath(obj, atomPath) {
1640
+ const segments = parseAtomPath(atomPath);
1641
+ let current = obj;
1642
+ for (const seg of segments) {
1643
+ switch (seg.type) {
1644
+ case "root":
1645
+ break;
1646
+ case "property":
1647
+ if (current == null || typeof current !== "object") {
1648
+ throw new Error(`Cannot access property '${seg.name}' on ${current === null ? "null" : typeof current} at path: ${atomPath}`);
1649
+ }
1650
+ current = current[seg.name];
1651
+ break;
1652
+ case "index":
1653
+ if (!Array.isArray(current)) {
1654
+ throw new Error(`Cannot access index ${seg.index} on non-array at path: ${atomPath}`);
1655
+ }
1656
+ current = current[seg.index];
1657
+ break;
1658
+ case "keyFilter": {
1659
+ if (!Array.isArray(current)) {
1660
+ throw new Error(`Cannot apply key filter on non-array at path: ${atomPath}`);
1661
+ }
1662
+ const prop = seg.property;
1663
+ const isPath = !seg.literalKey && prop.includes(".") && NESTED_PATH_RE3.test(prop);
1664
+ current = current.find((el) => {
1665
+ const resolved = isPath ? prop.split(".").reduce((c, s) => c?.[s], el) : el[prop];
1666
+ return JSON.stringify(resolved) === JSON.stringify(seg.value);
1667
+ });
1668
+ break;
1669
+ }
1670
+ case "valueFilter": {
1671
+ if (!Array.isArray(current)) {
1672
+ throw new Error(`Cannot apply value filter on non-array at path: ${atomPath}`);
1673
+ }
1674
+ current = current.find((el) => JSON.stringify(el) === JSON.stringify(seg.value));
1675
+ break;
1676
+ }
1677
+ }
1678
+ }
1679
+ return current;
1680
+ }
1601
1681
  function applyAtom(obj, atom) {
1602
1682
  const validation = validateAtom(atom);
1603
1683
  if (!validation.valid) {
@@ -1605,7 +1685,33 @@ function applyAtom(obj, atom) {
1605
1685
  }
1606
1686
  let result = obj;
1607
1687
  for (const op of atom.operations) {
1608
- if (op.path === "$") {
1688
+ if (op.op === "move") {
1689
+ const value = resolveValueAtPath(result, op.from);
1690
+ const removeOp = { op: "remove", path: op.from, oldValue: value };
1691
+ if (removeOp.path === "$") {
1692
+ result = applyRootOp(result, removeOp);
1693
+ } else {
1694
+ const removeChange = atomOpToAtomicChange(removeOp);
1695
+ applyChangeset(result, unatomizeChangeset([removeChange]));
1696
+ }
1697
+ const addOp = { op: "add", path: op.path, value };
1698
+ if (addOp.path === "$") {
1699
+ result = applyRootOp(result, addOp);
1700
+ } else {
1701
+ const addChange = atomOpToAtomicChange(addOp);
1702
+ applyChangeset(result, unatomizeChangeset([addChange]));
1703
+ }
1704
+ } else if (op.op === "copy") {
1705
+ const source = resolveValueAtPath(result, op.from);
1706
+ const value = source === void 0 ? void 0 : JSON.parse(JSON.stringify(source));
1707
+ const addOp = { op: "add", path: op.path, value };
1708
+ if (addOp.path === "$") {
1709
+ result = applyRootOp(result, addOp);
1710
+ } else {
1711
+ const addChange = atomOpToAtomicChange(addOp);
1712
+ applyChangeset(result, unatomizeChangeset([addChange]));
1713
+ }
1714
+ } else if (op.path === "$") {
1609
1715
  result = applyRootOp(result, op);
1610
1716
  } else {
1611
1717
  const atomicChange = atomOpToAtomicChange(op);