json-diff-ts 2.0.0 → 2.2.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/README.md CHANGED
@@ -33,7 +33,8 @@ const oldData = {
33
33
  characters: [
34
34
  { id: 'LUK', name: 'Luke Skywalker', force: true },
35
35
  { id: 'LEI', name: 'Leia Organa', force: true }
36
- ]
36
+ ],
37
+ weapons: ['Lightsaber', 'Blaster']
37
38
  };
38
39
 
39
40
  const newData = {
@@ -42,7 +43,8 @@ const newData = {
42
43
  characters: [
43
44
  { id: 'LUK', name: 'Luke Skywalker', force: true, rank: 'Commander' },
44
45
  { id: 'HAN', name: 'Han Solo', force: false }
45
- ]
46
+ ],
47
+ weapons: ['Lightsaber', 'Blaster', 'Bowcaster']
46
48
  };
47
49
 
48
50
  const diffs = diff(oldData, newData, { characters: 'id' });
@@ -116,7 +118,13 @@ const expectedDiffs = [
116
118
  Paths can be utilized to identify keys within nested arrays.
117
119
 
118
120
  ```javascript
119
- const diffs = diff(oldData, newData, { characters.subarray: 'id' });
121
+ const diffs = diff(oldData, newData, { 'characters.subarray': 'id' });
122
+ ```
123
+
124
+ You can also designate the root by using '.' instead of an empty string ('').
125
+
126
+ ```javascript
127
+ const diffs = diff(oldData, newData, { '.characters.subarray': 'id' });
120
128
  ```
121
129
 
122
130
  You can use a function to dynamically resolve the key of the object.
@@ -138,6 +146,12 @@ embeddedObjKeys.set(/^char\w+$/, 'id'); // instead of 'id' you can specify a fun
138
146
  const diffs = diff(oldObj, newObj, embeddedObjKeys);
139
147
  ```
140
148
 
149
+ Compare string arrays by value instead of index
150
+
151
+ ```javascript
152
+ const diffs = diff(oldObj, newObj, { stringArr: '$value' });
153
+ ```
154
+
141
155
  ### `flattenChangeset`
142
156
 
143
157
  Transforms a complex changeset into a flat list of atomic changes, each describable by a JSONPath.
@@ -238,6 +252,8 @@ Discover more about the company behind this project: [hololux](https://hololux.c
238
252
 
239
253
  ## Release Notes
240
254
 
255
+ - **v2.2.0:** Fix lodash-es decependency, exclude keys, compare string arrays by value
256
+ - **v2.1.0:** Resolves a problem related to JSON Path filters by replacing the single equal sign (=) with a double equal sign (==). This update maintains compatibility with existing flat changes. Allows to use either '' or '.' as root in the path.
241
257
  - **v2.0.0:** json-diff-ts has been upgraded to an ECMAScript module! This major update brings optimizations and enhanced documentation. Additionally, a previously existing issue where all paths were treated as regex has been fixed. In this new version, you'll need to use a Map instead of a Record for regex paths. Please note that this is a breaking change if you were using regex paths in the previous versions.
242
258
  - **v1.2.6:** Enhanced JSON Path handling for period-inclusive segments.
243
259
  - **v1.2.5:** Patched dependencies; added key name resolution support for key functions.
package/lib/jsonDiff.d.ts CHANGED
@@ -23,7 +23,15 @@ export interface IFlatChange {
23
23
  value?: any;
24
24
  oldValue?: any;
25
25
  }
26
- export declare function diff(oldObj: any, newObj: any, embeddedObjKeys?: EmbeddedObjKeysType | EmbeddedObjKeysMapType): IChange[];
26
+ /**
27
+ * Computes the difference between two objects.
28
+ *
29
+ * @param {any} oldObj - The original object.
30
+ * @param {any} newObj - The updated object.
31
+ * @param {EmbeddedObjKeysType | EmbeddedObjKeysMapType} embeddedObjKeys - An optional parameter specifying keys of embedded objects.
32
+ * @returns {IChange[]} - An array of changes that transform the old object into the new object.
33
+ */
34
+ export declare function diff(oldObj: any, newObj: any, embeddedObjKeys?: EmbeddedObjKeysType | EmbeddedObjKeysMapType, keysToSkip?: string[]): IChange[];
27
35
  /**
28
36
  * Applies all changes in the changeset to the object.
29
37
  *
package/lib/jsonDiff.js CHANGED
@@ -5,8 +5,27 @@ export var Operation;
5
5
  Operation["ADD"] = "ADD";
6
6
  Operation["UPDATE"] = "UPDATE";
7
7
  })(Operation || (Operation = {}));
8
- export function diff(oldObj, newObj, embeddedObjKeys) {
9
- return compare(oldObj, newObj, [], embeddedObjKeys, []);
8
+ /**
9
+ * Computes the difference between two objects.
10
+ *
11
+ * @param {any} oldObj - The original object.
12
+ * @param {any} newObj - The updated object.
13
+ * @param {EmbeddedObjKeysType | EmbeddedObjKeysMapType} embeddedObjKeys - An optional parameter specifying keys of embedded objects.
14
+ * @returns {IChange[]} - An array of changes that transform the old object into the new object.
15
+ */
16
+ export function diff(oldObj, newObj, embeddedObjKeys, keysToSkip) {
17
+ // Trim leading '.' from keys in embeddedObjKeys
18
+ if (embeddedObjKeys instanceof Map) {
19
+ embeddedObjKeys = new Map(Array.from(embeddedObjKeys.entries()).map(([key, value]) => [
20
+ key instanceof RegExp ? key : key.replace(/^\./, ''),
21
+ value
22
+ ]));
23
+ }
24
+ else if (embeddedObjKeys) {
25
+ embeddedObjKeys = Object.fromEntries(Object.entries(embeddedObjKeys).map(([key, value]) => [key.replace(/^\./, ''), value]));
26
+ }
27
+ // Compare old and new objects to generate a list of changes
28
+ return compare(oldObj, newObj, [], embeddedObjKeys, [], keysToSkip);
10
29
  }
11
30
  /**
12
31
  * Applies all changes in the changeset to the object.
@@ -73,13 +92,31 @@ export const flattenChangeset = (obj, path = '$', embeddedKey) => {
73
92
  }
74
93
  else {
75
94
  if (obj.changes || embeddedKey) {
76
- path = embeddedKey
77
- ? embeddedKey === '$index'
78
- ? `${path}[${obj.key}]`
79
- : obj.type === Operation.ADD
80
- ? path
81
- : filterExpression(path, embeddedKey, obj.key)
82
- : (path = append(path, obj.key));
95
+ if (embeddedKey) {
96
+ if (embeddedKey === '$index') {
97
+ path = `${path}[${obj.key}]`;
98
+ }
99
+ else if (embeddedKey === '$value') {
100
+ path = `${path}[?(@='${obj.key}')]`;
101
+ const valueType = getTypeOfObj(obj.value);
102
+ return [
103
+ {
104
+ ...obj,
105
+ path,
106
+ valueType
107
+ }
108
+ ];
109
+ }
110
+ else if (obj.type === Operation.ADD) {
111
+ // do nothing
112
+ }
113
+ else {
114
+ path = filterExpression(path, embeddedKey, obj.key);
115
+ }
116
+ }
117
+ else {
118
+ path = append(path, obj.key);
119
+ }
83
120
  return flattenChangeset(obj.changes || obj, path, obj.embeddedKey);
84
121
  }
85
122
  else {
@@ -115,14 +152,7 @@ export const unflattenChanges = (changes) => {
115
152
  changes.forEach((change) => {
116
153
  const obj = {};
117
154
  let ptr = obj;
118
- const segments = change.path.split(/([^@])\./).reduce((acc, curr, i) => {
119
- const x = Math.floor(i / 2);
120
- if (!acc[x]) {
121
- acc[x] = '';
122
- }
123
- acc[x] += curr;
124
- return acc;
125
- }, []);
155
+ const segments = change.path.split(/(?<=[^@])\.(?=[^@])/);
126
156
  if (segments.length === 1) {
127
157
  ptr.key = change.key;
128
158
  ptr.type = change.type;
@@ -133,8 +163,8 @@ export const unflattenChanges = (changes) => {
133
163
  else {
134
164
  for (let i = 1; i < segments.length; i++) {
135
165
  const segment = segments[i];
136
- // Matches JSONPath segments: "items[?(@.id='123')]" or "items[2]"
137
- const result = /^(.+)\[\?\(@\.(.+)='(.+)'\)]$|^(.+)\[(\d+)\]/.exec(segment);
166
+ // Matches JSONPath segments: "items[?(@.id=='123')]", "items[?(@.id==123)]", "items[2]", "items[?(@='123')]"
167
+ const result = /^(.+?)\[\?\(@.?(?:([^=]*))?={1,2}'(.*)'\)\]$|^(.+?)\[(\d+)\]$/.exec(segment); //NOSONAR
138
168
  // array
139
169
  if (result) {
140
170
  let key;
@@ -142,13 +172,13 @@ export const unflattenChanges = (changes) => {
142
172
  let arrKey;
143
173
  if (result[1]) {
144
174
  key = result[1];
145
- embeddedKey = result[2];
175
+ embeddedKey = result[2] || '$value';
146
176
  arrKey = result[3];
147
177
  }
148
178
  else {
149
179
  key = result[4];
150
180
  embeddedKey = '$index';
151
- arrKey = Number(result[5]);
181
+ arrKey = Number(result[4]);
152
182
  }
153
183
  // leaf
154
184
  if (i === segments.length - 1) {
@@ -241,7 +271,7 @@ const getKey = (path) => {
241
271
  const left = path[path.length - 1];
242
272
  return left != null ? left : '$root';
243
273
  };
244
- const compare = (oldObj, newObj, path, embeddedObjKeys, keyPath) => {
274
+ const compare = (oldObj, newObj, path, embeddedObjKeys, keyPath, keysToSkip) => {
245
275
  let changes = [];
246
276
  const typeOfOldObj = getTypeOfObj(oldObj);
247
277
  const typeOfNewObj = getTypeOfObj(newObj);
@@ -260,7 +290,7 @@ const compare = (oldObj, newObj, path, embeddedObjKeys, keyPath) => {
260
290
  })));
261
291
  break;
262
292
  case 'Object':
263
- const diffs = compareObject(oldObj, newObj, path, embeddedObjKeys, keyPath);
293
+ const diffs = compareObject(oldObj, newObj, path, embeddedObjKeys, keyPath, false, keysToSkip);
264
294
  if (diffs.length) {
265
295
  if (path.length) {
266
296
  changes.push({
@@ -275,7 +305,7 @@ const compare = (oldObj, newObj, path, embeddedObjKeys, keyPath) => {
275
305
  }
276
306
  break;
277
307
  case 'Array':
278
- changes = changes.concat(compareArray(oldObj, newObj, path, embeddedObjKeys, keyPath));
308
+ changes = changes.concat(compareArray(oldObj, newObj, path, embeddedObjKeys, keyPath, keysToSkip));
279
309
  break;
280
310
  case 'Function':
281
311
  break;
@@ -285,7 +315,7 @@ const compare = (oldObj, newObj, path, embeddedObjKeys, keyPath) => {
285
315
  }
286
316
  return changes;
287
317
  };
288
- const compareObject = (oldObj, newObj, path, embeddedObjKeys, keyPath, skipPath = false) => {
318
+ const compareObject = (oldObj, newObj, path, embeddedObjKeys, keyPath, skipPath = false, keysToSkip = []) => {
289
319
  let k;
290
320
  let newKeyPath;
291
321
  let newPath;
@@ -293,13 +323,13 @@ const compareObject = (oldObj, newObj, path, embeddedObjKeys, keyPath, skipPath
293
323
  skipPath = false;
294
324
  }
295
325
  let changes = [];
296
- const oldObjKeys = Object.keys(oldObj);
297
- const newObjKeys = Object.keys(newObj);
326
+ const oldObjKeys = Object.keys(oldObj).filter((key) => keysToSkip.indexOf(key) === -1);
327
+ const newObjKeys = Object.keys(newObj).filter((key) => keysToSkip.indexOf(key) === -1);
298
328
  const intersectionKeys = intersection(oldObjKeys, newObjKeys);
299
329
  for (k of intersectionKeys) {
300
330
  newPath = path.concat([k]);
301
331
  newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
302
- const diffs = compare(oldObj[k], newObj[k], newPath, embeddedObjKeys, newKeyPath);
332
+ const diffs = compare(oldObj[k], newObj[k], newPath, embeddedObjKeys, newKeyPath, keysToSkip);
303
333
  if (diffs.length) {
304
334
  changes = changes.concat(diffs);
305
335
  }
@@ -326,12 +356,12 @@ const compareObject = (oldObj, newObj, path, embeddedObjKeys, keyPath, skipPath
326
356
  }
327
357
  return changes;
328
358
  };
329
- const compareArray = (oldObj, newObj, path, embeddedObjKeys, keyPath) => {
359
+ const compareArray = (oldObj, newObj, path, embeddedObjKeys, keyPath, keysToSkip) => {
330
360
  const left = getObjectKey(embeddedObjKeys, keyPath);
331
361
  const uniqKey = left != null ? left : '$index';
332
362
  const indexedOldObj = convertArrayToObj(oldObj, uniqKey);
333
363
  const indexedNewObj = convertArrayToObj(newObj, uniqKey);
334
- const diffs = compareObject(indexedOldObj, indexedNewObj, path, embeddedObjKeys, keyPath, true);
364
+ const diffs = compareObject(indexedOldObj, indexedNewObj, path, embeddedObjKeys, keyPath, true, keysToSkip);
335
365
  if (diffs.length) {
336
366
  return [
337
367
  {
@@ -370,7 +400,12 @@ const getObjectKey = (embeddedObjKeys, keyPath) => {
370
400
  };
371
401
  const convertArrayToObj = (arr, uniqKey) => {
372
402
  let obj = {};
373
- if (uniqKey !== '$index') {
403
+ if (uniqKey === '$value') {
404
+ arr.forEach((value) => {
405
+ obj[value] = value;
406
+ });
407
+ }
408
+ else if (uniqKey !== '$index') {
374
409
  obj = keyBy(arr, uniqKey);
375
410
  }
376
411
  else {
@@ -414,6 +449,9 @@ const removeKey = (obj, key, embeddedKey) => {
414
449
  }
415
450
  };
416
451
  const indexOfItemInArray = (arr, key, value) => {
452
+ if (key === '$value') {
453
+ return arr.indexOf(value);
454
+ }
417
455
  for (let i = 0; i < arr.length; i++) {
418
456
  const item = arr[i];
419
457
  if (item && item[key] ? item[key].toString() === value.toString() : undefined) {
@@ -453,8 +491,14 @@ const applyArrayChange = (arr, change) => (() => {
453
491
  if (change.embeddedKey === '$index') {
454
492
  element = arr[subchange.key];
455
493
  }
494
+ else if (change.embeddedKey === '$value') {
495
+ const index = arr.indexOf(subchange.key);
496
+ if (index !== -1) {
497
+ element = arr[index];
498
+ }
499
+ }
456
500
  else {
457
- element = find(arr, (el) => el[change.embeddedKey].toString() === subchange.key.toString());
501
+ element = find(arr, (el) => el[change.embeddedKey]?.toString() === subchange.key.toString());
458
502
  }
459
503
  result.push(applyChangeset(element, subchange.changes));
460
504
  }
@@ -513,7 +557,8 @@ function append(basePath, nextSegment) {
513
557
  }
514
558
  /** returns a JSON Path filter expression; e.g., `$.pet[(?name='spot')]` */
515
559
  function filterExpression(basePath, filterKey, filterValue) {
560
+ const value = typeof filterValue === 'number' ? filterValue : `'${filterValue}'`;
516
561
  return typeof filterKey === 'string' && filterKey.includes('.')
517
- ? `${basePath}[?(@[${filterKey}]='${filterValue}')]`
518
- : `${basePath}[?(@.${filterKey}='${filterValue}')]`;
562
+ ? `${basePath}[?(@[${filterKey}]==${value})]`
563
+ : `${basePath}[?(@.${filterKey}==${value})]`;
519
564
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-diff-ts",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "A JSON diff tool for JavaScript written in TypeScript.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -9,8 +9,8 @@
9
9
  "build": "tsc",
10
10
  "format": "prettier --write \"src/**/*.ts\"",
11
11
  "lint": "eslint 'src/**/*.ts'",
12
- "test": "NODE_OPTIONS=--experimental-vm-modules npx jest --config jest.config.mjs",
13
- "test:watch": "NODE_OPTIONS=--experimental-vm-modules npx jest --watch --config jest.config.mjs",
12
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.mjs",
13
+ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch --config jest.config.mjs",
14
14
  "prepare": "npm run build",
15
15
  "prepublishOnly": "npm test && npm run lint",
16
16
  "preversion": "npm run lint",
@@ -37,15 +37,17 @@
37
37
  "url": "https://github.com/ltwlf/json-diff-ts/issues"
38
38
  },
39
39
  "homepage": "https://github.com/ltwlf/json-diff-ts#readme",
40
+ "dependencies": {
41
+ "lodash-es": "^4.17.21"
42
+ },
40
43
  "devDependencies": {
41
44
  "@jest/globals": "^29.7.0",
42
45
  "@types/jest": "^29.5.7",
43
46
  "@types/lodash-es": "^4.17.10",
44
47
  "eslint": "^8.53.0",
45
- "jest": "^29.5.0",
46
- "lodash-es": "^4.17.21",
48
+ "jest": "^29.7.0",
47
49
  "prettier": "^3.0.3",
48
50
  "ts-jest": "^29.0.5",
49
- "typescript": "^4.9.5"
51
+ "typescript": "^5.3.2"
50
52
  }
51
53
  }