Skip to content

Commit 128034e

Browse files
committed
fix(maxDepth): do not return a replace operation for identical array hashes
1 parent 5906b87 commit 128034e

File tree

2 files changed

+103
-66
lines changed

2 files changed

+103
-66
lines changed

src/index.spec.ts

Lines changed: 101 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { JsonValue, ObjectHashContext, Patch } from './index';
2-
import { generateJSONPatch, pathInfo } from './index';
3-
import { applyPatch, deepClone } from 'fast-json-patch';
4-
import { assert, expect } from 'chai';
1+
import type {JsonValue, ObjectHashContext, Patch} from './index';
2+
import {generateJSONPatch, pathInfo} from './index';
3+
import {applyPatch, deepClone} from 'fast-json-patch';
4+
import {assert, expect} from 'chai';
55

66
type Title = string;
77
type Before = JsonValue;
@@ -18,9 +18,9 @@ const jsonValues = {
1818
primitiveNumberZero: 0,
1919
primitiveBooleanTrue: true,
2020
primitiveBooleanFalse: false,
21-
jsonObjectWithFlatPropertiesAndStringValues: { a: 'a', b: 'b', c: 'c' },
22-
jsonObjectWithFlatPropertiesAndNumberValues: { a: 3, b: 2, c: 1 },
23-
jsonObjectWithFlatPropertiesAndMixedValues: { a: true, b: 'b', c: 12 },
21+
jsonObjectWithFlatPropertiesAndStringValues: {a: 'a', b: 'b', c: 'c'},
22+
jsonObjectWithFlatPropertiesAndNumberValues: {a: 3, b: 2, c: 1},
23+
jsonObjectWithFlatPropertiesAndMixedValues: {a: true, b: 'b', c: 12},
2424
} as const;
2525

