json-diff-ts 5.0.0-alpha.0 → 5.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -129,7 +129,15 @@ const { diffDelta, applyDelta, revertDelta } = require('json-diff-ts');
129
129
 
130
130
  ## What is JSON Delta?
131
131
 
132
- [JSON Delta](https://github.com/ltwlf/json-delta-format) is a specification for representing atomic changes to JSON documents. json-diff-ts is the originating implementation from which the spec was derived. A Python implementation is also available: [json-delta-py](https://github.com/ltwlf/json-delta-py).
132
+ [JSON Delta](https://github.com/ltwlf/json-delta-format) is a specification for representing atomic changes to JSON documents. json-diff-ts is the originating implementation from which the spec was derived.
133
+
134
+ ```text
135
+ json-delta-format (specification)
136
+ ├── json-diff-ts (TypeScript implementation) ← this package
137
+ └── json-delta-py (Python implementation)
138
+ ```
139
+
140
+ The specification defines the wire format. Each language implementation produces and consumes compatible deltas.
133
141
 
134
142
  A delta is a self-describing JSON document you can store, transmit, and consume in any language:
135
143
 
@@ -230,6 +238,15 @@ const { valid, errors } = validateDelta(maybeDelta);
230
238
  | `validateDelta` | `(delta) => { valid, errors }` | Structural validation |
231
239
  | `toDelta` | `(changeset, options?) => IJsonDelta` | Bridge: v4 changeset to JSON Delta |
232
240
  | `fromDelta` | `(delta) => IAtomicChange[]` | Bridge: JSON Delta to v4 atomic changes |
241
+ | `squashDeltas` | `(source, deltas, options?) => IJsonDelta` | Compact multiple deltas into one net-effect delta |
242
+ | `deltaMap` | `(delta, fn) => IJsonDelta` | Transform each operation in a delta |
243
+ | `deltaStamp` | `(delta, extensions) => IJsonDelta` | Set extension properties on all operations |
244
+ | `deltaGroupBy` | `(delta, keyFn) => Record<string, IJsonDelta>` | Group operations into sub-deltas |
245
+ | `operationSpecDict` | `(op) => IDeltaOperation` | Strip extension properties from operation |
246
+ | `operationExtensions` | `(op) => Record<string, any>` | Get extension properties from operation |
247
+ | `deltaSpecDict` | `(delta) => IJsonDelta` | Strip all extensions from delta |
248
+ | `deltaExtensions` | `(delta) => Record<string, any>` | Get envelope extensions from delta |
249
+ | `leafProperty` | `(op) => string \| null` | Terminal property name from operation path |
233
250
 
234
251
  ### DeltaOptions
235
252
 
@@ -243,6 +260,125 @@ interface DeltaOptions extends Options {
243
260
  }
244
261
  ```
245
262
 
263
+ ### Delta Workflow Helpers
264
+
265
+ Transform, inspect, and compact deltas for workflow automation.
266
+
267
+ #### `squashDeltas` -- Compact Multiple Deltas
268
+
269
+ Combine a sequence of deltas into a single net-effect delta. Useful for compacting audit logs or collapsing undo history:
270
+
271
+ ```typescript
272
+ import { diffDelta, applyDelta, squashDeltas } from 'json-diff-ts';
273
+
274
+ const source = { name: 'Alice', role: 'viewer' };
275
+ const d1 = diffDelta(source, { name: 'Bob', role: 'viewer' });
276
+ const d2 = diffDelta({ name: 'Bob', role: 'viewer' }, { name: 'Bob', role: 'admin' });
277
+
278
+ const squashed = squashDeltas(source, [d1, d2]);
279
+ // squashed.operations => [
280
+ // { op: 'replace', path: '$.name', value: 'Bob', oldValue: 'Alice' },
281
+ // { op: 'replace', path: '$.role', value: 'admin', oldValue: 'viewer' }
282
+ // ]
283
+
284
+ // Verify: applying the squashed delta equals applying both sequentially
285
+ const result = applyDelta(structuredClone(source), squashed);
286
+ // result => { name: 'Bob', role: 'admin' }
287
+ ```
288
+
289
+ Options: `reversible`, `arrayIdentityKeys`, `target` (pre-computed final state), `verifyTarget` (default: true).
290
+
291
+ #### `deltaMap` / `deltaStamp` / `deltaGroupBy` -- Delta Transformations
292
+
293
+ All transforms are immutable — they return new deltas without modifying the original:
294
+
295
+ ```typescript
296
+ import { diffDelta, deltaMap, deltaStamp, deltaGroupBy } from 'json-diff-ts';
297
+
298
+ const delta = diffDelta(
299
+ { name: 'Alice', age: 30, role: 'viewer' },
300
+ { name: 'Bob', age: 31, status: 'active' }
301
+ );
302
+
303
+ // Stamp metadata onto every operation
304
+ const stamped = deltaStamp(delta, { x_author: 'system', x_ts: Date.now() });
305
+
306
+ // Transform operations
307
+ const prefixed = deltaMap(delta, (op) => ({
308
+ ...op,
309
+ path: op.path.replace('$', '$.data'),
310
+ }));
311
+
312
+ // Group by operation type
313
+ const groups = deltaGroupBy(delta, (op) => op.op);
314
+ // groups => { replace: IJsonDelta, add: IJsonDelta, remove: IJsonDelta }
315
+ ```
316
+
317
+ #### `operationSpecDict` / `deltaSpecDict` -- Spec Introspection
318
+
319
+ Separate spec-defined fields from extension properties:
320
+
321
+ ```typescript
322
+ import { operationSpecDict, operationExtensions, deltaSpecDict } from 'json-diff-ts';
323
+
324
+ const op = { op: 'replace', path: '$.name', value: 'Bob', x_author: 'system' };
325
+ operationSpecDict(op); // { op: 'replace', path: '$.name', value: 'Bob' }
326
+ operationExtensions(op); // { x_author: 'system' }
327
+
328
+ // Strip all extensions from a delta
329
+ const clean = deltaSpecDict(delta);
330
+ ```
331
+
332
+ #### `leafProperty` -- Path Introspection
333
+
334
+ Extract the terminal property name from an operation's path:
335
+
336
+ ```typescript
337
+ import { leafProperty } from 'json-diff-ts';
338
+
339
+ leafProperty({ op: 'replace', path: '$.user.name' }); // 'name'
340
+ leafProperty({ op: 'add', path: '$.items[?(@.id==1)]' }); // null (filter)
341
+ leafProperty({ op: 'replace', path: '$' }); // null (root)
342
+ ```
343
+
344
+ ---
345
+
346
+ ## Comparison Serialization
347
+
348
+ Serialize enriched comparison trees to plain objects or flat change lists.
349
+
350
+ ```typescript
351
+ import { compare, comparisonToDict, comparisonToFlatList } from 'json-diff-ts';
352
+
353
+ const result = compare(
354
+ { name: 'Alice', age: 30, role: 'viewer' },
355
+ { name: 'Bob', age: 30, status: 'active' }
356
+ );
357
+
358
+ // Recursive plain object
359
+ const dict = comparisonToDict(result);
360
+ // {
361
+ // type: 'CONTAINER',
362
+ // value: {
363
+ // name: { type: 'UPDATE', value: 'Bob', oldValue: 'Alice' },
364
+ // age: { type: 'UNCHANGED', value: 30 },
365
+ // role: { type: 'REMOVE', oldValue: 'viewer' },
366
+ // status: { type: 'ADD', value: 'active' }
367
+ // }
368
+ // }
369
+
370
+ // Flat list of leaf changes with paths
371
+ const flat = comparisonToFlatList(result);
372
+ // [
373
+ // { path: '$.name', type: 'UPDATE', value: 'Bob', oldValue: 'Alice' },
374
+ // { path: '$.role', type: 'REMOVE', oldValue: 'viewer' },
375
+ // { path: '$.status', type: 'ADD', value: 'active' }
376
+ // ]
377
+
378
+ // Include unchanged entries
379
+ const all = comparisonToFlatList(result, { includeUnchanged: true });
380
+ ```
381
+
246
382
  ---
247
383
 
248
384
  ## Practical Examples
@@ -503,6 +639,8 @@ diff(old, new, { treatTypeChangeAsReplace: false });
503
639
  | --- | --- |
504
640
  | `compare(oldObj, newObj)` | Create enriched comparison object |
505
641
  | `enrich(obj)` | Create enriched representation |
642
+ | `comparisonToDict(node)` | Serialize comparison tree to plain object |
643
+ | `comparisonToFlatList(node, options?)` | Flatten comparison to leaf change list |
506
644
 
507
645
  ### Options
508
646
 
@@ -561,6 +699,11 @@ Use `$value` as the identity key: `{ arrayIdentityKeys: { tags: '$value' } }`. E
561
699
 
562
700
  ## Release Notes
563
701
 
702
+ - **v5.0.0-alpha.2:**
703
+ - Delta workflow helpers: `squashDeltas`, `deltaMap`, `deltaStamp`, `deltaGroupBy`
704
+ - Delta/operation introspection: `operationSpecDict`, `operationExtensions`, `deltaSpecDict`, `deltaExtensions`, `leafProperty`
705
+ - Comparison serialization: `comparisonToDict`, `comparisonToFlatList`
706
+
564
707
  - **v5.0.0-alpha.0:**
565
708
  - JSON Delta API: `diffDelta`, `applyDelta`, `revertDelta`, `invertDelta`, `toDelta`, `fromDelta`, `validateDelta`
566
709
  - Canonical path production with typed filter literals
package/dist/index.cjs CHANGED
@@ -28,8 +28,15 @@ __export(index_exports, {
28
28
  atomizeChangeset: () => atomizeChangeset,
29
29
  buildDeltaPath: () => buildDeltaPath,
30
30
  compare: () => compare2,
31
+ comparisonToDict: () => comparisonToDict,
32
+ comparisonToFlatList: () => comparisonToFlatList,
31
33
  createContainer: () => createContainer,
32
34
  createValue: () => createValue,
35
+ deltaExtensions: () => deltaExtensions,
36
+ deltaGroupBy: () => deltaGroupBy,
37
+ deltaMap: () => deltaMap,
38
+ deltaSpecDict: () => deltaSpecDict,
39
+ deltaStamp: () => deltaStamp,
33
40
  diff: () => diff,
34
41
  diffDelta: () => diffDelta,
35
42
  enrich: () => enrich,
@@ -37,10 +44,14 @@ __export(index_exports, {
37
44
  fromDelta: () => fromDelta,
38
45
  getTypeOfObj: () => getTypeOfObj,
39
46
  invertDelta: () => invertDelta,
47
+ leafProperty: () => leafProperty,
48
+ operationExtensions: () => operationExtensions,
49
+ operationSpecDict: () => operationSpecDict,
40
50
  parseDeltaPath: () => parseDeltaPath,
41
51
  parseFilterLiteral: () => parseFilterLiteral,
42
52
  revertChangeset: () => revertChangeset,
43
53
  revertDelta: () => revertDelta,
54
+ squashDeltas: () => squashDeltas,
44
55
  toDelta: () => toDelta,
45
56
  unatomizeChangeset: () => unatomizeChangeset,
46
57
  validateDelta: () => validateDelta
@@ -783,6 +794,74 @@ var compare2 = (oldObject, newObject) => {
783
794
  }
784
795
  return applyChangelist(enrich(oldObject), atomizeChangeset(diff(oldObject, newObject)));
785
796
  };
797
+ var comparisonToDict = (node) => {
798
+ const result = { type: node.type };
799
+ if (node.type === "CONTAINER" /* CONTAINER */) {
800
+ if (Array.isArray(node.value)) {
801
+ const children = node.value;
802
+ const serialized = new Array(children.length);
803
+ for (let i = 0; i < children.length; i++) {
804
+ const child = children[i];
805
+ serialized[i] = child != null ? comparisonToDict(child) : null;
806
+ }
807
+ result.value = serialized;
808
+ } else if (node.value && typeof node.value === "object") {
809
+ const obj = /* @__PURE__ */ Object.create(null);
810
+ for (const [key, child] of Object.entries(
811
+ node.value
812
+ )) {
813
+ if (child == null) continue;
814
+ obj[key] = comparisonToDict(child);
815
+ }
816
+ result.value = obj;
817
+ }
818
+ } else {
819
+ if (node.type === "UNCHANGED" /* UNCHANGED */ || node.type === "ADD" /* ADD */ || node.type === "UPDATE" /* UPDATE */) {
820
+ result.value = node.value;
821
+ }
822
+ if (node.type === "REMOVE" /* REMOVE */ || node.type === "UPDATE" /* UPDATE */) {
823
+ result.oldValue = node.oldValue;
824
+ }
825
+ }
826
+ return result;
827
+ };
828
+ var IDENT_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
829
+ var comparisonToFlatList = (node, options = {}) => {
830
+ const results = [];
831
+ flattenNode(node, "$", options.includeUnchanged ?? false, results);
832
+ return results;
833
+ };
834
+ function flattenNode(node, path, includeUnchanged, results) {
835
+ if (node.type === "CONTAINER" /* CONTAINER */) {
836
+ if (Array.isArray(node.value)) {
837
+ for (let i = 0; i < node.value.length; i++) {
838
+ const child = node.value[i];
839
+ if (child == null) continue;
840
+ flattenNode(child, `${path}[${i}]`, includeUnchanged, results);
841
+ }
842
+ } else if (node.value && typeof node.value === "object") {
843
+ for (const [key, child] of Object.entries(
844
+ node.value
845
+ )) {
846
+ if (child == null) continue;
847
+ const childPath = IDENT_RE.test(key) ? `${path}.${key}` : `${path}['${key.replace(/'/g, "''")}']`;
848
+ flattenNode(child, childPath, includeUnchanged, results);
849
+ }
850
+ }
851
+ return;
852
+ }
853
+ if (node.type === "UNCHANGED" /* UNCHANGED */ && !includeUnchanged) {
854
+ return;
855
+ }
856
+ const entry = { path, type: node.type };
857
+ if (node.type === "UNCHANGED" /* UNCHANGED */ || node.type === "ADD" /* ADD */ || node.type === "UPDATE" /* UPDATE */) {
858
+ entry.value = node.value;
859
+ }
860
+ if (node.type === "REMOVE" /* REMOVE */ || node.type === "UPDATE" /* UPDATE */) {
861
+ entry.oldValue = node.oldValue;
862
+ }
863
+ results.push(entry);
864
+ }
786
865
 
