json-diff-ts 3.0.0-beta.0 → 3.0.0-beta.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.
@@ -1,68 +0,0 @@
1
- import { chain, keys, replace, set } from 'lodash-es';
2
- import { diff, flattenChangeset, getTypeOfObj, Operation } from './jsonDiff.js';
3
- export var CompareOperation;
4
- (function (CompareOperation) {
5
- CompareOperation["CONTAINER"] = "CONTAINER";
6
- CompareOperation["UNCHANGED"] = "UNCHANGED";
7
- })(CompareOperation || (CompareOperation = {}));
8
- export const createValue = (value) => ({ type: CompareOperation.UNCHANGED, value });
9
- export const createContainer = (value) => ({
10
- type: CompareOperation.CONTAINER,
11
- value
12
- });
13
- export const enrich = (object) => {
14
- const objectType = getTypeOfObj(object);
15
- switch (objectType) {
16
- case 'Object':
17
- return keys(object)
18
- .map((key) => ({ key, value: enrich(object[key]) }))
19
- .reduce((accumulator, entry) => {
20
- accumulator.value[entry.key] = entry.value;
21
- return accumulator;
22
- }, createContainer({}));
23
- case 'Array':
24
- return chain(object)
25
- .map((value) => enrich(value))
26
- .reduce((accumulator, value) => {
27
- accumulator.value.push(value);
28
- return accumulator;
29
- }, createContainer([]))
30
- .value();
31
- case 'Function':
32
- return undefined;
33
- case 'Date':
34
- default:
35
- // Primitive value
36
- return createValue(object);
37
- }
38
- };
39
- export const applyChangelist = (object, changelist) => {
40
- chain(changelist)
41
- .map((entry) => ({ ...entry, path: replace(entry.path, '$.', '.') }))
42
- .map((entry) => ({
43
- ...entry,
44
- path: replace(entry.path, /(\[(?<array>\d)\]\.)/g, 'ARRVAL_START$<array>ARRVAL_END')
45
- }))
46
- .map((entry) => ({ ...entry, path: replace(entry.path, /(?<dot>\.)/g, '.value$<dot>') }))
47
- .map((entry) => ({ ...entry, path: replace(entry.path, /\./, '') }))
48
- .map((entry) => ({ ...entry, path: replace(entry.path, /ARRVAL_START/g, '.value[') }))
49
- .map((entry) => ({ ...entry, path: replace(entry.path, /ARRVAL_END/g, '].value.') }))
50
- .value()
51
- .forEach((entry) => {
52
- switch (entry.type) {
53
- case Operation.ADD:
54
- case Operation.UPDATE:
55
- set(object, entry.path, { type: entry.type, value: entry.value, oldValue: entry.oldValue });
56
- break;
57
- case Operation.REMOVE:
58
- set(object, entry.path, { type: entry.type, value: undefined, oldValue: entry.value });
59
- break;
60
- default:
61
- throw new Error();
62
- }
63
- });
64
- return object;
65
- };
66
- export const compare = (oldObject, newObject) => {
67
- return applyChangelist(enrich(oldObject), flattenChangeset(diff(oldObject, newObject)));
68
- };
package/lib/jsonDiff.js DELETED
@@ -1,564 +0,0 @@
1
- import { difference, find, intersection, keyBy } from 'lodash-es';
2
- export var Operation;
3
- (function (Operation) {
4
- Operation["REMOVE"] = "REMOVE";
5
- Operation["ADD"] = "ADD";
6
- Operation["UPDATE"] = "UPDATE";
7
- })(Operation || (Operation = {}));
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);
29
- }
30
- /**
31
- * Applies all changes in the changeset to the object.
32
- *
33
- * @param {any} obj - The object to apply changes to.
34
- * @param {Changeset} changeset - The changeset to apply.
35
- * @returns {any} - The object after the changes from the changeset have been applied.
36
- *
37
- * The function first checks if a changeset is provided. If so, it iterates over each change in the changeset.
38
- * If the change value is not null or undefined, or if the change type is REMOVE, it applies the change to the object directly.
39
- * Otherwise, it applies the change to the corresponding branch of the object.
40
- */
41
- export const applyChangeset = (obj, changeset) => {
42
- if (changeset) {
43
- changeset.forEach((change) => {
44
- const { type, key, value, embeddedKey } = change;
45
- if ((value !== null && value !== undefined) || type === Operation.REMOVE) {
46
- // Apply the change to the object
47
- applyLeafChange(obj, change, embeddedKey);
48
- }
49
- else {
50
- // Apply the change to the branch
51
- applyBranchChange(obj[key], change);
52
- }
53
- });
54
- }
55
- return obj;
56
- };
57
- /**
58
- * Reverts the changes made to an object based on a given changeset.
59
- *
60
- * @param {any} obj - The object on which to revert changes.
61
- * @param {Changeset} changeset - The changeset to revert.
62
- * @returns {any} - The object after the changes from the changeset have been reverted.
63
- *
64
- * The function first checks if a changeset is provided. If so, it reverses the changeset to start reverting from the last change.
65
- * It then iterates over each change in the changeset. If the change does not have any nested changes, it reverts the change on the object directly.
66
- * If the change does have nested changes, it reverts the changes on the corresponding branch of the object.
67
- */
68
- export const revertChangeset = (obj, changeset) => {
69
- if (changeset) {
70
- changeset
71
- .reverse()
72
- .forEach((change) => !change.changes ? revertLeafChange(obj, change) : revertBranchChange(obj[change.key], change));
73
- }
74
- return obj;
75
- };
76
- /**
77
- * Flattens a changeset into an array of flat changes.
78
- *
79
- * @param {Changeset | IChange} obj - The changeset or change to flatten.
80
- * @param {string} [path='$'] - The current path in the changeset.
81
- * @param {string | FunctionKey} [embeddedKey] - The key to use for embedded objects.
82
- * @returns {IFlatChange[]} - An array of flat changes.
83
- *
84
- * The function first checks if the input is an array. If so, it recursively flattens each change in the array.
85
- * If the input is not an array, it checks if the change has nested changes or an embedded key.
86
- * If so, it updates the path and recursively flattens the nested changes or the embedded object.
87
- * If the change does not have nested changes or an embedded key, it creates a flat change and returns it in an array.
88
- */
89
- export const flattenChangeset = (obj, path = '$', embeddedKey) => {
90
- if (Array.isArray(obj)) {
91
- return obj.reduce((memo, change) => [...memo, ...flattenChangeset(change, path, embeddedKey)], []);
92
- }
93
- else {
94
- if (obj.changes || embeddedKey) {
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
- }
120
- return flattenChangeset(obj.changes || obj, path, obj.embeddedKey);
121
- }
122
- else {
123
- const valueType = getTypeOfObj(obj.value);
124
- return [
125
- {
126
- ...obj,
127
- path: valueType === 'Object' || path.endsWith(`[${obj.key}]`) ? path : append(path, obj.key),
128
- valueType
129
- }
130
- ];
131
- }
132
- }
133
- };
134
- /**
135
- * Transforms a flat changeset into a nested changeset.
136
- *
137
- * @param {IFlatChange | IFlatChange[]} changes - The flat changeset to unflatten.
138
- * @returns {IChange[]} - The unflattened changeset.
139
- *
140
- * The function first checks if the input is a single change or an array of changes.
141
- * It then iterates over each change and splits its path into segments.
142
- * For each segment, it checks if it represents an array or a leaf node.
143
- * If it represents an array, it creates a new change object and updates the pointer to this new object.
144
- * If it represents a leaf node, it sets the key, type, value, and oldValue of the current change object.
145
- * Finally, it pushes the unflattened change object into the changes array.
146
- */
147
- export const unflattenChanges = (changes) => {
148
- if (!Array.isArray(changes)) {
149
- changes = [changes];
150
- }
151
- const changesArr = [];
152
- changes.forEach((change) => {
153
- const obj = {};
154
- let ptr = obj;
155
- const segments = change.path.split(/(?<=[^@])\.(?=[^@])/);
156
- if (segments.length === 1) {
157
- ptr.key = change.key;
158
- ptr.type = change.type;
159
- ptr.value = change.value;
160
- ptr.oldValue = change.oldValue;
161
- changesArr.push(ptr);
162
- }
163
- else {
164
- for (let i = 1; i < segments.length; i++) {
165
- const segment = segments[i];
166
- // Matches JSONPath segments: "items[?(@.id=='123')]", "items[?(@.id==123)]", "items[2]", "items[?(@='123')]"
167
- const result = /^(.+?)\[\?\(@.?(?:([^=]*))?={1,2}'(.*)'\)\]$|^(.+?)\[(\d+)\]$/.exec(segment); //NOSONAR
168
- // array
169
- if (result) {
170
- let key;
171
- let embeddedKey;
172
- let arrKey;
173
- if (result[1]) {
174
- key = result[1];
175
- embeddedKey = result[2] || '$value';
176
- arrKey = result[3];
177
- }
178
- else {
179
- key = result[4];
180
- embeddedKey = '$index';
181
- arrKey = Number(result[4]);
182
- }
183
- // leaf
184
- if (i === segments.length - 1) {
185
- ptr.key = key;
186
- ptr.embeddedKey = embeddedKey;
187
- ptr.type = Operation.UPDATE;
188
- ptr.changes = [
189
- {
190
- type: change.type,
191
- key: arrKey,
192
- value: change.value,
193
- oldValue: change.oldValue
194
- }
195
- ];
196
- }
197
- else {
198
- // object
199
- ptr.key = key;
200
- ptr.embeddedKey = embeddedKey;
201
- ptr.type = Operation.UPDATE;
202
- const newPtr = {};
203
- ptr.changes = [
204
- {
205
- type: Operation.UPDATE,
206
- key: arrKey,
207
- changes: [newPtr]
208
- }
209
- ];
210
- ptr = newPtr;
211
- }
212
- }
213
- else {
214
- // leaf
215
- if (i === segments.length - 1) {
216
- // check if value is a primitive or object
217
- if (change.value !== null && change.valueType === 'Object') {
218
- ptr.key = segment;
219
- ptr.type = Operation.UPDATE;
220
- ptr.changes = [
221
- {
222
- key: change.key,
223
- type: change.type,
224
- value: change.value
225
- }
226
- ];
227
- }
228
- else {
229
- ptr.key = change.key;
230
- ptr.type = change.type;
231
- ptr.value = change.value;
232
- ptr.oldValue = change.oldValue;
233
- }
234
- }
235
- else {
236
- // branch
237
- ptr.key = segment;
238
- ptr.type = Operation.UPDATE;
239
- const newPtr = {};
240
- ptr.changes = [newPtr];
241
- ptr = newPtr;
242
- }
243
- }
244
- }
245
- changesArr.push(obj);
246
- }
247
- });
248
- return changesArr;
249
- };
250
- /**
251
- * Determines the type of a given object.
252
- *
253
- * @param {any} obj - The object whose type is to be determined.
254
- * @returns {string | null} - The type of the object, or null if the object is null.
255
- *
256
- * This function first checks if the object is undefined or null, and returns 'undefined' or null respectively.
257
- * If the object is neither undefined nor null, it uses Object.prototype.toString to get the object's type.
258
- * The type is extracted from the string returned by Object.prototype.toString using a regular expression.
259
- */
260
- export const getTypeOfObj = (obj) => {
261
- if (typeof obj === 'undefined') {
262
- return 'undefined';
263
- }
264
- if (obj === null) {
265
- return null;
266
- }
267
- // Extracts the "Type" from "[object Type]" string.
268
- return Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1];
269
- };
270
- const getKey = (path) => {
271
- const left = path[path.length - 1];
272
- return left != null ? left : '$root';
273
- };
274
- const compare = (oldObj, newObj, path, embeddedObjKeys, keyPath, keysToSkip) => {
275
- let changes = [];
276
- const typeOfOldObj = getTypeOfObj(oldObj);
277
- const typeOfNewObj = getTypeOfObj(newObj);
278
- // if type of object changes, consider it as old obj has been deleted and a new object has been added
279
- if (typeOfOldObj !== typeOfNewObj) {
280
- changes.push({ type: Operation.REMOVE, key: getKey(path), value: oldObj });
281
- changes.push({ type: Operation.ADD, key: getKey(path), value: newObj });
282
- return changes;
283
- }
284
- switch (typeOfOldObj) {
285
- case 'Date':
286
- changes = changes.concat(comparePrimitives(oldObj.getTime(), newObj.getTime(), path).map((x) => ({
287
- ...x,
288
- value: new Date(x.value),
289
- oldValue: new Date(x.oldValue)
290
- })));
291
- break;
292
- case 'Object':
293
- const diffs = compareObject(oldObj, newObj, path, embeddedObjKeys, keyPath, false, keysToSkip);
294
- if (diffs.length) {
295
- if (path.length) {
296
- changes.push({
297
- type: Operation.UPDATE,
298
- key: getKey(path),
299
- changes: diffs
300
- });
301
- }
302
- else {
303
- changes = changes.concat(diffs);
304
- }
305
- }
306
- break;
307
- case 'Array':
308
- changes = changes.concat(compareArray(oldObj, newObj, path, embeddedObjKeys, keyPath, keysToSkip));
309
- break;
310
- case 'Function':
311
- break;
312
- // do nothing
313
- default:
314
- changes = changes.concat(comparePrimitives(oldObj, newObj, path));
315
- }
316
- return changes;
317
- };
318
- const compareObject = (oldObj, newObj, path, embeddedObjKeys, keyPath, skipPath = false, keysToSkip = []) => {
319
- let k;
320
- let newKeyPath;
321
- let newPath;
322
- if (skipPath == null) {
323
- skipPath = false;
324
- }
325
- let changes = [];
326
- const oldObjKeys = Object.keys(oldObj).filter((key) => keysToSkip.indexOf(key) === -1);
327
- const newObjKeys = Object.keys(newObj).filter((key) => keysToSkip.indexOf(key) === -1);
328
- const intersectionKeys = intersection(oldObjKeys, newObjKeys);
329
- for (k of intersectionKeys) {
330
- newPath = path.concat([k]);
331
- newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
332
- const diffs = compare(oldObj[k], newObj[k], newPath, embeddedObjKeys, newKeyPath, keysToSkip);
333
- if (diffs.length) {
334
- changes = changes.concat(diffs);
335
- }
336
- }
337
- const addedKeys = difference(newObjKeys, oldObjKeys);
338
- for (k of addedKeys) {
339
- newPath = path.concat([k]);
340
- newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
341
- changes.push({
342
- type: Operation.ADD,
343
- key: getKey(newPath),
344
- value: newObj[k]
345
- });
346
- }
347
- const deletedKeys = difference(oldObjKeys, newObjKeys);
348
- for (k of deletedKeys) {
349
- newPath = path.concat([k]);
350
- newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
351
- changes.push({
352
- type: Operation.REMOVE,
353
- key: getKey(newPath),
354
- value: oldObj[k]
355
- });
356
- }
357
- return changes;
358
- };
359
- const compareArray = (oldObj, newObj, path, embeddedObjKeys, keyPath, keysToSkip) => {
360
- const left = getObjectKey(embeddedObjKeys, keyPath);
361
- const uniqKey = left != null ? left : '$index';
362
- const indexedOldObj = convertArrayToObj(oldObj, uniqKey);
363
- const indexedNewObj = convertArrayToObj(newObj, uniqKey);
364
- const diffs = compareObject(indexedOldObj, indexedNewObj, path, embeddedObjKeys, keyPath, true, keysToSkip);
365
- if (diffs.length) {
366
- return [
367
- {
368
- type: Operation.UPDATE,
369
- key: getKey(path),
370
- embeddedKey: typeof uniqKey === 'function' && uniqKey.length === 2 ? uniqKey(newObj[0], true) : uniqKey,
371
- changes: diffs
372
- }
373
- ];
374
- }
375
- else {
376
- return [];
377
- }
378
- };
379
- const getObjectKey = (embeddedObjKeys, keyPath) => {
380
- if (embeddedObjKeys != null) {
381
- const path = keyPath.join('.');
382
- if (embeddedObjKeys instanceof Map) {
383
- for (const [key, value] of embeddedObjKeys.entries()) {
384
- if (key instanceof RegExp) {
385
- if (path.match(key)) {
386
- return value;
387
- }
388
- }
389
- else if (path === key) {
390
- return value;
391
- }
392
- }
393
- }
394
- const key = embeddedObjKeys[path];
395
- if (key != null) {
396
- return key;
397
- }
398
- }
399
- return undefined;
400
- };
401
- const convertArrayToObj = (arr, uniqKey) => {
402
- let obj = {};
403
- if (uniqKey === '$value') {
404
- arr.forEach((value) => {
405
- obj[value] = value;
406
- });
407
- }
408
- else if (uniqKey !== '$index') {
409
- obj = keyBy(arr, uniqKey);
410
- }
411
- else {
412
- for (let i = 0; i < arr.length; i++) {
413
- const value = arr[i];
414
- obj[i] = value;
415
- }
416
- }
417
- return obj;
418
- };
419
- const comparePrimitives = (oldObj, newObj, path) => {
420
- const changes = [];
421
- if (oldObj !== newObj) {
422
- changes.push({
423
- type: Operation.UPDATE,
424
- key: getKey(path),
425
- value: newObj,
426
- oldValue: oldObj
427
- });
428
- }
429
- return changes;
430
- };
431
- const removeKey = (obj, key, embeddedKey) => {
432
- if (Array.isArray(obj)) {
433
- if (embeddedKey === '$index') {
434
- obj.splice(key);
435
- return;
436
- }
437
- const index = indexOfItemInArray(obj, embeddedKey, key);
438
- if (index === -1) {
439
- // tslint:disable-next-line:no-console
440
- console.warn(`Element with the key '${embeddedKey}' and value '${key}' could not be found in the array'`);
441
- return;
442
- }
443
- return obj.splice(index != null ? index : key, 1);
444
- }
445
- else {
446
- obj[key] = undefined;
447
- delete obj[key];
448
- return;
449
- }
450
- };
451
- const indexOfItemInArray = (arr, key, value) => {
452
- if (key === '$value') {
453
- return arr.indexOf(value);
454
- }
455
- for (let i = 0; i < arr.length; i++) {
456
- const item = arr[i];
457
- if (item && item[key] ? item[key].toString() === value.toString() : undefined) {
458
- return i;
459
- }
460
- }
461
- return -1;
462
- };
463
- const modifyKeyValue = (obj, key, value) => (obj[key] = value);
464
- const addKeyValue = (obj, key, value) => {
465
- if (Array.isArray(obj)) {
466
- return obj.push(value);
467
- }
468
- else {
469
- return obj ? (obj[key] = value) : null;
470
- }
471
- };
472
- const applyLeafChange = (obj, change, embeddedKey) => {
473
- const { type, key, value } = change;
474
- switch (type) {
475
- case Operation.ADD:
476
- return addKeyValue(obj, key, value);
477
- case Operation.UPDATE:
478
- return modifyKeyValue(obj, key, value);
479
- case Operation.REMOVE:
480
- return removeKey(obj, key, embeddedKey);
481
- }
482
- };
483
- const applyArrayChange = (arr, change) => (() => {
484
- const result = [];
485
- for (const subchange of change.changes) {
486
- if (subchange.value != null || subchange.type === Operation.REMOVE) {
487
- result.push(applyLeafChange(arr, subchange, change.embeddedKey));
488
- }
489
- else {
490
- let element;
491
- if (change.embeddedKey === '$index') {
492
- element = arr[subchange.key];
493
- }
494
- else if (change.embeddedKey === '$value') {
495
- const index = arr.indexOf(subchange.key);
496
- if (index !== -1) {
497
- element = arr[index];
498
- }
499
- }
500
- else {
501
- element = find(arr, (el) => el[change.embeddedKey]?.toString() === subchange.key.toString());
502
- }
503
- result.push(applyChangeset(element, subchange.changes));
504
- }
505
- }
506
- return result;
507
- })();
508
- const applyBranchChange = (obj, change) => {
509
- if (Array.isArray(obj)) {
510
- return applyArrayChange(obj, change);
511
- }
512
- else {
513
- return applyChangeset(obj, change.changes);
514
- }
515
- };
516
- const revertLeafChange = (obj, change, embeddedKey = '$index') => {
517
- const { type, key, value, oldValue } = change;
518
- switch (type) {
519
- case Operation.ADD:
520
- return removeKey(obj, key, embeddedKey);
521
- case Operation.UPDATE:
522
- return modifyKeyValue(obj, key, oldValue);
523
- case Operation.REMOVE:
524
- return addKeyValue(obj, key, value);
525
- }
526
- };
527
- const revertArrayChange = (arr, change) => (() => {
528
- const result = [];
529
- for (const subchange of change.changes) {
530
- if (subchange.value != null || subchange.type === Operation.REMOVE) {
531
- result.push(revertLeafChange(arr, subchange, change.embeddedKey));
532
- }
533
- else {
534
- let element;
535
- if (change.embeddedKey === '$index') {
536
- element = arr[+subchange.key];
537
- }
538
- else {
539
- element = find(arr, (el) => el[change.embeddedKey].toString() === subchange.key);
540
- }
541
- result.push(revertChangeset(element, subchange.changes));
542
- }
543
- }
544
- return result;
545
- })();
546
- const revertBranchChange = (obj, change) => {
547
- if (Array.isArray(obj)) {
548
- return revertArrayChange(obj, change);
549
- }
550
- else {
551
- return revertChangeset(obj, change.changes);
552
- }
553
- };
554
- /** combine a base JSON Path with a subsequent segment */
555
- function append(basePath, nextSegment) {
556
- return nextSegment.includes('.') ? `${basePath}[${nextSegment}]` : `${basePath}.${nextSegment}`;
557
- }
558
- /** returns a JSON Path filter expression; e.g., `$.pet[(?name='spot')]` */
559
- function filterExpression(basePath, filterKey, filterValue) {
560
- const value = typeof filterValue === 'number' ? filterValue : `'${filterValue}'`;
561
- return typeof filterKey === 'string' && filterKey.includes('.')
562
- ? `${basePath}[?(@[${filterKey}]==${value})]`
563
- : `${basePath}[?(@.${filterKey}==${value})]`;
564
- }