2626
describe('a generate json patch function', () => {
@@ -47,12 +47,12 @@ describe('a generate json patch function', () => {
4747
'adds root array elements',
4848
[1, 2, 3],
4949
[1, 2, 3, 4],
50-
[{ op: 'add', path: '/3', value: 4 }],
50+
[{op: 'add', path: '/3', value: 4}],
5151
],
5252
[
5353
'adds root object property',
54-
{ a: 'a', b: 'b' },
55-
{ a: 'a', b: 'b', c: 'c' },
54+
{a: 'a', b: 'b'},
55+
{a: 'a', b: 'b', c: 'c'},
5656
[
5757
{
5858
op: 'add',
@@ -65,38 +65,38 @@ describe('a generate json patch function', () => {
6565
'removes root array elements',
6666
[1, 2, 3, 4],
6767
[1, 2, 3],
68-
[{ op: 'remove', path: '/3' }],
68+
[{op: 'remove', path: '/3'}],
6969
],
7070
[
7171
'removes root object property',
72-
{ a: 'a', b: 'b', c: 'c' },
73-
{ a: 'a', b: 'b' },
74-
[{ op: 'remove', path: '/c' }],
72+
{a: 'a', b: 'b', c: 'c'},
73+
{a: 'a', b: 'b'},
74+
[{op: 'remove', path: '/c'}],
7575
],
7676
[
7777
'replaces root number values',
7878
1,
7979
2,
80-
[{ op: 'replace', path: '', value: 2 }],
80+
[{op: 'replace', path: '', value: 2}],
8181
],
8282
[
8383
'replaces root string values',
8484
'hello',
8585
'world',
86-
[{ op: 'replace', path: '', value: 'world' }],
86+
[{op: 'replace', path: '', value: 'world'}],
8787
],
8888
[
8989
'replaces root boolean values',
9090
true,
9191
false,
92-
[{ op: 'replace', path: '', value: false }],
92+
[{op: 'replace', path: '', value: false}],
9393
],
9494

9595
['replaces root empty arrays', [], [], []],
9696
[
9797
'replaces root object property',
98-
{ a: 'a', b: 'b' },
99-
{ a: 'a', b: 'c' },
98+
{a: 'a', b: 'b'},
99+
{a: 'a', b: 'c'},
100100
[
101101
{
102102
op: 'replace',
@@ -109,12 +109,12 @@ describe('a generate json patch function', () => {
109109
'replaces root array elements',
110110
[1, 2, 3],
111111
[1, 2, 4],
112-
[{ op: 'replace', path: '/2', value: 4 }],
112+
[{op: 'replace', path: '/2', value: 4}],
113113
],
114114
[
115115
'replaces an obj prop with an array property',
116-
{ prop: { hello: 'world' } },
117-
{ prop: ['hello', 'world'] },
116+
{prop: {hello: 'world'}},
117+
{prop: ['hello', 'world']},
118118
[
119119
{
120120
op: 'replace',
@@ -125,20 +125,20 @@ describe('a generate json patch function', () => {
125125
],
126126
[
127127
'replaces an array prop with an obj property',
128-
{ prop: ['hello', 'world'] },
129-
{ prop: { hello: 'world' } },
128+
{prop: ['hello', 'world']},
129+
{prop: {hello: 'world'}},
130130
[
131131
{
132132
op: 'replace',
133133
path: '/prop',
134-
value: { hello: 'world' },
134+
value: {hello: 'world'},
135135
},
136136
],
137137
],
138138
[
139139
'replaces a deep nested object property',
140-
{ root: { first: { second: { third: 'before' } } } },
141-
{ root: { first: { second: { third: 'after' } } } },
140+
{root: {first: {second: {third: 'before'}}}},
141+
{root: {first: {second: {third: 'after'}}}},
142142
[
143143
{
144144
op: 'replace',
@@ -153,13 +153,13 @@ describe('a generate json patch function', () => {
153153
root: {
154154
first: [
155155
{},
156-
{ second: { third: 'before', list: ['hello', 'world'] } },
156+
{second: {third: 'before', list: ['hello', 'world']}},
157157
],
158158
},
159159
},
160160
{
161161
root: {
162-
first: [{}, { second: { third: 'after', list: ['hello', 'world'] } }],
162+
first: [{}, {second: {third: 'after', list: ['hello', 'world']}}],
163163
},
164164
},
165165
[
@@ -172,8 +172,8 @@ describe('a generate json patch function', () => {
172172
],
173173
[
174174
'detects several changes on arrays by reference',
175-
{ root: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }] },
176-
{ root: [{ id: 4 }, { id: 3 }, { id: 2 }] },
175+
{root: [{id: 1}, {id: 2}, {id: 3}, {id: 4}]},
176+
{root: [{id: 4}, {id: 3}, {id: 2}]},
177177
[
178178
{
179179
op: 'remove',
@@ -210,8 +210,8 @@ describe('a generate json patch function', () => {
210210

211211
describe('with an array value hash function', () => {
212212
it('throws when objectHash is not a function', () => {
213-
const before = [{ id: 1, paramOne: 'before' }];
214-
const after = [{ id: 2, paramOne: 'after' }];
213+
const before = [{id: 1, paramOne: 'before'}];
214+
const after = [{id: 2, paramOne: 'after'}];
215215

216216
assert.throws(() =>
217217
generateJSONPatch(before, after, {
@@ -223,12 +223,12 @@ describe('a generate json patch function', () => {
223223

224224
it('handles changes with change and move on the same property', () => {
225225
const before = [
226-
{ id: 1, paramOne: 'future', paramTwo: 'past' },
227-
{ id: 2, paramOne: 'current' },
226+
{id: 1, paramOne: 'future', paramTwo: 'past'},
227+
{id: 2, paramOne: 'current'},
228228
];
229229
const after = [
230-
{ id: 2, paramOne: 'current' },
231-
{ id: 1, paramOne: 'current' },
230+
{id: 2, paramOne: 'current'},
231+
{id: 1, paramOne: 'current'},
232232
];
233233

234234
const patch = generateJSONPatch(before, after, {
@@ -239,19 +239,19 @@ describe('a generate json patch function', () => {
239239

240240
const patched = doPatch(before, patch);
241241
expect(patched).to.be.eql([
242-
{ id: 2, paramOne: 'current' },
243-
{ id: 1, paramOne: 'current' },
242+
{id: 2, paramOne: 'current'},
243+
{id: 1, paramOne: 'current'},
244244
]);
245245
});
246246

247247
it('handles changes on array objects with different shape', () => {
248-
const before = [{ id: 1, paramOne: 'current' }];
248+
const before = [{id: 1, paramOne: 'current'}];
249249
const after = [
250250
{
251251
id: 1,
252252
paramOne: 'future',
253253
paramTwo: 'past',
254-
paramThree: { nested: 'some text' },
254+
paramThree: {nested: 'some text'},
255255
},
256256
];
257257

@@ -268,7 +268,7 @@ describe('a generate json patch function', () => {
268268
id: 1,
269269
paramOne: 'future',
270270
paramTwo: 'past',
271-
paramThree: { nested: 'some text' },
271+
paramThree: {nested: 'some text'},
272272
},
273273
]);
274274
});
@@ -487,7 +487,7 @@ describe('a generate json patch function', () => {
487487
objectHash: function (obj: any) {
488488
return `${obj.id}`;
489489
},
490-
array: { ignoreMove: true },
490+
array: {ignoreMove: true},
491491
});
492492

493493
const patched = doPatch(before, patch);
@@ -514,10 +514,10 @@ describe('a generate json patch function', () => {
514514
type: 'Granada',
515515
colors: ['red', 'silver', 'yellow'],
516516
engine: [
517-
{ name: 'Cologne V6 2.6', hp: 125 },
518-
{ name: 'Cologne V6 2.0', hp: 90 },
519-
{ name: 'Cologne V6 2.3', hp: 108 },
520-
{ name: 'Essex V6 3.0', hp: 150 },
517+
{name: 'Cologne V6 2.6', hp: 125},
518+
{name: 'Cologne V6 2.0', hp: 90},
519+
{name: 'Cologne V6 2.3', hp: 108},
520+
{name: 'Essex V6 3.0', hp: 150},
521521
],
522522
};
523523

@@ -526,16 +526,16 @@ describe('a generate json patch function', () => {
526526
type: 'Granada',
527527
colors: ['red', 'silver', 'yellow'],
528528
engine: [
529-
{ name: 'Essex V6 3.0', hp: 138 },
530-
{ name: 'Cologne V6 2.6', hp: 125 },
531-
{ name: 'Cologne V6 2.0', hp: 90 },
532-
{ name: 'Cologne V6 2.3', hp: 108 },
529+
{name: 'Essex V6 3.0', hp: 138},
530+
{name: 'Cologne V6 2.6', hp: 125},
531+
{name: 'Cologne V6 2.0', hp: 90},
532+
{name: 'Cologne V6 2.3', hp: 108},
533533
],
534534
};
535535

536536
const patch = generateJSONPatch(before, after, {
537537
objectHash: function (value: JsonValue, context: ObjectHashContext) {
538-
const { length, last } = pathInfo(context.path);
538+
const {length, last} = pathInfo(context.path);
539539
if (length === 2 && last === 'engine') {
540540
// @ts-ignore
541541
return value?.name;
@@ -548,8 +548,8 @@ describe('a generate json patch function', () => {
548548
expect(patched).to.be.eql(after);
549549

550550
expect(patch).to.be.eql([
551-
{ op: 'replace', path: '/engine/3/hp', value: 138 },
552-
{ op: 'move', from: '/engine/3', path: '/engine/0' },
551+
{op: 'replace', path: '/engine/3/hp', value: 138},
552+
{op: 'move', from: '/engine/3', path: '/engine/0'},
553553
]);
554554
});
555555
});
@@ -584,7 +584,7 @@ describe('a generate json patch function', () => {
584584
expect(patched).to.be.eql({
585585
id: 1,
586586
paramOne: 'after',
587-
paramTwo: { ignoreMe: 'before', doNotIgnoreMe: 'after' },
587+
paramTwo: {ignoreMe: 'before', doNotIgnoreMe: 'after'},
588588
});
589589
});
590590

@@ -627,14 +627,14 @@ describe('a generate json patch function', () => {
627627
paramTwo: {
628628
ignoreMe: 'before',
629629
doNotIgnoreMe: 'after',
630-
two: { ignoreMe: 'after' },
630+
two: {ignoreMe: 'after'},
631631
},
632632
});
633633

634634
expect(patch).to.eql([
635-
{ op: 'replace', path: '/paramOne', value: 'after' },
636-
{ op: 'replace', path: '/paramTwo/doNotIgnoreMe', value: 'after' },
637-
{ op: 'replace', path: '/paramTwo/two/ignoreMe', value: 'after' },
635+
{op: 'replace', path: '/paramOne', value: 'after'},
636+
{op: 'replace', path: '/paramTwo/doNotIgnoreMe', value: 'after'},
637+
{op: 'replace', path: '/paramTwo/two/ignoreMe', value: 'after'},
638638
]);
639639
});
640640

@@ -648,8 +648,8 @@ describe('a generate json patch function', () => {
648648
expect(patched).to.be.eql([1]);
649649

650650
expect(patch).to.eql([
651-
{ op: 'remove', path: '/2' },
652-
{ op: 'remove', path: '/1' },
651+
{op: 'remove', path: '/2'},
652+
{op: 'remove', path: '/1'},
653653
]);
654654
});
655655
});
@@ -680,7 +680,7 @@ describe('a generate json patch function', () => {
680680
};
681681

682682
it('detects changes as a given depth of 3', () => {
683-
const patch = generateJSONPatch(before, after, { maxDepth: 3 });
683+
const patch = generateJSONPatch(before, after, {maxDepth: 3});
684684
expect(patch).to.eql([
685685
{
686686
op: 'replace',
@@ -696,10 +696,45 @@ describe('a generate json patch function', () => {
696696
]);
697697
});
698698

699+
it('creates empty patch for arrays with object hash', () => {
700+
const before = {
701+
obj: {
702+
arrayField: [{nested: {id: 'one', value: 'hello'}}, {
703+
nested: {
704+
id: 'two',
705+
value: 'world'
706+
}
707+
}]
708+
}
709+
};
710+
const after = {
711+
obj: {
712+
arrayField: [{nested: {value: 'hello', id: 'one',}}, {
713+
nested: {
714+
id: 'two',
715+
value: 'world'
716+
}
717+
}]
718+
}
719+
};
720+
721+
const patch = generateJSONPatch(before, after, {
722+
maxDepth: 3,
723+
objectHash: function (obj, context) {
724+
if (context.path === '/obj/arrayField') {
725+
// @ts-ignore
726+
return obj.nested.id;
727+
}
728+
return context.index.toString();
729+
},
730+
});
731+
expect(patch).to.eql([]);
732+
});
733+
699734
it('detects changes as a given depth of 4', () => {
700735
const afterModified = structuredClone(after);
701736
afterModified.firstLevel.secondLevel.thirdLevelTwo = 'hello-world';
702-
const patch = generateJSONPatch(before, afterModified, { maxDepth: 4 });
737+
const patch = generateJSONPatch(before, afterModified, {maxDepth: 4});
703738
expect(patch).to.eql([
704739
{
705740
op: 'replace',
@@ -719,7 +754,7 @@ describe('a generate json patch function', () => {
719754
it('detects changes as a given depth of 4 for an array value', () => {
720755
const afterModified = structuredClone(before);
721756
afterModified.firstLevel.secondLevel.thirdLevelThree = ['test'];
722-
const patch = generateJSONPatch(before, afterModified, { maxDepth: 4 });
757+
const patch = generateJSONPatch(before, afterModified, {maxDepth: 4});
723758
expect(patch).to.eql([
724759
{
725760
op: 'replace',
@@ -733,7 +768,7 @@ describe('a generate json patch function', () => {
733768
const afterModified = structuredClone(before);
734769
// @ts-ignore
735770
delete afterModified.firstLevel.secondLevel.thirdLevelThree;
736-
const patch = generateJSONPatch(before, afterModified, { maxDepth: 4 });
771+
const patch = generateJSONPatch(before, afterModified, {maxDepth: 4});
737772
expect(patch).to.eql([
738773
{
739774
op: 'remove',
@@ -746,7 +781,7 @@ describe('a generate json patch function', () => {
746781
const afterModified = structuredClone(before);
747782
// @ts-ignore
748783
afterModified.firstLevel.secondLevel.thirdLevelThree = null;
749-
const patch = generateJSONPatch(before, afterModified, { maxDepth: 4 });
784+
const patch = generateJSONPatch(before, afterModified, {maxDepth: 4});
750785
expect(patch).to.eql([
751786
{
752787
op: 'replace',

0 commit comments

Comments
 (0)