787
866
  // src/deltaPath.ts
788
867
  function formatFilterLiteral(value) {
@@ -1511,6 +1590,113 @@ function revertDelta(obj, delta) {
1511
1590
  const inverse = invertDelta(delta);
1512
1591
  return applyDelta(obj, inverse);
1513
1592
  }
1593
+
1594
+ // src/deltaHelpers.ts
1595
+ var OP_SPEC_KEYS = /* @__PURE__ */ new Set(["op", "path", "value", "oldValue"]);
1596
+ var DELTA_SPEC_KEYS = /* @__PURE__ */ new Set(["format", "version", "operations"]);
1597
+ function operationSpecDict(op) {
1598
+ const result = { op: op.op, path: op.path };
1599
+ if ("value" in op) result.value = op.value;
1600
+ if ("oldValue" in op) result.oldValue = op.oldValue;
1601
+ return result;
1602
+ }
1603
+ function operationExtensions(op) {
1604
+ const result = /* @__PURE__ */ Object.create(null);
1605
+ for (const key of Object.keys(op)) {
1606
+ if (!OP_SPEC_KEYS.has(key)) {
1607
+ result[key] = op[key];
1608
+ }
1609
+ }
1610
+ return result;
1611
+ }
1612
+ function leafProperty(op) {
1613
+ const segments = parseDeltaPath(op.path);
1614
+ if (segments.length === 0) return null;
1615
+ const last = segments[segments.length - 1];
1616
+ return last.type === "property" ? last.name : null;
1617
+ }
1618
+ function deltaSpecDict(delta) {
1619
+ return {
1620
+ format: delta.format,
1621
+ version: delta.version,
1622
+ operations: delta.operations.map(operationSpecDict)
1623
+ };
1624
+ }
1625
+ function deltaExtensions(delta) {
1626
+ const result = /* @__PURE__ */ Object.create(null);
1627
+ for (const key of Object.keys(delta)) {
1628
+ if (!DELTA_SPEC_KEYS.has(key)) {
1629
+ result[key] = delta[key];
1630
+ }
1631
+ }
1632
+ return result;
1633
+ }
1634
+ function deltaMap(delta, fn) {
1635
+ return { ...delta, operations: delta.operations.map((op, i) => fn(op, i)) };
1636
+ }
1637
+ function deltaStamp(delta, extensions) {
1638
+ return deltaMap(delta, (op) => ({ ...op, ...extensions }));
1639
+ }
1640
+ function deltaGroupBy(delta, keyFn) {
1641
+ const groups = /* @__PURE__ */ Object.create(null);
1642
+ for (const op of delta.operations) {
1643
+ const k = keyFn(op);
1644
+ if (!groups[k]) groups[k] = [];
1645
+ groups[k].push(op);
1646
+ }
1647
+ const envelope = /* @__PURE__ */ Object.create(null);
1648
+ for (const key of Object.keys(delta)) {
1649
+ if (key !== "operations") {
1650
+ envelope[key] = delta[key];
1651
+ }
1652
+ }
1653
+ const result = /* @__PURE__ */ Object.create(null);
1654
+ for (const [k, ops] of Object.entries(groups)) {
1655
+ result[k] = { ...envelope, operations: ops };
1656
+ }
1657
+ return result;
1658
+ }
1659
+ function deepClone(obj) {
1660
+ return JSON.parse(JSON.stringify(obj));
1661
+ }
1662
+ function squashDeltas(source, deltas, options = {}) {
1663
+ const { target, verifyTarget = true, ...diffOptions } = options;
1664
+ let final;
1665
+ if (target !== void 0 && deltas.length > 0 && verifyTarget) {
1666
+ let computed = deepClone(source);
1667
+ for (const d of deltas) {
1668
+ computed = applyDelta(computed, d);
1669
+ }
1670
+ const verification = diffDelta(computed, target, diffOptions);
1671
+ if (verification.operations.length > 0) {
1672
+ throw new Error(
1673
+ "squashDeltas: provided target does not match sequential application of deltas to source"
1674
+ );
1675
+ }
1676
+ final = target;
1677
+ } else if (target !== void 0) {
1678
+ final = target;
1679
+ } else {
1680
+ final = deepClone(source);
1681
+ for (const d of deltas) {
1682
+ final = applyDelta(final, d);
1683
+ }
1684
+ }
1685
+ const result = diffDelta(source, final, diffOptions);
1686
+ for (const d of deltas) {
1687
+ for (const key of Object.keys(d)) {
1688
+ if (!DELTA_SPEC_KEYS.has(key)) {
1689
+ Object.defineProperty(result, key, {
1690
+ value: d[key],
1691
+ writable: true,
1692
+ enumerable: true,
1693
+ configurable: true
1694
+ });
1695
+ }
1696
+ }
1697
+ }
1698
+ return result;
1699
+ }
1514
1700
  // Annotate the CommonJS export names for ESM import in node:
1515
1701
  0 && (module.exports = {
1516
1702
  CompareOperation,
@@ -1521,8 +1707,15 @@ function revertDelta(obj, delta) {
1521
1707
  atomizeChangeset,
1522
1708
  buildDeltaPath,
1523
1709
  compare,
1710
+ comparisonToDict,
1711
+ comparisonToFlatList,
1524
1712
  createContainer,
1525
1713
  createValue,
1714
+ deltaExtensions,
1715
+ deltaGroupBy,
1716
+ deltaMap,
1717
+ deltaSpecDict,
1718
+ deltaStamp,
1526
1719
  diff,
1527
1720
  diffDelta,
1528
1721
  enrich,
@@ -1530,10 +1723,14 @@ function revertDelta(obj, delta) {
1530
1723
  fromDelta,
1531
1724
  getTypeOfObj,
1532
1725
  invertDelta,
1726
+ leafProperty,
1727
+ operationExtensions,
1728
+ operationSpecDict,
1533
1729
  parseDeltaPath,
1534
1730
  parseFilterLiteral,
1535
1731
  revertChangeset,
1536
1732
  revertDelta,
1733
+ squashDeltas,
1537
1734
  toDelta,
1538
1735
  unatomizeChangeset,
1539
1736
  validateDelta