json-diff-ts 1.2.6 → 2.0.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 +166 -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 +66 -7
- package/lib/jsonDiff.js +261 -196
- package/package.json +17 -18
package/README.md
CHANGED
|
@@ -4,315 +4,253 @@
|
|
|
4
4
|
[](https://snyk.io/test/github/ltwlf/json-diff-ts?targetFile=package.json)
|
|
5
5
|
[](https://sonarcloud.io/dashboard?id=ltwlf_json-diff-ts)
|
|
6
6
|
|
|
7
|
-
TypeScript
|
|
7
|
+
`json-diff-ts` is a TypeScript library that calculates and applies differences between JSON objects. A standout feature is its ability to identify elements in arrays using keys instead of indices, which offers a more intuitive way to handle arrays. It also supports JSONPath, a query language for JSON, which enables you to target specific parts of a JSON document with precision.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Another significant feature of this library is its ability to transform changesets into atomic changes. This means that each change in the data can be isolated and applied independently, providing a granular level of control over the data manipulation process.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
This library is particularly valuable for applications where tracking changes in JSON data is crucial. It simplifies the process of comparing JSON objects and applying changes. The support for key-based array identification can be especially useful in complex JSON structures where tracking by index is not efficient or intuitive. JSONPath support further enhances its capabilities by allowing precise targeting of specific parts in a JSON document, making it a versatile tool for handling JSON data.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## Installation
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
```sh
|
|
16
|
+
npm install json-diff-ts
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Capabilities
|
|
20
|
+
|
|
21
|
+
### `diff`
|
|
22
|
+
|
|
23
|
+
Generates a difference set for JSON objects. When comparing arrays, if a specific key is provided, differences are determined by matching elements via this key rather than array indices.
|
|
24
|
+
|
|
25
|
+
#### Examples using Star Wars data:
|
|
16
26
|
|
|
17
27
|
```javascript
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{ name: 'kid1', age: 1 },
|
|
27
|
-
{ name: 'kid2', age: 2 }
|
|
28
|
+
import { diff } from 'json-diff-ts';
|
|
29
|
+
|
|
30
|
+
const oldData = {
|
|
31
|
+
planet: 'Tatooine',
|
|
32
|
+
faction: 'Jedi',
|
|
33
|
+
characters: [
|
|
34
|
+
{ id: 'LUK', name: 'Luke Skywalker', force: true },
|
|
35
|
+
{ id: 'LEI', name: 'Leia Organa', force: true }
|
|
28
36
|
]
|
|
29
37
|
};
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{ name: '
|
|
36
|
-
{ name: '
|
|
37
|
-
{ name: 'kid2', age: 2 }
|
|
39
|
+
const newData = {
|
|
40
|
+
planet: 'Alderaan',
|
|
41
|
+
faction: 'Rebel Alliance',
|
|
42
|
+
characters: [
|
|
43
|
+
{ id: 'LUK', name: 'Luke Skywalker', force: true, rank: 'Commander' },
|
|
44
|
+
{ id: 'HAN', name: 'Han Solo', force: false }
|
|
38
45
|
]
|
|
39
46
|
};
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
// keys can also be hierarchical e.g. {children: 'name', 'children.grandChildren', 'age'}
|
|
43
|
-
// or use functions that return the key of an object e.g. {children: function(obj) { return obj.key; }}
|
|
44
|
-
// when you use a function flatten can not generate the correct path.
|
|
45
|
-
// to fix this, you can add an additional parameter e.g. (obj, getKeyNameFlag) => {...}. getKeyNameFlag will be true when the diff library try to resolve the key name instead of the key value. You can return a static string or use obj to check which key name you should return. obj will be the first object of the array!
|
|
46
|
-
diffs = changesets.diff(oldObj, newObj, { children: 'name' });
|
|
48
|
+
const diffs = diff(oldData, newData, { characters: 'id' });
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
const expectedDiffs = [
|
|
49
51
|
{
|
|
50
|
-
type: '
|
|
51
|
-
key: '
|
|
52
|
-
value: '
|
|
53
|
-
oldValue: '
|
|
52
|
+
type: 'UPDATE',
|
|
53
|
+
key: 'planet',
|
|
54
|
+
value: 'Alderaan',
|
|
55
|
+
oldValue: 'Tatooine'
|
|
54
56
|
},
|
|
55
57
|
{
|
|
56
|
-
type: '
|
|
57
|
-
key: '
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
type: 'UPDATE',
|
|
59
|
+
key: 'faction',
|
|
60
|
+
value: 'Rebel Alliance',
|
|
61
|
+
oldValue: 'Jedi'
|
|
60
62
|
},
|
|
61
63
|
{
|
|
62
|
-
type: '
|
|
63
|
-
key: '
|
|
64
|
-
|
|
64
|
+
type: 'UPDATE',
|
|
65
|
+
key: 'characters',
|
|
66
|
+
embeddedKey: 'id',
|
|
65
67
|
changes: [
|
|
66
68
|
{
|
|
67
|
-
type: '
|
|
68
|
-
key: '
|
|
69
|
-
changes: [
|
|
69
|
+
type: 'UPDATE',
|
|
70
|
+
key: 'LUK',
|
|
71
|
+
changes: [
|
|
72
|
+
{
|
|
73
|
+
type: 'ADD',
|
|
74
|
+
key: 'rank',
|
|
75
|
+
value: 'Commander'
|
|
76
|
+
}
|
|
77
|
+
]
|
|
70
78
|
},
|
|
71
79
|
{
|
|
72
|
-
type: '
|
|
73
|
-
key: '
|
|
74
|
-
value: {
|
|
80
|
+
type: 'ADD',
|
|
81
|
+
key: 'HAN',
|
|
82
|
+
value: {
|
|
83
|
+
id: 'HAN',
|
|
84
|
+
name: 'Han Solo',
|
|
85
|
+
force: false
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: 'REMOVE',
|
|
90
|
+
key: 'LEI',
|
|
91
|
+
value: {
|
|
92
|
+
id: 'LEI',
|
|
93
|
+
name: 'Leia Organa',
|
|
94
|
+
force: true
|
|
95
|
+
}
|
|
75
96
|
}
|
|
76
97
|
]
|
|
77
98
|
},
|
|
78
99
|
{
|
|
79
|
-
type: '
|
|
80
|
-
key: '
|
|
81
|
-
|
|
100
|
+
type: 'UPDATE',
|
|
101
|
+
key: 'weapons',
|
|
102
|
+
embeddedKey: '$index',
|
|
103
|
+
changes: [
|
|
104
|
+
{
|
|
105
|
+
type: 'ADD',
|
|
106
|
+
key: '2',
|
|
107
|
+
value: 'Bowcaster'
|
|
108
|
+
}
|
|
109
|
+
]
|
|
82
110
|
}
|
|
83
|
-
]
|
|
111
|
+
];
|
|
84
112
|
```
|
|
85
113
|
|
|
86
|
-
|
|
114
|
+
#### Advanced
|
|
87
115
|
|
|
88
|
-
|
|
116
|
+
Paths can be utilized to identify keys within nested arrays.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
const diffs = diff(oldData, newData, { characters.subarray: 'id' });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
You can use a function to dynamically resolve the key of the object.
|
|
123
|
+
The first parameter is the object and the second is to signal if the function should return the key name instead of the value. This is needed to flatten the changeset
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
const diffs = diff(oldData, newData, {
|
|
127
|
+
characters: (obj, shouldReturnKeyName) => (shouldReturnKeyName ? 'id' : obj.id)
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
If you're using the Map type, you can employ regular expressions for path identification.
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
const embeddedObjKeys: EmbeddedObjKeysMapType = new Map();
|
|
135
|
+
|
|
136
|
+
embeddedObjKeys.set(/^char\w+$/, 'id'); // instead of 'id' you can specify a function
|
|
137
|
+
|
|
138
|
+
const diffs = diff(oldObj, newObj, embeddedObjKeys);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `flattenChangeset`
|
|
142
|
+
|
|
143
|
+
Transforms a complex changeset into a flat list of atomic changes, each describable by a JSONPath.
|
|
89
144
|
|
|
90
145
|
#### Examples:
|
|
91
146
|
|
|
92
147
|
```javascript
|
|
93
148
|
const flatChanges = flattenChangeset(diffs);
|
|
94
|
-
//
|
|
95
|
-
const changeset = unflattenChanges(flatChanges.slice(
|
|
96
|
-
//
|
|
149
|
+
// Restore the changeset from a selection of flat changes
|
|
150
|
+
const changeset = unflattenChanges(flatChanges.slice(0, 3));
|
|
151
|
+
// Alternatively, apply the changes using a JSONPath-capable library
|
|
97
152
|
// ...
|
|
98
153
|
```
|
|
99
154
|
|
|
100
|
-
|
|
155
|
+
A **flatChange** will have the following structure:
|
|
101
156
|
|
|
102
157
|
```javascript
|
|
103
158
|
[
|
|
104
|
-
{ type: 'UPDATE', key: '
|
|
105
|
-
|
|
106
|
-
{ type: '
|
|
107
|
-
{
|
|
108
|
-
type: 'UPDATE',
|
|
109
|
-
key: 'date',
|
|
110
|
-
value: '2014-10-12T09:13:00.000Z',
|
|
111
|
-
oldValue: '2014-10-13T09:13:00.000Z',
|
|
112
|
-
path: '$.date',
|
|
113
|
-
valueType: 'Date'
|
|
114
|
-
},
|
|
115
|
-
{ type: 'ADD', key: '2', value: 1, path: '$.coins[2]', valueType: 'Number' },
|
|
116
|
-
{ type: 'REMOVE', key: '0', value: 'car', path: '$.toys[0]', valueType: 'String' },
|
|
117
|
-
{ type: 'REMOVE', key: '1', value: 'doll', path: '$.toys[1]', valueType: 'String' },
|
|
118
|
-
{ type: 'REMOVE', key: '0', path: '$.pets[0]', valueType: 'undefined' },
|
|
119
|
-
{ type: 'REMOVE', key: '1', value: null, path: '$.pets[1]', valueType: null },
|
|
120
|
-
{ type: 'UPDATE', key: 'age', value: 0, oldValue: 1, path: "$.children[?(@.name='kid1')].age", valueType: 'Number' },
|
|
121
|
-
{
|
|
122
|
-
type: 'UPDATE',
|
|
123
|
-
key: 'value',
|
|
124
|
-
value: 'heihei',
|
|
125
|
-
oldValue: 'haha',
|
|
126
|
-
path: "$.children[?(@.name='kid1')].subset[?(@.id='1')].value",
|
|
127
|
-
valueType: 'String'
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
type: 'REMOVE',
|
|
131
|
-
key: '2',
|
|
132
|
-
value: { id: 2, value: 'hehe' },
|
|
133
|
-
path: "$.children[?(@.name='kid1')].subset[?(@.id='2')]",
|
|
134
|
-
valueType: 'Object'
|
|
135
|
-
},
|
|
136
|
-
{ type: 'ADD', key: 'kid3', value: { name: 'kid3', age: 3 }, path: '$.children', valueType: 'Object' }
|
|
159
|
+
{ type: 'UPDATE', key: 'planet', value: 'Alderaan', oldValue: 'Tatooine', path: '$.planet', valueType: 'String' },
|
|
160
|
+
// ... Additional flat changes here
|
|
161
|
+
{ type: 'ADD', key: 'rank', value: 'Commander', path: "$.characters[?(@.id=='LUK')].rank", valueType: 'String' }
|
|
137
162
|
];
|
|
138
163
|
```
|
|
139
164
|
|
|
140
|
-
### applyChange
|
|
165
|
+
### `applyChange`
|
|
141
166
|
|
|
142
167
|
#### Examples:
|
|
143
168
|
|
|
144
169
|
```javascript
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
name: 'joe',
|
|
148
|
-
age: 55,
|
|
149
|
-
coins: [2, 5],
|
|
150
|
-
children: [
|
|
151
|
-
{ name: 'kid1', age: 1 },
|
|
152
|
-
{ name: 'kid2', age: 2 }
|
|
153
|
-
]
|
|
170
|
+
const oldData = {
|
|
171
|
+
// ... Initial data here
|
|
154
172
|
};
|
|
155
173
|
|
|
156
|
-
//
|
|
157
|
-
diffs = [
|
|
158
|
-
|
|
159
|
-
type: 'update',
|
|
160
|
-
key: 'name',
|
|
161
|
-
value: 'smith',
|
|
162
|
-
oldValue: 'joe'
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
type: 'update',
|
|
166
|
-
key: 'coins',
|
|
167
|
-
embededKey: '$index',
|
|
168
|
-
changes: [{ type: 'add', key: '2', value: 1 }]
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
type: 'update',
|
|
172
|
-
key: 'children',
|
|
173
|
-
embededKey: 'name', // The key property name of the elements in an array
|
|
174
|
-
changes: [
|
|
175
|
-
{
|
|
176
|
-
type: 'update',
|
|
177
|
-
key: 'kid1',
|
|
178
|
-
changes: [{ type: 'update', key: 'age', value: 0, oldValue: 1 }]
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
type: 'add',
|
|
182
|
-
key: 'kid3',
|
|
183
|
-
value: { name: 'kid3', age: 3 }
|
|
184
|
-
}
|
|
185
|
-
]
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
type: 'remove',
|
|
189
|
-
key: 'age',
|
|
190
|
-
value: 55
|
|
191
|
-
}
|
|
174
|
+
// Sample diffs array, similar to the one generated in the diff example
|
|
175
|
+
const diffs = [
|
|
176
|
+
// ... Diff objects here
|
|
192
177
|
];
|
|
193
178
|
|
|
194
|
-
changesets.applyChanges(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
children: [
|
|
199
|
-
{ name: 'kid3', age: 3 },
|
|
200
|
-
{ name: 'kid1', age: 0 },
|
|
201
|
-
{ name: 'kid2', age: 2 }
|
|
202
|
-
]
|
|
179
|
+
changesets.applyChanges(oldData, diffs);
|
|
180
|
+
|
|
181
|
+
expect(oldData).to.eql({
|
|
182
|
+
// ... Updated data here
|
|
203
183
|
});
|
|
204
184
|
```
|
|
205
185
|
|
|
206
|
-
### revertChange
|
|
186
|
+
### `revertChange`
|
|
207
187
|
|
|
208
188
|
#### Examples:
|
|
209
189
|
|
|
210
190
|
```javascript
|
|
191
|
+
const newData = {
|
|
192
|
+
// ... Updated data here
|
|
193
|
+
};
|
|
211
194
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
coins: [2, 5, 1],
|
|
217
|
-
children: [
|
|
218
|
-
{name: 'kid3', age: 3},
|
|
219
|
-
{name: 'kid1', age: 0},
|
|
220
|
-
{name: 'kid2', age: 2}
|
|
221
|
-
]};
|
|
222
|
-
|
|
223
|
-
// Assume children is an array of child object and the child object has 'name' as its primary key
|
|
224
|
-
diffs = [
|
|
225
|
-
{
|
|
226
|
-
type: 'update', key: 'name', value: 'smith', oldValue: 'joe'
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
type: 'update', key: 'coins', embededKey: '$index', changes: [
|
|
230
|
-
{type: 'add', key: '2', value: 1 }
|
|
231
|
-
]
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
type: 'update',
|
|
235
|
-
key: 'children',
|
|
236
|
-
embededKey: 'name', // The key property name of the elements in an array
|
|
237
|
-
changes: [
|
|
238
|
-
{
|
|
239
|
-
type: 'update', key: 'kid1', changes: [
|
|
240
|
-
{type: 'update', key: 'age', value: 0, oldValue: 1 }
|
|
241
|
-
]
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
type: 'add', key: 'kid3', value: {name: 'kid3', age: 3 }
|
|
245
|
-
}
|
|
246
|
-
]
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
type: 'remove', key: 'age', value: 55
|
|
250
|
-
}
|
|
251
|
-
]
|
|
252
|
-
|
|
253
|
-
changesets.revertChanges(newObj, diffs)
|
|
254
|
-
expect(newObj).to.eql {
|
|
255
|
-
name: 'joe',
|
|
256
|
-
age: 55,
|
|
257
|
-
coins: [2, 5],
|
|
258
|
-
children: [
|
|
259
|
-
{name: 'kid1', age: 1},
|
|
260
|
-
{name: 'kid2', age: 2}
|
|
261
|
-
]};
|
|
262
|
-
|
|
263
|
-
```
|
|
195
|
+
// Sample diffs array
|
|
196
|
+
const diffs = [
|
|
197
|
+
// ... Diff objects here
|
|
198
|
+
];
|
|
264
199
|
|
|
265
|
-
|
|
200
|
+
changesets.revertChanges(newData, diffs);
|
|
266
201
|
|
|
267
|
-
|
|
268
|
-
|
|
202
|
+
expect(newData).to.eql({
|
|
203
|
+
// ... Original data restored here
|
|
204
|
+
});
|
|
269
205
|
```
|
|
270
206
|
|
|
271
|
-
|
|
207
|
+
### `jsonPath`
|
|
272
208
|
|
|
273
|
-
|
|
274
|
-
npm run test
|
|
275
|
-
```
|
|
209
|
+
The `json-diff-ts` library uses JSONPath to address specific parts of a JSON document in both the changeset and the application/reversion of changes.
|
|
276
210
|
|
|
277
|
-
|
|
211
|
+
#### Examples:
|
|
278
212
|
|
|
279
|
-
|
|
213
|
+
```javascript
|
|
280
214
|
|
|
281
|
-
|
|
215
|
+
const jsonPath = changesets.jsonPath;
|
|
282
216
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
- v1.2.4 Fix readme (npm install); update TypeScript and Lodash
|
|
287
|
-
- v1.2.3 Update outdated dependencies; update TypeScript version to 4.5.2
|
|
288
|
-
- v1.2.2 Add support for functions to resove object keys (PR by [Abraxxa](https://github.com/abraxxa))
|
|
217
|
+
cost data = {
|
|
218
|
+
// ... Some JSON data
|
|
219
|
+
};
|
|
289
220
|
|
|
290
|
-
|
|
221
|
+
const value = jsonPath.query(data, '$.characters[?(@.id=="LUK")].name');
|
|
291
222
|
|
|
292
|
-
|
|
223
|
+
expect(value).to.eql(['Luke Skywalker']);
|
|
224
|
+
```
|
|
293
225
|
|
|
294
|
-
##
|
|
226
|
+
## Contributing
|
|
295
227
|
|
|
296
|
-
|
|
228
|
+
Contributions are welcome! Please follow the provided issue templates and code of conduct.
|
|
297
229
|
|
|
298
|
-
|
|
230
|
+
## Contact
|
|
299
231
|
|
|
300
|
-
|
|
232
|
+
Reach out to the maintainer via LinkedIn or Twitter:
|
|
301
233
|
|
|
302
|
-
|
|
234
|
+
- LinkedIn: [Christian Glessner](https://www.linkedin.com/in/christian-glessner/)
|
|
235
|
+
- Twitter: [@leitwolf_io](https://twitter.com/leitwolf_io)
|
|
303
236
|
|
|
304
|
-
|
|
237
|
+
Discover more about the company behind this project: [hololux](https://hololux.com)
|
|
305
238
|
|
|
306
|
-
|
|
239
|
+
## Release Notes
|
|
307
240
|
|
|
308
|
-
**
|
|
241
|
+
- **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
|
+
- **v1.2.6:** Enhanced JSON Path handling for period-inclusive segments.
|
|
243
|
+
- **v1.2.5:** Patched dependencies; added key name resolution support for key functions.
|
|
244
|
+
- **v1.2.4:** Documentation updates; upgraded TypeScript and Lodash.
|
|
245
|
+
- **v1.2.3:** Dependency updates; switched to TypeScript 4.5.2.
|
|
246
|
+
- **v1.2.2:** Implemented object key resolution functions support.
|
|
309
247
|
|
|
310
|
-
|
|
248
|
+
## Acknowledgments
|
|
311
249
|
|
|
312
|
-
|
|
250
|
+
This project takes inspiration and code from [diff-json](https://www.npmjs.com/package/diff-json) by viruschidai@gmail.com.
|
|
313
251
|
|
|
314
|
-
|
|
252
|
+
## License
|
|
315
253
|
|
|
316
|
-
|
|
254
|
+
json-diff-ts is open-sourced software licensed under the [MIT license](LICENSE).
|
|
317
255
|
|
|
318
|
-
|
|
256
|
+
The original diff-json project is also under the MIT License. For more information, refer to its [license details](https://www.npmjs.com/package/diff-json#license).
|
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './jsonDiff';
|
|
2
|
-
export * from './jsonCompare';
|
|
1
|
+
export * from './jsonDiff.js';
|
|
2
|
+
export * from './jsonCompare.js';
|
package/lib/index.js
CHANGED
|
@@ -1,18 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./jsonDiff"), exports);
|
|
18
|
-
__exportStar(require("./jsonCompare"), exports);
|
|
1
|
+
export * from './jsonDiff.js';
|
|
2
|
+
export * from './jsonCompare.js';
|
package/lib/jsonCompare.d.ts
CHANGED
package/lib/jsonCompare.js
CHANGED
|
@@ -1,64 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const lodash_1 = require("lodash");
|
|
5
|
-
const jsonDiff_1 = require("./jsonDiff");
|
|
6
|
-
var CompareOperation;
|
|
1
|
+
import { chain, keys, replace, set } from 'lodash-es';
|
|
2
|
+
import { diff, flattenChangeset, getTypeOfObj, Operation } from './jsonDiff.js';
|
|
3
|
+
export var CompareOperation;
|
|
7
4
|
(function (CompareOperation) {
|
|
8
5
|
CompareOperation["CONTAINER"] = "CONTAINER";
|
|
9
6
|
CompareOperation["UNCHANGED"] = "UNCHANGED";
|
|
10
|
-
})(CompareOperation
|
|
11
|
-
const createValue = (value) => ({ type: CompareOperation.UNCHANGED, value });
|
|
12
|
-
|
|
13
|
-
const createContainer = (value) => ({
|
|
7
|
+
})(CompareOperation || (CompareOperation = {}));
|
|
8
|
+
export const createValue = (value) => ({ type: CompareOperation.UNCHANGED, value });
|
|
9
|
+
export const createContainer = (value) => ({
|
|
14
10
|
type: CompareOperation.CONTAINER,
|
|
15
11
|
value
|
|
16
12
|
});
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const objectType = (0, jsonDiff_1.getTypeOfObj)(object);
|
|
13
|
+
export const enrich = (object) => {
|
|
14
|
+
const objectType = getTypeOfObj(object);
|
|
20
15
|
switch (objectType) {
|
|
21
16
|
case 'Object':
|
|
22
|
-
return
|
|
23
|
-
.map((key) => ({ key, value:
|
|
17
|
+
return keys(object)
|
|
18
|
+
.map((key) => ({ key, value: enrich(object[key]) }))
|
|
24
19
|
.reduce((accumulator, entry) => {
|
|
25
20
|
accumulator.value[entry.key] = entry.value;
|
|
26
21
|
return accumulator;
|
|
27
|
-
},
|
|
22
|
+
}, createContainer({}));
|
|
28
23
|
case 'Array':
|
|
29
|
-
return
|
|
30
|
-
.map(value =>
|
|
24
|
+
return chain(object)
|
|
25
|
+
.map((value) => enrich(value))
|
|
31
26
|
.reduce((accumulator, value) => {
|
|
32
27
|
accumulator.value.push(value);
|
|
33
28
|
return accumulator;
|
|
34
|
-
},
|
|
29
|
+
}, createContainer([]))
|
|
35
30
|
.value();
|
|
36
31
|
case 'Function':
|
|
37
32
|
return undefined;
|
|
38
33
|
case 'Date':
|
|
39
34
|
default:
|
|
40
35
|
// Primitive value
|
|
41
|
-
return
|
|
36
|
+
return createValue(object);
|
|
42
37
|
}
|
|
43
38
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.map(entry => (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.map(entry => (
|
|
52
|
-
.map(entry => (
|
|
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.') }))
|
|
53
50
|
.value()
|
|
54
|
-
.forEach(entry => {
|
|
51
|
+
.forEach((entry) => {
|
|
55
52
|
switch (entry.type) {
|
|
56
|
-
case
|
|
57
|
-
case
|
|
58
|
-
|
|
53
|
+
case Operation.ADD:
|
|
54
|
+
case Operation.UPDATE:
|
|
55
|
+
set(object, entry.path, { type: entry.type, value: entry.value, oldValue: entry.oldValue });
|
|
59
56
|
break;
|
|
60
|
-
case
|
|
61
|
-
|
|
57
|
+
case Operation.REMOVE:
|
|
58
|
+
set(object, entry.path, { type: entry.type, value: undefined, oldValue: entry.value });
|
|
62
59
|
break;
|
|
63
60
|
default:
|
|
64
61
|
throw new Error();
|
|
@@ -66,8 +63,6 @@ const applyChangelist = (object, changelist) => {
|
|
|
66
63
|
});
|
|
67
64
|
return object;
|
|
68
65
|
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return (0, exports.applyChangelist)((0, exports.enrich)(oldObject), (0, jsonDiff_1.flattenChangeset)((0, jsonDiff_1.diff)(oldObject, newObject)));
|
|
66
|
+
export const compare = (oldObject, newObject) => {
|
|
67
|
+
return applyChangelist(enrich(oldObject), flattenChangeset(diff(oldObject, newObject)));
|
|
72
68
|
};
|
|
73
|
-
exports.compare = compare;
|
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,68 @@ export interface IFlatChange {
|
|
|
26
23
|
value?: any;
|
|
27
24
|
oldValue?: any;
|
|
28
25
|
}
|
|
26
|
+
export declare function diff(oldObj: any, newObj: any, embeddedObjKeys?: EmbeddedObjKeysType | EmbeddedObjKeysMapType): IChange[];
|
|
27
|
+
/**
|
|
28
|
+
* Applies all changes in the changeset to the object.
|
|
29
|
+
*
|
|
30
|
+
* @param {any} obj - The object to apply changes to.
|
|
31
|
+
* @param {Changeset} changeset - The changeset to apply.
|
|
32
|
+
* @returns {any} - The object after the changes from the changeset have been applied.
|
|
33
|
+
*
|
|
34
|
+
* The function first checks if a changeset is provided. If so, it iterates over each change in the changeset.
|
|
35
|
+
* If the change value is not null or undefined, or if the change type is REMOVE, it applies the change to the object directly.
|
|
36
|
+
* Otherwise, it applies the change to the corresponding branch of the object.
|
|
37
|
+
*/
|
|
38
|
+
export declare const applyChangeset: (obj: any, changeset: Changeset) => any;
|
|
39
|
+
/**
|
|
40
|
+
* Reverts the changes made to an object based on a given changeset.
|
|
41
|
+
*
|
|
42
|
+
* @param {any} obj - The object on which to revert changes.
|
|
43
|
+
* @param {Changeset} changeset - The changeset to revert.
|
|
44
|
+
* @returns {any} - The object after the changes from the changeset have been reverted.
|
|
45
|
+
*
|
|
46
|
+
* The function first checks if a changeset is provided. If so, it reverses the changeset to start reverting from the last change.
|
|
47
|
+
* 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.
|
|
48
|
+
* If the change does have nested changes, it reverts the changes on the corresponding branch of the object.
|
|
49
|
+
*/
|
|
50
|
+
export declare const revertChangeset: (obj: any, changeset: Changeset) => any;
|
|
51
|
+
/**
|
|
52
|
+
* Flattens a changeset into an array of flat changes.
|
|
53
|
+
*
|
|
54
|
+
* @param {Changeset | IChange} obj - The changeset or change to flatten.
|
|
55
|
+
* @param {string} [path='$'] - The current path in the changeset.
|
|
56
|
+
* @param {string | FunctionKey} [embeddedKey] - The key to use for embedded objects.
|
|
57
|
+
* @returns {IFlatChange[]} - An array of flat changes.
|
|
58
|
+
*
|
|
59
|
+
* The function first checks if the input is an array. If so, it recursively flattens each change in the array.
|
|
60
|
+
* If the input is not an array, it checks if the change has nested changes or an embedded key.
|
|
61
|
+
* If so, it updates the path and recursively flattens the nested changes or the embedded object.
|
|
62
|
+
* If the change does not have nested changes or an embedded key, it creates a flat change and returns it in an array.
|
|
63
|
+
*/
|
|
29
64
|
export declare const flattenChangeset: (obj: Changeset | IChange, path?: string, embeddedKey?: string | FunctionKey) => IFlatChange[];
|
|
65
|
+
/**
|
|
66
|
+
* Transforms a flat changeset into a nested changeset.
|
|
67
|
+
*
|
|
68
|
+
* @param {IFlatChange | IFlatChange[]} changes - The flat changeset to unflatten.
|
|
69
|
+
* @returns {IChange[]} - The unflattened changeset.
|
|
70
|
+
*
|
|
71
|
+
* The function first checks if the input is a single change or an array of changes.
|
|
72
|
+
* It then iterates over each change and splits its path into segments.
|
|
73
|
+
* For each segment, it checks if it represents an array or a leaf node.
|
|
74
|
+
* If it represents an array, it creates a new change object and updates the pointer to this new object.
|
|
75
|
+
* If it represents a leaf node, it sets the key, type, value, and oldValue of the current change object.
|
|
76
|
+
* Finally, it pushes the unflattened change object into the changes array.
|
|
77
|
+
*/
|
|
30
78
|
export declare const unflattenChanges: (changes: IFlatChange | IFlatChange[]) => IChange[];
|
|
79
|
+
/**
|
|
80
|
+
* Determines the type of a given object.
|
|
81
|
+
*
|
|
82
|
+
* @param {any} obj - The object whose type is to be determined.
|
|
83
|
+
* @returns {string | null} - The type of the object, or null if the object is null.
|
|
84
|
+
*
|
|
85
|
+
* This function first checks if the object is undefined or null, and returns 'undefined' or null respectively.
|
|
86
|
+
* If the object is neither undefined nor null, it uses Object.prototype.toString to get the object's type.
|
|
87
|
+
* The type is extracted from the string returned by Object.prototype.toString using a regular expression.
|
|
88
|
+
*/
|
|
89
|
+
export declare const getTypeOfObj: (obj: any) => string;
|
|
31
90
|
export {};
|
package/lib/jsonDiff.js
CHANGED
|
@@ -1,25 +1,250 @@
|
|
|
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
|
+
export function diff(oldObj, newObj, embeddedObjKeys) {
|
|
9
|
+
return compare(oldObj, newObj, [], embeddedObjKeys, []);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Applies all changes in the changeset to the object.
|
|
13
|
+
*
|
|
14
|
+
* @param {any} obj - The object to apply changes to.
|
|
15
|
+
* @param {Changeset} changeset - The changeset to apply.
|
|
16
|
+
* @returns {any} - The object after the changes from the changeset have been applied.
|
|
17
|
+
*
|
|
18
|
+
* The function first checks if a changeset is provided. If so, it iterates over each change in the changeset.
|
|
19
|
+
* If the change value is not null or undefined, or if the change type is REMOVE, it applies the change to the object directly.
|
|
20
|
+
* Otherwise, it applies the change to the corresponding branch of the object.
|
|
21
|
+
*/
|
|
22
|
+
export const applyChangeset = (obj, changeset) => {
|
|
23
|
+
if (changeset) {
|
|
24
|
+
changeset.forEach((change) => {
|
|
25
|
+
const { type, key, value, embeddedKey } = change;
|
|
26
|
+
if ((value !== null && value !== undefined) || type === Operation.REMOVE) {
|
|
27
|
+
// Apply the change to the object
|
|
28
|
+
applyLeafChange(obj, change, embeddedKey);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// Apply the change to the branch
|
|
32
|
+
applyBranchChange(obj[key], change);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return obj;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Reverts the changes made to an object based on a given changeset.
|
|
40
|
+
*
|
|
41
|
+
* @param {any} obj - The object on which to revert changes.
|
|
42
|
+
* @param {Changeset} changeset - The changeset to revert.
|
|
43
|
+
* @returns {any} - The object after the changes from the changeset have been reverted.
|
|
44
|
+
*
|
|
45
|
+
* The function first checks if a changeset is provided. If so, it reverses the changeset to start reverting from the last change.
|
|
46
|
+
* 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.
|
|
47
|
+
* If the change does have nested changes, it reverts the changes on the corresponding branch of the object.
|
|
48
|
+
*/
|
|
49
|
+
export const revertChangeset = (obj, changeset) => {
|
|
50
|
+
if (changeset) {
|
|
51
|
+
changeset
|
|
52
|
+
.reverse()
|
|
53
|
+
.forEach((change) => !change.changes ? revertLeafChange(obj, change) : revertBranchChange(obj[change.key], change));
|
|
54
|
+
}
|
|
55
|
+
return obj;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Flattens a changeset into an array of flat changes.
|
|
59
|
+
*
|
|
60
|
+
* @param {Changeset | IChange} obj - The changeset or change to flatten.
|
|
61
|
+
* @param {string} [path='$'] - The current path in the changeset.
|
|
62
|
+
* @param {string | FunctionKey} [embeddedKey] - The key to use for embedded objects.
|
|
63
|
+
* @returns {IFlatChange[]} - An array of flat changes.
|
|
64
|
+
*
|
|
65
|
+
* The function first checks if the input is an array. If so, it recursively flattens each change in the array.
|
|
66
|
+
* If the input is not an array, it checks if the change has nested changes or an embedded key.
|
|
67
|
+
* If so, it updates the path and recursively flattens the nested changes or the embedded object.
|
|
68
|
+
* If the change does not have nested changes or an embedded key, it creates a flat change and returns it in an array.
|
|
69
|
+
*/
|
|
70
|
+
export const flattenChangeset = (obj, path = '$', embeddedKey) => {
|
|
71
|
+
if (Array.isArray(obj)) {
|
|
72
|
+
return obj.reduce((memo, change) => [...memo, ...flattenChangeset(change, path, embeddedKey)], []);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
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));
|
|
83
|
+
return flattenChangeset(obj.changes || obj, path, obj.embeddedKey);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const valueType = getTypeOfObj(obj.value);
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
...obj,
|
|
90
|
+
path: valueType === 'Object' || path.endsWith(`[${obj.key}]`) ? path : append(path, obj.key),
|
|
91
|
+
valueType
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Transforms a flat changeset into a nested changeset.
|
|
99
|
+
*
|
|
100
|
+
* @param {IFlatChange | IFlatChange[]} changes - The flat changeset to unflatten.
|
|
101
|
+
* @returns {IChange[]} - The unflattened changeset.
|
|
102
|
+
*
|
|
103
|
+
* The function first checks if the input is a single change or an array of changes.
|
|
104
|
+
* It then iterates over each change and splits its path into segments.
|
|
105
|
+
* For each segment, it checks if it represents an array or a leaf node.
|
|
106
|
+
* If it represents an array, it creates a new change object and updates the pointer to this new object.
|
|
107
|
+
* If it represents a leaf node, it sets the key, type, value, and oldValue of the current change object.
|
|
108
|
+
* Finally, it pushes the unflattened change object into the changes array.
|
|
109
|
+
*/
|
|
110
|
+
export const unflattenChanges = (changes) => {
|
|
111
|
+
if (!Array.isArray(changes)) {
|
|
112
|
+
changes = [changes];
|
|
113
|
+
}
|
|
114
|
+
const changesArr = [];
|
|
115
|
+
changes.forEach((change) => {
|
|
116
|
+
const obj = {};
|
|
117
|
+
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
|
+
}, []);
|
|
126
|
+
if (segments.length === 1) {
|
|
127
|
+
ptr.key = change.key;
|
|
128
|
+
ptr.type = change.type;
|
|
129
|
+
ptr.value = change.value;
|
|
130
|
+
ptr.oldValue = change.oldValue;
|
|
131
|
+
changesArr.push(ptr);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
for (let i = 1; i < segments.length; i++) {
|
|
135
|
+
const segment = segments[i];
|
|
136
|
+
// Matches JSONPath segments: "items[?(@.id='123')]" or "items[2]"
|
|
137
|
+
const result = /^(.+)\[\?\(@\.(.+)='(.+)'\)]$|^(.+)\[(\d+)\]/.exec(segment);
|
|
138
|
+
// array
|
|
139
|
+
if (result) {
|
|
140
|
+
let key;
|
|
141
|
+
let embeddedKey;
|
|
142
|
+
let arrKey;
|
|
143
|
+
if (result[1]) {
|
|
144
|
+
key = result[1];
|
|
145
|
+
embeddedKey = result[2];
|
|
146
|
+
arrKey = result[3];
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
key = result[4];
|
|
150
|
+
embeddedKey = '$index';
|
|
151
|
+
arrKey = Number(result[5]);
|
|
152
|
+
}
|
|
153
|
+
// leaf
|
|
154
|
+
if (i === segments.length - 1) {
|
|
155
|
+
ptr.key = key;
|
|
156
|
+
ptr.embeddedKey = embeddedKey;
|
|
157
|
+
ptr.type = Operation.UPDATE;
|
|
158
|
+
ptr.changes = [
|
|
159
|
+
{
|
|
160
|
+
type: change.type,
|
|
161
|
+
key: arrKey,
|
|
162
|
+
value: change.value,
|
|
163
|
+
oldValue: change.oldValue
|
|
164
|
+
}
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// object
|
|
169
|
+
ptr.key = key;
|
|
170
|
+
ptr.embeddedKey = embeddedKey;
|
|
171
|
+
ptr.type = Operation.UPDATE;
|
|
172
|
+
const newPtr = {};
|
|
173
|
+
ptr.changes = [
|
|
174
|
+
{
|
|
175
|
+
type: Operation.UPDATE,
|
|
176
|
+
key: arrKey,
|
|
177
|
+
changes: [newPtr]
|
|
178
|
+
}
|
|
179
|
+
];
|
|
180
|
+
ptr = newPtr;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// leaf
|
|
185
|
+
if (i === segments.length - 1) {
|
|
186
|
+
// check if value is a primitive or object
|
|
187
|
+
if (change.value !== null && change.valueType === 'Object') {
|
|
188
|
+
ptr.key = segment;
|
|
189
|
+
ptr.type = Operation.UPDATE;
|
|
190
|
+
ptr.changes = [
|
|
191
|
+
{
|
|
192
|
+
key: change.key,
|
|
193
|
+
type: change.type,
|
|
194
|
+
value: change.value
|
|
195
|
+
}
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
ptr.key = change.key;
|
|
200
|
+
ptr.type = change.type;
|
|
201
|
+
ptr.value = change.value;
|
|
202
|
+
ptr.oldValue = change.oldValue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// branch
|
|
207
|
+
ptr.key = segment;
|
|
208
|
+
ptr.type = Operation.UPDATE;
|
|
209
|
+
const newPtr = {};
|
|
210
|
+
ptr.changes = [newPtr];
|
|
211
|
+
ptr = newPtr;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
changesArr.push(obj);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return changesArr;
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Determines the type of a given object.
|
|
222
|
+
*
|
|
223
|
+
* @param {any} obj - The object whose type is to be determined.
|
|
224
|
+
* @returns {string | null} - The type of the object, or null if the object is null.
|
|
225
|
+
*
|
|
226
|
+
* This function first checks if the object is undefined or null, and returns 'undefined' or null respectively.
|
|
227
|
+
* If the object is neither undefined nor null, it uses Object.prototype.toString to get the object's type.
|
|
228
|
+
* The type is extracted from the string returned by Object.prototype.toString using a regular expression.
|
|
229
|
+
*/
|
|
230
|
+
export const getTypeOfObj = (obj) => {
|
|
6
231
|
if (typeof obj === 'undefined') {
|
|
7
232
|
return 'undefined';
|
|
8
233
|
}
|
|
9
234
|
if (obj === null) {
|
|
10
235
|
return null;
|
|
11
236
|
}
|
|
237
|
+
// Extracts the "Type" from "[object Type]" string.
|
|
12
238
|
return Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1];
|
|
13
239
|
};
|
|
14
|
-
exports.getTypeOfObj = getTypeOfObj;
|
|
15
240
|
const getKey = (path) => {
|
|
16
241
|
const left = path[path.length - 1];
|
|
17
242
|
return left != null ? left : '$root';
|
|
18
243
|
};
|
|
19
244
|
const compare = (oldObj, newObj, path, embeddedObjKeys, keyPath) => {
|
|
20
245
|
let changes = [];
|
|
21
|
-
const typeOfOldObj =
|
|
22
|
-
const typeOfNewObj =
|
|
246
|
+
const typeOfOldObj = getTypeOfObj(oldObj);
|
|
247
|
+
const typeOfNewObj = getTypeOfObj(newObj);
|
|
23
248
|
// if type of object changes, consider it as old obj has been deleted and a new object has been added
|
|
24
249
|
if (typeOfOldObj !== typeOfNewObj) {
|
|
25
250
|
changes.push({ type: Operation.REMOVE, key: getKey(path), value: oldObj });
|
|
@@ -28,7 +253,11 @@ const compare = (oldObj, newObj, path, embeddedObjKeys, keyPath) => {
|
|
|
28
253
|
}
|
|
29
254
|
switch (typeOfOldObj) {
|
|
30
255
|
case 'Date':
|
|
31
|
-
changes = changes.concat(comparePrimitives(oldObj.getTime(), newObj.getTime(), path).map((x) => (
|
|
256
|
+
changes = changes.concat(comparePrimitives(oldObj.getTime(), newObj.getTime(), path).map((x) => ({
|
|
257
|
+
...x,
|
|
258
|
+
value: new Date(x.value),
|
|
259
|
+
oldValue: new Date(x.oldValue)
|
|
260
|
+
})));
|
|
32
261
|
break;
|
|
33
262
|
case 'Object':
|
|
34
263
|
const diffs = compareObject(oldObj, newObj, path, embeddedObjKeys, keyPath);
|
|
@@ -66,7 +295,7 @@ const compareObject = (oldObj, newObj, path, embeddedObjKeys, keyPath, skipPath
|
|
|
66
295
|
let changes = [];
|
|
67
296
|
const oldObjKeys = Object.keys(oldObj);
|
|
68
297
|
const newObjKeys = Object.keys(newObj);
|
|
69
|
-
const intersectionKeys =
|
|
298
|
+
const intersectionKeys = intersection(oldObjKeys, newObjKeys);
|
|
70
299
|
for (k of intersectionKeys) {
|
|
71
300
|
newPath = path.concat([k]);
|
|
72
301
|
newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
|
|
@@ -75,7 +304,7 @@ const compareObject = (oldObj, newObj, path, embeddedObjKeys, keyPath, skipPath
|
|
|
75
304
|
changes = changes.concat(diffs);
|
|
76
305
|
}
|
|
77
306
|
}
|
|
78
|
-
const addedKeys =
|
|
307
|
+
const addedKeys = difference(newObjKeys, oldObjKeys);
|
|
79
308
|
for (k of addedKeys) {
|
|
80
309
|
newPath = path.concat([k]);
|
|
81
310
|
newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
|
|
@@ -85,7 +314,7 @@ const compareObject = (oldObj, newObj, path, embeddedObjKeys, keyPath, skipPath
|
|
|
85
314
|
value: newObj[k]
|
|
86
315
|
});
|
|
87
316
|
}
|
|
88
|
-
const deletedKeys =
|
|
317
|
+
const deletedKeys = difference(oldObjKeys, newObjKeys);
|
|
89
318
|
for (k of deletedKeys) {
|
|
90
319
|
newPath = path.concat([k]);
|
|
91
320
|
newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
|
|
@@ -120,22 +349,29 @@ const compareArray = (oldObj, newObj, path, embeddedObjKeys, keyPath) => {
|
|
|
120
349
|
const getObjectKey = (embeddedObjKeys, keyPath) => {
|
|
121
350
|
if (embeddedObjKeys != null) {
|
|
122
351
|
const path = keyPath.join('.');
|
|
352
|
+
if (embeddedObjKeys instanceof Map) {
|
|
353
|
+
for (const [key, value] of embeddedObjKeys.entries()) {
|
|
354
|
+
if (key instanceof RegExp) {
|
|
355
|
+
if (path.match(key)) {
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (path === key) {
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
123
364
|
const key = embeddedObjKeys[path];
|
|
124
365
|
if (key != null) {
|
|
125
366
|
return key;
|
|
126
367
|
}
|
|
127
|
-
for (const regex in embeddedObjKeys) {
|
|
128
|
-
if (path.match(new RegExp(regex))) {
|
|
129
|
-
return embeddedObjKeys[regex];
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
368
|
}
|
|
133
369
|
return undefined;
|
|
134
370
|
};
|
|
135
371
|
const convertArrayToObj = (arr, uniqKey) => {
|
|
136
372
|
let obj = {};
|
|
137
373
|
if (uniqKey !== '$index') {
|
|
138
|
-
obj =
|
|
374
|
+
obj = keyBy(arr, uniqKey);
|
|
139
375
|
}
|
|
140
376
|
else {
|
|
141
377
|
for (let i = 0; i < arr.length; i++) {
|
|
@@ -157,7 +393,6 @@ const comparePrimitives = (oldObj, newObj, path) => {
|
|
|
157
393
|
}
|
|
158
394
|
return changes;
|
|
159
395
|
};
|
|
160
|
-
// const isEmbeddedKey = key => /\$.*=/gi.test(key)
|
|
161
396
|
const removeKey = (obj, key, embeddedKey) => {
|
|
162
397
|
if (Array.isArray(obj)) {
|
|
163
398
|
if (embeddedKey === '$index') {
|
|
@@ -219,9 +454,9 @@ const applyArrayChange = (arr, change) => (() => {
|
|
|
219
454
|
element = arr[subchange.key];
|
|
220
455
|
}
|
|
221
456
|
else {
|
|
222
|
-
element =
|
|
457
|
+
element = find(arr, (el) => el[change.embeddedKey].toString() === subchange.key.toString());
|
|
223
458
|
}
|
|
224
|
-
result.push(
|
|
459
|
+
result.push(applyChangeset(element, subchange.changes));
|
|
225
460
|
}
|
|
226
461
|
}
|
|
227
462
|
return result;
|
|
@@ -231,7 +466,7 @@ const applyBranchChange = (obj, change) => {
|
|
|
231
466
|
return applyArrayChange(obj, change);
|
|
232
467
|
}
|
|
233
468
|
else {
|
|
234
|
-
return
|
|
469
|
+
return applyChangeset(obj, change.changes);
|
|
235
470
|
}
|
|
236
471
|
};
|
|
237
472
|
const revertLeafChange = (obj, change, embeddedKey = '$index') => {
|
|
@@ -257,9 +492,9 @@ const revertArrayChange = (arr, change) => (() => {
|
|
|
257
492
|
element = arr[+subchange.key];
|
|
258
493
|
}
|
|
259
494
|
else {
|
|
260
|
-
element =
|
|
495
|
+
element = find(arr, (el) => el[change.embeddedKey].toString() === subchange.key);
|
|
261
496
|
}
|
|
262
|
-
result.push(
|
|
497
|
+
result.push(revertChangeset(element, subchange.changes));
|
|
263
498
|
}
|
|
264
499
|
}
|
|
265
500
|
return result;
|
|
@@ -269,182 +504,12 @@ const revertBranchChange = (obj, change) => {
|
|
|
269
504
|
return revertArrayChange(obj, change);
|
|
270
505
|
}
|
|
271
506
|
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));
|
|
291
|
-
}
|
|
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];
|
|
507
|
+
return revertChangeset(obj, change.changes);
|
|
330
508
|
}
|
|
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
509
|
};
|
|
442
|
-
exports.unflattenChanges = unflattenChanges;
|
|
443
510
|
/** combine a base JSON Path with a subsequent segment */
|
|
444
511
|
function append(basePath, nextSegment) {
|
|
445
|
-
return nextSegment.includes('.')
|
|
446
|
-
? `${basePath}[${nextSegment}]`
|
|
447
|
-
: `${basePath}.${nextSegment}`;
|
|
512
|
+
return nextSegment.includes('.') ? `${basePath}[${nextSegment}]` : `${basePath}.${nextSegment}`;
|
|
448
513
|
}
|
|
449
514
|
/** returns a JSON Path filter expression; e.g., `$.pet[(?name='spot')]` */
|
|
450
515
|
function filterExpression(basePath, filterKey, filterValue) {
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "json-diff-ts",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A diff tool for JavaScript
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "A JSON diff tool for JavaScript written in TypeScript.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"scripts": {
|
|
8
9
|
"build": "tsc",
|
|
9
|
-
"format": "prettier --write \"src/**/*.ts\"
|
|
10
|
-
"lint": "
|
|
11
|
-
"test": "jest --config jest.config.
|
|
12
|
-
"test:watch": "jest --watch --config jest.config.
|
|
10
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
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",
|
|
13
14
|
"prepare": "npm run build",
|
|
14
15
|
"prepublishOnly": "npm test && npm run lint",
|
|
15
16
|
"preversion": "npm run lint",
|
|
@@ -37,16 +38,14 @@
|
|
|
37
38
|
},
|
|
38
39
|
"homepage": "https://github.com/ltwlf/json-diff-ts#readme",
|
|
39
40
|
"devDependencies": {
|
|
40
|
-
"@
|
|
41
|
-
"@types/
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
"peerDependencies": {
|
|
50
|
-
"lodash": "^4.x"
|
|
41
|
+
"@jest/globals": "^29.7.0",
|
|
42
|
+
"@types/jest": "^29.5.7",
|
|
43
|
+
"@types/lodash-es": "^4.17.10",
|
|
44
|
+
"eslint": "^8.53.0",
|
|
45
|
+
"jest": "^29.5.0",
|
|
46
|
+
"lodash-es": "^4.17.21",
|
|
47
|
+
"prettier": "^3.0.3",
|
|
48
|
+
"ts-jest": "^29.0.5",
|
|
49
|
+
"typescript": "^4.9.5"
|
|
51
50
|
}
|
|
52
|
-
}
|
|
51
|
+
}
|