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