Skip to content

Commit 98d7884

Browse files
authored
chore: update readme with more config examples (#18)
[Rendered Readme](https://github.com/marcolink/generate-json-patch/blob/chore/update-readme/README.md)
1 parent a41a4bd commit 98d7884

File tree

1 file changed

+198
-62
lines changed

1 file changed

+198
-62
lines changed

README.md

Lines changed: 198 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ Create [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902/) compliant JSON
1212
- Can diff any two [JSON](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/) compliant objects - returns differences as [JSON Patch](http://jsonpatch.com/).
1313
- Elegant array diffing by providing an `objectHash` to match array elements
1414
- Ignore specific keys by providing a `propertyFilter`
15+
- LCS-based move detection (or disable moves with `array.ignoreMove`)
16+
- Limit traversal with `maxDepth` to collapse deep trees into a single replace
1517
- :paw_prints: ***Is it small?*** Zero dependencies - it's ~**3 KB** (minified).
18+
- Ships ESM + CJS builds with types
1619
- :crystal_ball: ***Is it fast?*** I haven't done any performance comparison yet.
17-
- :hatched_chick: ***Is it stable?*** Test coverage is high, but it's still in its early days - bugs are expected.
1820
- The interface is inspired by [jsondiffpatch](https://github.com/benjamine/jsondiffpatch)
1921
- **100%** Typescript
2022

@@ -29,89 +31,223 @@ npm install generate-json-patch
2931
```typescript
3032
import { generateJSONPatch } from 'generate-json-patch';
3133

32-
const before = { manufacturer: "Ford", type: "Granada", year: 1972 };
33-
const after = { manufacturer: "Ford", type: "Granada", year: 1974 };
34+
const before = {
35+
manufacturer: 'Ford',
36+
model: 'Granada',
37+
year: 1972,
38+
colors: ['red', 'silver', 'yellow'],
39+
engine: [
40+
{ name: 'Cologne V6 2.6', hp: 125 },
41+
{ name: 'Cologne V6 2.0', hp: 90 },
42+
{ name: 'Cologne V6 2.3', hp: 108 },
43+
{ name: 'Essex V6 3.0', hp: 138 },
44+
],
45+
};
46+
47+
const after = {
48+
manufacturer: 'Ford',
49+
model: 'Granada',
50+
year: 1974,
51+
colors: ['red', 'silver', 'yellow'],
52+
engine: [
53+
{ name: 'Essex V6 3.0', hp: 138 },
54+
{ name: 'Cologne V6 2.6', hp: 125 },
55+
{ name: 'Cologne V6 2.3', hp: 108 },
56+
{ name: 'Cologne V6 2.0', hp: 90 },
57+
],
58+
};
3459

3560
const patch = generateJSONPatch(before, after);
3661

37-
console.log(patch) // => [{op: 'replace', path: '/year', value: 1974}]
62+
console.log(patch);
63+
// [
64+
// { op: 'replace', path: '/year', value: 1974 },
65+
// { op: 'move', from: '/engine/3', path: '/engine/0' },
66+
// ]
3867
```
3968

4069
## Configuration
4170

71+
`generateJSONPatch(before, after, config?)` accepts the options below. The examples reuse the same payload shown in the Usage section.
72+
73+
### `objectHash`
74+
75+
Match array elements by a stable hash instead of position. Useful to detect moves and edits for arrays of objects.
76+
4277
```typescript
43-
import { generateJSONPatch, JsonPatchConfig, JsonValue, ObjectHashContext } from 'generate-json-patch';
44-
45-
generateJSONPatch({/*...*/}, {/*...*/}, {
46-
// called when comparing array elements
47-
objectHash: function(value: JsonValue, context: GeneratePatchContext) {
48-
// for arrays of primitive values like string and numbers, a stringification is sufficent:
49-
// return JSON.stringify(value)
50-
// If we know the shape of the value, we can match be specific properties
51-
return value.name
52-
},
53-
// called for every property on objects. Can be used to ignore sensitive or irrelevant
54-
// properties when comparing data.
55-
propertyFilter: function (propertyName: string, context: ObjectHashContext) {
56-
return !['sensitiveProperty'].includes(propertyName);
57-
},
58-
array: {
59-
// When true, no move operations will be created.
60-
// The rersulting patch will not lead to identical objects,
61-
// as postions of array elements can be different!
62-
ignoreMove: true
78+
import { generateJSONPatch, type JsonValue, type ObjectHashContext, pathInfo } from 'generate-json-patch';
79+
80+
const before = {
81+
manufacturer: 'Ford',
82+
model: 'Granada',
83+
year: 1972,
84+
colors: ['red', 'silver', 'yellow'],
85+
engine: [
86+
{ name: 'Cologne V6 2.6', hp: 125 },
87+
{ name: 'Cologne V6 2.0', hp: 90 },
88+
{ name: 'Cologne V6 2.3', hp: 108 },
89+
{ name: 'Essex V6 3.0', hp: 138 },
90+
],
91+
};
92+
93+
const after = {
94+
manufacturer: 'Ford',
95+
model: 'Granada',
96+
year: 1974,
97+
colors: ['red', 'silver', 'yellow'],
98+
engine: [
99+
{ name: 'Essex V6 3.0', hp: 138 },
100+
{ name: 'Cologne V6 2.6', hp: 125 },
101+
{ name: 'Cologne V6 2.3', hp: 108 },
102+
{ name: 'Cologne V6 2.0', hp: 90 },
103+
],
104+
};
105+
106+
const patch = generateJSONPatch(before, after, {
107+
objectHash(value: JsonValue, context: ObjectHashContext) {
108+
const { length, last } = pathInfo(context.path);
109+
if (length === 2 && last === 'engine') {
110+
// keep engine comparisons stable by model name
111+
// @ts-expect-error JsonValue does not guarantee shape
112+
return value?.name;
63113
}
114+
// default to position for other arrays
115+
return context.index.toString();
116+
},
64117
});
65-
```
66118

67-
### Patch Context
68-
Both config function (`objectHash`, `propertyFilter`), receive a context as second parameter.
69-
This allows for granular decision-making on the provided data.
119+
console.log(patch);
120+
// [
121+
// { op: 'replace', path: '/year', value: 1974 },
122+
// { op: 'move', from: '/engine/3', path: '/engine/0' },
123+
// ]
124+
```
70125

71-
#### Example
72-
```typescript
73-
import {generateJSONPatch, JsonPatchConfig, JsonValue, ObjectHashContext, pathInfo} from 'generate-json-patch';
126+
### `propertyFilter`
127+
128+
Skip properties when diffing. Return `false` to ignore a field.
74129

130+
```typescript
75131
const before = {
76-
manufacturer: "Ford",
77-
type: "Granada",
78-
colors: ['red', 'silver', 'yellow'],
79-
engine: [
80-
{ name: 'Cologne V6 2.6', hp: 125 },
81-
{ name: 'Cologne V6 2.0', hp: 90 },
82-
{ name: 'Cologne V6 2.3', hp: 108 },
83-
{ name: 'Essex V6 3.0', hp: 138 },
84-
]
85-
}
132+
manufacturer: 'Ford',
133+
model: 'Granada',
134+
year: 1972,
135+
vin: 'secret-123',
136+
};
86137

87138
const after = {
88-
manufacturer: "Ford",
89-
type: "Granada",
90-
colors: ['red', 'silver', 'yellow'],
91-
engine: [
92-
{name: 'Essex V6 3.0', hp: 138},
93-
{name: 'Cologne V6 2.6', hp: 125},
94-
{name: 'Cologne V6 2.0', hp: 90},
95-
{name: 'Cologne V6 2.3', hp: 108},
96-
]
97-
}
139+
manufacturer: 'Ford',
140+
model: 'Granada',
141+
year: 1974,
142+
vin: 'secret-456',
143+
};
98144

99145
const patch = generateJSONPatch(before, after, {
100-
objectHash: function (value: JsonValue, context: ObjectHashContext) {
101-
const {length, last} = pathInfo(context.path)
102-
if (length === 2 && last === 'engine') {
103-
return value.name
104-
}
105-
return JSON.stringify(value)
106-
}
146+
propertyFilter(propertyName) {
147+
return propertyName !== 'vin';
148+
},
107149
});
108150

109-
console.log(patch) // => [
110-
// { op: 'replace', path: '/engine/3/hp', value: 138 },
111-
// { op: 'move', from: '/engine/3', path: '/engine/0' }
151+
console.log(patch);
152+
// [
153+
// { op: 'replace', path: '/year', value: 1974 }
112154
// ]
113155
```
114156

115-
> For more examples, check out the [tests](./src/index.spec.ts)
157+
### `array.ignoreMove`
158+
159+
Prevent move operations if order does not matter to you. The resulting patch will not reorder arrays.
160+
161+
```typescript
162+
const before = {
163+
manufacturer: 'Ford',
164+
model: 'Granada',
165+
year: 1972,
166+
engine: [
167+
{ name: 'Cologne V6 2.6', hp: 125 },
168+
{ name: 'Cologne V6 2.0', hp: 90 },
169+
{ name: 'Cologne V6 2.3', hp: 108 },
170+
{ name: 'Essex V6 3.0', hp: 138 },
171+
],
172+
};
173+
174+
const after = {
175+
manufacturer: 'Ford',
176+
model: 'Granada',
177+
year: 1972,
178+
engine: [
179+
{ name: 'Essex V6 3.0', hp: 138 },
180+
{ name: 'Cologne V6 2.6', hp: 125 },
181+
{ name: 'Cologne V6 2.3', hp: 108 },
182+
{ name: 'Cologne V6 2.0', hp: 90 },
183+
],
184+
};
185+
186+
const unorderedPatch = generateJSONPatch(before, after, {
187+
objectHash: (value: any) => value.name,
188+
array: { ignoreMove: true },
189+
});
190+
191+
console.log(unorderedPatch);
192+
// []
193+
```
194+
195+
### `maxDepth`
196+
197+
Stop descending deeper than a given path depth. When the limit is reached, a `replace` is emitted for that subtree.
198+
199+
```typescript
200+
const before = {
201+
manufacturer: 'Ford',
202+
model: 'Granada',
203+
year: 1972,
204+
specs: {
205+
trim: 'Base',
206+
colorOptions: ['red', 'silver', 'yellow'],
207+
},
208+
};
209+
210+
const after = {
211+
manufacturer: 'Ford',
212+
model: 'Granada',
213+
year: 1974,
214+
specs: {
215+
trim: 'Ghia',
216+
colorOptions: ['red', 'silver', 'yellow'],
217+
},
218+
};
219+
220+
const patch = generateJSONPatch(before, after, { maxDepth: 2 });
221+
222+
console.log(patch);
223+
// [
224+
// {
225+
// op: 'replace',
226+
// path: '/specs',
227+
// value: { trim: 'Ghia', colorOptions: ['red', 'silver', 'yellow'] },
228+
// },
229+
// { op: 'replace', path: '/year', value: 1974 },
230+
// ]
231+
```
232+
233+
### Patch Context
234+
235+
Both config functions (`objectHash`, `propertyFilter`) receive a context as the second parameter to drive fine-grained decisions:
116236

237+
- `side`: `'left' | 'right'` indicating the value being inspected
238+
- `path`: JSON Pointer-style path to the current value
239+
- `index`: only on `objectHash`, giving the array index being processed
117240

241+
See the `objectHash` example above for how `pathInfo` can be combined with the context to scope hashing logic.
242+
243+
### How moves are found (Longest Common Subsequence)
244+
245+
When `ignoreMove` is `false`, array reorders emit move operations instead of delete/add pairs. We minimize moves by:
246+
247+
1. Hashing array elements with `objectHash` to get stable identifiers.
248+
2. Computing the **[Longest Common Subsequence (LCS)](https://en.wikipedia.org/wiki/Longest_common_subsequence)** between the current order and the target order. The LCS represents items that stay in place.
249+
3. Walking the target order and moving only the out-of-place items, keeping LCS items anchored. This yields the smallest set of `{ op: 'move', from, path }` operations needed to reach the target sequence.
250+
251+
This is implemented in `move-operations.ts` (`longestCommonSequence` + `moveOperations`) and is exercised in the tests in `src/move-operations.spec.ts`.
252+
253+
> For more examples, check out the [tests](./src/index.spec.ts)

0 commit comments

Comments
 (0)