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

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
@@ -487,12 +487,14 @@ var compareArray = (oldObj, newObj, path, keyPath, options) => {
487
487
  const isFunctionKey = typeof uniqKey === "function" && uniqKey.length === 2;
488
488
  if (diffs.length) {
489
489
  const resolvedKey = isFunctionKey ? uniqKey(newObj[0] ?? oldObj[0], true) : uniqKey;
490
+ 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));
490
492
  return [
491
493
  {
492
494
  type: "UPDATE" /* UPDATE */,
493
495
  key: getKey(path),
494
496
  embeddedKey: resolvedKey,
495
- ...isFunctionKey && typeof resolvedKey === "string" && NESTED_PATH_RE.test(resolvedKey) && resolvedKey.includes(".") ? { embeddedKeyIsPath: true } : {},
497
+ ...isNestedPath ? { embeddedKeyIsPath: true } : {},
496
498
  changes: diffs
497
499
  }
498
500
  ];
@@ -533,7 +535,8 @@ var convertArrayToObj = (arr, uniqKey) => {
533
535
  obj[i] = value;
534
536
  }
535
537
  } else {
536
- const keyFunction = typeof uniqKey === "string" ? (item) => item[uniqKey] : uniqKey;
538
+ const maybeNestedPath = typeof uniqKey === "string" && uniqKey.includes(".") && NESTED_PATH_RE.test(uniqKey);
539
+ 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;
537
540
  obj = keyBy(arr, keyFunction);
538
541
  }
539
542
  return obj;
@@ -580,7 +583,7 @@ var indexOfItemInArray = (arr, key, value, isPath) => {
580
583
  const item = arr[i];
581
584
  if (item == null) continue;
582
585
  const resolved = resolveProperty(item, key, isPath);
583
- if (resolved != null && String(resolved) === String(value)) {
586
+ if (resolved !== void 0 && String(resolved) === String(value)) {
584
587
  return i;
585
588
  }
586
589
  }
@@ -636,7 +639,7 @@ var applyArrayChange = (arr, change) => {
636
639
  } else {
637
640
  element = arr.find((el) => {
638
641
  const resolved = resolveProperty(el, change.embeddedKey, change.embeddedKeyIsPath);
639
- return resolved != null && String(resolved) === String(subchange.key);
642
+ return resolved !== void 0 && String(resolved) === String(subchange.key);
640
643
  });
641
644
  }
642
645
  if (element) {
@@ -706,7 +709,7 @@ var revertArrayChange = (arr, change) => {
706
709
  } else {
707
710
  element = arr.find((el) => {
708
711
  const resolved = resolveProperty(el, change.embeddedKey, change.embeddedKeyIsPath);
709
- return resolved != null && String(resolved) === String(subchange.key);
712
+ return resolved !== void 0 && String(resolved) === String(subchange.key);
710
713
  });
711
714
  }
712
715
  if (element) {
@@ -1247,7 +1250,7 @@ function validateAtom(atom) {
1247
1250
  errors.push(`operations[${i}]: must be an object`);
1248
1251
  continue;
1249
1252
  }
1250
- if (!["add", "remove", "replace"].includes(op.op)) {
1253
+ if (!["add", "remove", "replace", "move", "copy"].includes(op.op)) {
1251
1254
  errors.push(`operations[${i}]: invalid op '${op.op}'`);
1252
1255
  }
1253
1256
  if (typeof op.path !== "string") {
@@ -1267,6 +1270,33 @@ function validateAtom(atom) {
1267
1270
  if (op.op === "replace" && !("value" in op)) {
1268
1271
  errors.push(`operations[${i}]: replace operation must have value`);
1269
1272
  }
1273
+ if (op.op === "move") {
1274
+ if (typeof op.from !== "string") {
1275
+ errors.push(`operations[${i}]: move operation must have from (string)`);
1276
+ }
1277
+ if ("value" in op) {
1278
+ errors.push(`operations[${i}]: move operation must not have value`);
1279
+ }
1280
+ if ("oldValue" in op) {
1281
+ errors.push(`operations[${i}]: move operation must not have oldValue`);
1282
+ }
1283
+ if (typeof op.from === "string" && typeof op.path === "string") {
1284
+ if (op.from === op.path) {
1285
+ errors.push(`operations[${i}]: move operation from must not equal path (self-move)`);
1286
+ }
1287
+ if (op.from !== "$" && (op.path.startsWith(op.from + ".") || op.path.startsWith(op.from + "["))) {
1288
+ errors.push(`operations[${i}]: move operation path must not be a subtree of from`);
1289
+ }
1290
+ }
1291
+ }
1292
+ if (op.op === "copy") {
1293
+ if (typeof op.from !== "string") {
1294
+ errors.push(`operations[${i}]: copy operation must have from (string)`);
1295
+ }
1296
+ if ("oldValue" in op) {
1297
+ errors.push(`operations[${i}]: copy operation must not have oldValue`);
1298
+ }
1299
+ }
1270
1300
  }
1271
1301
  }
1272
1302
  return { valid: errors.length === 0, errors };
@@ -1334,7 +1364,8 @@ function walkChanges(changes, basePath, oldCtx, newCtx, ops, options) {
1334
1364
  childChange.key,
1335
1365
  childOld,
1336
1366
  childNew,
1337
- childChange
1367
+ childChange,
1368
+ change.embeddedKeyIsPath
1338
1369
  );
1339
1370
  if (childChange.changes) {
1340
1371
  const oldEl = findElement(childOld, change.embeddedKey, childChange.key);
@@ -1377,7 +1408,7 @@ function emitLeafOp(change, path, ops, options) {
1377
1408
  }
1378
1409
  }
1379
1410
  }
1380
- function buildCanonicalFilterPath(basePath, embeddedKey, changeKey, oldArr, newArr, change) {
1411
+ function buildCanonicalFilterPath(basePath, embeddedKey, changeKey, oldArr, newArr, change, embeddedKeyIsPath) {
1381
1412
  if (embeddedKey === "$index") {
1382
1413
  return `${basePath}[${changeKey}]`;
1383
1414
  }
@@ -1397,9 +1428,11 @@ function buildCanonicalFilterPath(basePath, embeddedKey, changeKey, oldArr, newA
1397
1428
  const memberAccess2 = typeof keyName === "string" && SIMPLE_PROPERTY_RE2.test(keyName) ? `.${keyName}` : `.${changeKey}`;
1398
1429
  return `${basePath}[?(@${memberAccess2}=='${changeKey}')]`;
1399
1430
  }
1400
- const element = findElementByKey(oldArr, newArr, embeddedKey, changeKey, change.type);
1401
- const typedVal = element ? element[embeddedKey] : changeKey;
1402
- const memberAccess = SIMPLE_PROPERTY_RE2.test(embeddedKey) ? `.${embeddedKey}` : `['${embeddedKey.replace(/'/g, "''")}']`;
1431
+ const isNestedPath = embeddedKeyIsPath && NESTED_PATH_RE3.test(embeddedKey);
1432
+ const element = findElementByKey(oldArr, newArr, embeddedKey, changeKey, change.type, isNestedPath);
1433
+ const resolved = element !== void 0 ? resolveNestedKey(element, embeddedKey, !!isNestedPath) : void 0;
1434
+ const typedVal = resolved !== void 0 ? resolved : changeKey;
1435
+ const memberAccess = SIMPLE_PROPERTY_RE2.test(embeddedKey) || isNestedPath ? `.${embeddedKey}` : `['${embeddedKey.replace(/'/g, "''")}']`;
1403
1436
  return `${basePath}[?(@${memberAccess}==${formatFilterLiteral(typedVal)})]`;
1404
1437
  }
1405
1438
  function findActualValue(oldArr, newArr, stringKey, opType) {
@@ -1438,13 +1471,20 @@ function findElement(arr, embeddedKey, changeKey) {
1438
1471
  }
1439
1472
  return arr.find((item) => item && String(item[embeddedKey]) === changeKey);
1440
1473
  }
1441
- function findElementByKey(oldArr, newArr, embeddedKey, changeKey, opType) {
1474
+ function resolveNestedKey(item, key, isPath) {
1475
+ if (isPath) {
1476
+ return key.split(".").reduce((c, s) => c?.[s], item);
1477
+ }
1478
+ return item?.[key];
1479
+ }
1480
+ function findElementByKey(oldArr, newArr, embeddedKey, changeKey, opType, isPath) {
1481
+ const match = (item) => item && String(resolveNestedKey(item, embeddedKey, !!isPath)) === changeKey;
1442
1482
  if (opType === "REMOVE" /* REMOVE */ || opType === "UPDATE" /* UPDATE */) {
1443
- const el = oldArr?.find((item) => item && String(item[embeddedKey]) === changeKey);
1483
+ const el = oldArr?.find(match);
1444
1484
  if (el) return el;
1445
1485
  }
1446
1486
  if (opType === "ADD" /* ADD */ || opType === "UPDATE" /* UPDATE */) {
1447
- const el = newArr?.find((item) => item && String(item[embeddedKey]) === changeKey);
1487
+ const el = newArr?.find(match);
1448
1488
  if (el) return el;
1449
1489
  }
1450
1490
  return void 0;
@@ -1525,6 +1565,9 @@ function fromAtom(atom) {
1525
1565
  throw new Error(`Invalid atom: ${validation.errors.join(", ")}`);
1526
1566
  }
1527
1567
  return atom.operations.map((op) => {
1568
+ if (op.op === "move" || op.op === "copy") {
1569
+ throw new Error(`${op.op} operations cannot be converted to v4 atomic changes`);
1570
+ }
1528
1571
  const atomicPath = atomPathToAtomicPath(op.path);
1529
1572
  const key = extractKeyFromAtomicPath(atomicPath);
1530
1573
  switch (op.op) {
@@ -1570,11 +1613,14 @@ function invertAtom(atom) {
1570
1613
  if (op.op === "remove" && !("oldValue" in op)) {
1571
1614
  throw new Error(`operations[${i}]: remove operation missing oldValue \u2014 atom is not reversible`);
1572
1615
  }
1616
+ if (op.op === "copy" && !("value" in op)) {
1617
+ throw new Error(`operations[${i}]: copy operation missing value \u2014 atom is not reversible`);
1618
+ }
1573
1619
  }
1574
1620
  const invertedOps = [...atom.operations].reverse().map((op) => {
1575
1621
  const extensions = {};
1576
1622
  for (const key of Object.keys(op)) {
1577
- if (!["op", "path", "value", "oldValue"].includes(key)) {
1623
+ if (!["op", "path", "from", "value", "oldValue"].includes(key)) {
1578
1624
  extensions[key] = op[key];
1579
1625
  }
1580
1626
  }
@@ -1585,6 +1631,10 @@ function invertAtom(atom) {
1585
1631
  return { op: "add", path: op.path, value: op.oldValue, ...extensions };
1586
1632
  case "replace":
1587
1633
  return { op: "replace", path: op.path, value: op.oldValue, oldValue: op.value, ...extensions };
1634
+ case "move":
1635
+ return { op: "move", from: op.path, path: op.from, ...extensions };
1636
+ case "copy":
1637
+ return { op: "remove", path: op.path, oldValue: op.value, ...extensions };
1588
1638
  /* istanbul ignore next -- exhaustive switch */
1589
1639
  default:
1590
1640
  throw new Error(`Unknown operation: ${op.op}`);
@@ -1598,6 +1648,49 @@ function invertAtom(atom) {
1598
1648
  }
1599
1649
  return envelope;
1600
1650
  }
1651
+ var NESTED_PATH_RE3 = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
1652
+ function resolveValueAtPath(obj, atomPath) {
1653
+ const segments = parseAtomPath(atomPath);
1654
+ let current = obj;
1655
+ for (const seg of segments) {
1656
+ switch (seg.type) {
1657
+ case "root":
1658
+ break;
1659
+ case "property":
1660
+ if (current == null || typeof current !== "object") {
1661
+ throw new Error(`Cannot access property '${seg.name}' on ${current === null ? "null" : typeof current} at path: ${atomPath}`);
1662
+ }
1663
+ current = current[seg.name];
1664
+ break;
1665
+ case "index":
1666
+ if (!Array.isArray(current)) {
1667
+ throw new Error(`Cannot access index ${seg.index} on non-array at path: ${atomPath}`);
1668
+ }
1669
+ current = current[seg.index];
1670
+ break;
1671
+ case "keyFilter": {
1672
+ if (!Array.isArray(current)) {
1673
+ throw new Error(`Cannot apply key filter on non-array at path: ${atomPath}`);
1674
+ }
1675
+ const prop = seg.property;
1676
+ const isPath = !seg.literalKey && prop.includes(".") && NESTED_PATH_RE3.test(prop);
1677
+ current = current.find((el) => {
1678
+ const resolved = isPath ? prop.split(".").reduce((c, s) => c?.[s], el) : el[prop];
1679
+ return JSON.stringify(resolved) === JSON.stringify(seg.value);
1680
+ });
1681
+ break;
1682
+ }
1683
+ case "valueFilter": {
1684
+ if (!Array.isArray(current)) {
1685
+ throw new Error(`Cannot apply value filter on non-array at path: ${atomPath}`);
1686
+ }
1687
+ current = current.find((el) => JSON.stringify(el) === JSON.stringify(seg.value));
1688
+ break;
1689
+ }
1690
+ }
1691
+ }
1692
+ return current;
1693
+ }
1601
1694
  function applyAtom(obj, atom) {
1602
1695
  const validation = validateAtom(atom);
1603
1696
  if (!validation.valid) {
@@ -1605,7 +1698,33 @@ function applyAtom(obj, atom) {
1605
1698
  }
1606
1699
  let result = obj;
1607
1700
  for (const op of atom.operations) {
1608
- if (op.path === "$") {
1701
+ if (op.op === "move") {
1702
+ const value = resolveValueAtPath(result, op.from);
1703
+ const removeOp = { op: "remove", path: op.from, oldValue: value };
1704
+ if (removeOp.path === "$") {
1705
+ result = applyRootOp(result, removeOp);
1706
+ } else {
1707
+ const removeChange = atomOpToAtomicChange(removeOp);
1708
+ applyChangeset(result, unatomizeChangeset([removeChange]));
1709
+ }
1710
+ const addOp = { op: "add", path: op.path, value };
1711
+ if (addOp.path === "$") {
1712
+ result = applyRootOp(result, addOp);
1713
+ } else {
1714
+ const addChange = atomOpToAtomicChange(addOp);
1715
+ applyChangeset(result, unatomizeChangeset([addChange]));
1716
+ }
1717
+ } else if (op.op === "copy") {
1718
+ const source = resolveValueAtPath(result, op.from);
1719
+ const value = source === void 0 ? void 0 : JSON.parse(JSON.stringify(source));
1720
+ const addOp = { op: "add", path: op.path, value };
1721
+ if (addOp.path === "$") {
1722
+ result = applyRootOp(result, addOp);
1723
+ } else {
1724
+ const addChange = atomOpToAtomicChange(addOp);
1725
+ applyChangeset(result, unatomizeChangeset([addChange]));
1726
+ }
1727
+ } else if (op.path === "$") {
1609
1728
  result = applyRootOp(result, op);
1610
1729
  } else {
1611
1730
  const atomicChange = atomOpToAtomicChange(op);