@sanity/sdk 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.ts +56 -0
- package/dist/index.js +88 -199
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/_exports/index.ts +9 -1
- package/src/auth/authStore.ts +45 -0
- package/src/comlink/types.ts +18 -0
- package/src/document/documentStore.test.ts +2 -2
- package/src/document/documentStore.ts +2 -1
- package/src/document/patchOperations.test.ts +6 -6
- package/src/document/patchOperations.ts +123 -102
- package/src/document/processActions.ts +2 -6
- package/src/document/processMutations.ts +2 -2
- package/src/document/diffPatch.test.ts +0 -335
- package/src/document/diffPatch.ts +0 -288
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
import {makePatches, stringifyPatches} from '@sanity/diff-match-patch'
|
|
2
|
-
import {type Mutation, type SanityDocument} from '@sanity/types'
|
|
3
|
-
import {describe, expect, it} from 'vitest'
|
|
4
|
-
|
|
5
|
-
import {diffPatch, type PatchOperations} from './diffPatch'
|
|
6
|
-
import {processMutations} from './processMutations'
|
|
7
|
-
|
|
8
|
-
// A helper “document” that contains the system fields (which will be ignored)
|
|
9
|
-
const timestamp = '2025-02-02T00:00:00Z'
|
|
10
|
-
const docInfo = {
|
|
11
|
-
_id: 'foo',
|
|
12
|
-
_type: 'author',
|
|
13
|
-
_createdAt: timestamp,
|
|
14
|
-
_updatedAt: timestamp,
|
|
15
|
-
_rev: '1',
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function applyPatches(before: SanityDocument, patches: PatchOperations[]) {
|
|
19
|
-
const documents = processMutations({
|
|
20
|
-
documents: {[before._id]: before},
|
|
21
|
-
transactionId: 'tx1',
|
|
22
|
-
mutations: patches.map((patch): Mutation => ({patch: {id: before._id, ...patch}})),
|
|
23
|
-
timestamp: timestamp,
|
|
24
|
-
})
|
|
25
|
-
return documents[before._id] as SanityDocument
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe('diffPatch', () => {
|
|
29
|
-
it('returns an empty patch if the values are the same', () => {
|
|
30
|
-
const before = {...docInfo, a: 1, b: {c: 'test'}, d: [1, 2]}
|
|
31
|
-
const after = {...docInfo, a: 1, b: {c: 'test'}, d: [1, 2]}
|
|
32
|
-
const patches = diffPatch(before, after)
|
|
33
|
-
expect(patches).toEqual([])
|
|
34
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('returns a `set` patch if the type of a field changes', () => {
|
|
38
|
-
const before = {...docInfo, a: 1}
|
|
39
|
-
const after = {...docInfo, _rev: 'tx1', a: '1'}
|
|
40
|
-
const patches = diffPatch(before, after)
|
|
41
|
-
expect(patches).toEqual([{set: {a: '1'}}])
|
|
42
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('returns a `diffMatchPatch` patch if the string in a field changes', () => {
|
|
46
|
-
const before = {...docInfo, text: 'Hello'}
|
|
47
|
-
const after = {...docInfo, _rev: 'tx1', text: 'World'}
|
|
48
|
-
const patches = diffPatch(before, after)
|
|
49
|
-
|
|
50
|
-
const expectedPatches = makePatches('Hello', 'World')
|
|
51
|
-
const expectedPatchStr = stringifyPatches(expectedPatches)
|
|
52
|
-
|
|
53
|
-
expect(patches).toEqual([{diffMatchPatch: {text: expectedPatchStr}}])
|
|
54
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('returns a `set` patch if a number in a field changes', () => {
|
|
58
|
-
const before = {...docInfo, num: 5}
|
|
59
|
-
const after = {...docInfo, _rev: 'tx1', num: 10}
|
|
60
|
-
const patches = diffPatch(before, after)
|
|
61
|
-
expect(patches).toEqual([{set: {num: 10}}])
|
|
62
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('returns a `set` patch if a boolean field changes', () => {
|
|
66
|
-
const before = {...docInfo, bool: true}
|
|
67
|
-
const after = {...docInfo, _rev: 'tx1', bool: false}
|
|
68
|
-
const patches = diffPatch(before, after)
|
|
69
|
-
expect(patches).toEqual([{set: {bool: false}}])
|
|
70
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('returns a `set` patch if a function field changes', () => {
|
|
74
|
-
// Using a function in `before` and a number in `after` causes a type change.
|
|
75
|
-
const before = {...docInfo, fn: () => 42}
|
|
76
|
-
const after = {...docInfo, _rev: 'tx1', fn: 123}
|
|
77
|
-
const patches = diffPatch(before, after)
|
|
78
|
-
expect(patches).toEqual([{set: {fn: 123}}])
|
|
79
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('returns an `unset` patch if a field was removed', () => {
|
|
83
|
-
const before = {...docInfo, a: 1, b: 2}
|
|
84
|
-
const after = {...docInfo, _rev: 'tx1', a: 1}
|
|
85
|
-
const patches = diffPatch(before, after)
|
|
86
|
-
expect(patches).toEqual([{unset: ['b']}])
|
|
87
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('returns a `set` patch for a nested object property addition', () => {
|
|
91
|
-
const before = {...docInfo, nested: {a: 1}}
|
|
92
|
-
const after = {...docInfo, _rev: 'tx1', nested: {a: 1, b: 2}}
|
|
93
|
-
const patches = diffPatch(before, after)
|
|
94
|
-
// Only the new property “nested.b” is added.
|
|
95
|
-
expect(patches).toEqual([{set: {'nested.b': 2}}])
|
|
96
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
// Test that changes on keys starting with an underscore are ignored.
|
|
100
|
-
it('ignores internal system fields', () => {
|
|
101
|
-
const before = {...docInfo, _rev: 'foo', a: 1}
|
|
102
|
-
const after = {...docInfo, _rev: 'bar', a: 1}
|
|
103
|
-
const patches = diffPatch(before, after)
|
|
104
|
-
expect(patches).toEqual([])
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('returns patches for non-keyed arrays when an item is updated and one removed', () => {
|
|
108
|
-
const before = {...docInfo, arr: [1, 2, 3]}
|
|
109
|
-
const after = {...docInfo, _rev: 'tx1', arr: [1, 4]} // index 1 changed from 2→4 and index 2 removed
|
|
110
|
-
const patches = diffPatch(before, after)
|
|
111
|
-
// The diffing logic compares index by index:
|
|
112
|
-
// At index 0: 1 === 1 → no patch.
|
|
113
|
-
// At index 1: 2 !== 4 → set patch at "arr[1]".
|
|
114
|
-
// Then, since before had 3 items and after only 2, index 2 is removed.
|
|
115
|
-
expect(patches).toEqual([{set: {'arr[1]': 4}}, {unset: ['arr[2]']}])
|
|
116
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('returns an `insert` patch for non-keyed arrays when new items are appended', () => {
|
|
120
|
-
const before = {...docInfo, arr: [1, 2]}
|
|
121
|
-
const after = {...docInfo, _rev: 'tx1', arr: [1, 2, 3, 4]}
|
|
122
|
-
const patches = diffPatch(before, after)
|
|
123
|
-
// Here, the common prefix is at indices 0 and 1.
|
|
124
|
-
// Then the extra items [3,4] are inserted after index 1.
|
|
125
|
-
expect(patches).toEqual([{insert: {after: 'arr[1]', items: [3, 4]}}])
|
|
126
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('returns an `set` patch for non-keyed arrays when the original array is empty', () => {
|
|
130
|
-
const before = {...docInfo, arr: []}
|
|
131
|
-
const after = {...docInfo, _rev: 'tx1', arr: [1, 2, 3]}
|
|
132
|
-
const patches = diffPatch(before, after)
|
|
133
|
-
// When the before array is empty, the whole array is replaced.
|
|
134
|
-
expect(patches).toEqual([{set: {arr: [1, 2, 3]}}])
|
|
135
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('returns an `unset` patch if an object item (with `_key`) in an array was removed', () => {
|
|
139
|
-
const before = {
|
|
140
|
-
...docInfo,
|
|
141
|
-
items: [
|
|
142
|
-
{_key: 'a', val: 1},
|
|
143
|
-
{_key: 'b', val: 2},
|
|
144
|
-
],
|
|
145
|
-
}
|
|
146
|
-
const after = {
|
|
147
|
-
...docInfo,
|
|
148
|
-
_rev: 'tx1',
|
|
149
|
-
items: [{_key: 'a', val: 1}],
|
|
150
|
-
}
|
|
151
|
-
const patches = diffPatch(before, after)
|
|
152
|
-
expect(patches).toEqual([{unset: ['items[_key=="b"]']}])
|
|
153
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('returns an `insert` patch if an object item is added in a keyed array with no items', () => {
|
|
157
|
-
const before = {
|
|
158
|
-
...docInfo,
|
|
159
|
-
items: [],
|
|
160
|
-
}
|
|
161
|
-
const after = {
|
|
162
|
-
...docInfo,
|
|
163
|
-
_rev: 'tx1',
|
|
164
|
-
items: [{_key: 'a', val: 1}],
|
|
165
|
-
}
|
|
166
|
-
const patches = diffPatch(before, after)
|
|
167
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('returns an `insert` patch if an object item is added in a keyed array (insert after)', () => {
|
|
171
|
-
const before = {
|
|
172
|
-
...docInfo,
|
|
173
|
-
items: [{_key: 'a', val: 1}],
|
|
174
|
-
}
|
|
175
|
-
const after = {
|
|
176
|
-
...docInfo,
|
|
177
|
-
_rev: 'tx1',
|
|
178
|
-
items: [
|
|
179
|
-
{_key: 'a', val: 1},
|
|
180
|
-
{_key: 'b', val: 2},
|
|
181
|
-
],
|
|
182
|
-
}
|
|
183
|
-
const patches = diffPatch(before, after)
|
|
184
|
-
expect(patches).toEqual([
|
|
185
|
-
{
|
|
186
|
-
insert: {after: 'items[_key=="a"]', items: [{_key: 'b', val: 2}]},
|
|
187
|
-
},
|
|
188
|
-
])
|
|
189
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('returns a diff patch for a nested keyed array item update', () => {
|
|
193
|
-
const before = {
|
|
194
|
-
...docInfo,
|
|
195
|
-
items: [{_key: 'a', val: 'x'}],
|
|
196
|
-
}
|
|
197
|
-
const after = {
|
|
198
|
-
...docInfo,
|
|
199
|
-
_rev: 'tx1',
|
|
200
|
-
items: [{_key: 'a', val: 'y'}],
|
|
201
|
-
}
|
|
202
|
-
const patches = diffPatch(before, after)
|
|
203
|
-
const expectedDMP = makePatches('x', 'y')
|
|
204
|
-
const expectedPatchStr = stringifyPatches(expectedDMP)
|
|
205
|
-
// The patch applies to the nested property "val" of the keyed item "a"
|
|
206
|
-
expect(patches).toEqual([{diffMatchPatch: {'items[_key=="a"].val': expectedPatchStr}}])
|
|
207
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
// These tests exercise various branches in the keyed array diff logic.
|
|
211
|
-
it('returns an `insert` patch for keyed arrays when a new item is inserted at the beginning (look-ahead branch)', () => {
|
|
212
|
-
const before = {
|
|
213
|
-
...docInfo,
|
|
214
|
-
items: [{_key: 'b', val: 2}],
|
|
215
|
-
}
|
|
216
|
-
const after = {
|
|
217
|
-
...docInfo,
|
|
218
|
-
_rev: 'tx1',
|
|
219
|
-
items: [
|
|
220
|
-
{_key: 'a', val: 1},
|
|
221
|
-
{_key: 'b', val: 2},
|
|
222
|
-
],
|
|
223
|
-
}
|
|
224
|
-
const patches = diffPatch(before, after)
|
|
225
|
-
// At index 0, item "a" is new. Since at j=1 the item "b" exists in before,
|
|
226
|
-
// the new item will be inserted before "b".
|
|
227
|
-
expect(patches).toEqual([
|
|
228
|
-
{
|
|
229
|
-
insert: {before: 'items[_key=="b"]', items: [{_key: 'a', val: 1}]},
|
|
230
|
-
},
|
|
231
|
-
])
|
|
232
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
it('returns an `insert` patch for keyed arrays when all items are new (fallback branch)', () => {
|
|
236
|
-
const before = {
|
|
237
|
-
...docInfo,
|
|
238
|
-
items: [{_key: 'x', val: 'old'}],
|
|
239
|
-
}
|
|
240
|
-
const after = {
|
|
241
|
-
...docInfo,
|
|
242
|
-
_rev: 'tx1',
|
|
243
|
-
items: [
|
|
244
|
-
{_key: 'a', val: 'newA'},
|
|
245
|
-
{_key: 'b', val: 'newB'},
|
|
246
|
-
],
|
|
247
|
-
}
|
|
248
|
-
const patches = diffPatch(before, after)
|
|
249
|
-
expect(patches).toEqual([
|
|
250
|
-
{
|
|
251
|
-
insert: {
|
|
252
|
-
after: 'items[_key=="x"]',
|
|
253
|
-
items: [
|
|
254
|
-
{_key: 'a', val: 'newA'},
|
|
255
|
-
{_key: 'b', val: 'newB'},
|
|
256
|
-
],
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
{unset: ['items[_key=="x"]']},
|
|
260
|
-
])
|
|
261
|
-
|
|
262
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
it('returns an `insert` patch for keyed arrays when a new item is inserted in the middle', () => {
|
|
266
|
-
const before = {
|
|
267
|
-
...docInfo,
|
|
268
|
-
items: [
|
|
269
|
-
{_key: 'a', val: 1},
|
|
270
|
-
{_key: 'b', val: 2},
|
|
271
|
-
],
|
|
272
|
-
}
|
|
273
|
-
const after = {
|
|
274
|
-
...docInfo,
|
|
275
|
-
_rev: 'tx1',
|
|
276
|
-
items: [
|
|
277
|
-
{_key: 'a', val: 1},
|
|
278
|
-
{_key: 'x', val: 100},
|
|
279
|
-
{_key: 'b', val: 2},
|
|
280
|
-
],
|
|
281
|
-
}
|
|
282
|
-
const patches = diffPatch(before, after)
|
|
283
|
-
expect(patches).toEqual([
|
|
284
|
-
{
|
|
285
|
-
insert: {after: 'items[_key=="a"]', items: [{_key: 'x', val: 100}]},
|
|
286
|
-
},
|
|
287
|
-
])
|
|
288
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
it('returns a comprehensive set of patches for keyed arrays with removals, updates, and insertions', () => {
|
|
292
|
-
const before = {
|
|
293
|
-
...docInfo,
|
|
294
|
-
items: [
|
|
295
|
-
{_key: 'a', val: 1},
|
|
296
|
-
{_key: 'b', val: 2},
|
|
297
|
-
{_key: 'c', val: 3},
|
|
298
|
-
],
|
|
299
|
-
}
|
|
300
|
-
const after = {
|
|
301
|
-
...docInfo,
|
|
302
|
-
_rev: 'tx1',
|
|
303
|
-
items: [
|
|
304
|
-
{_key: 'a', val: 1}, // unchanged
|
|
305
|
-
{_key: 'x', val: 100}, // new insertion
|
|
306
|
-
{_key: 'b', val: 22}, // updated value
|
|
307
|
-
// { _key: 'c', ... } is removed
|
|
308
|
-
{_key: 'd', val: 4}, // new insertion
|
|
309
|
-
],
|
|
310
|
-
}
|
|
311
|
-
const patches = diffPatch(before, after)
|
|
312
|
-
// Expected patches:
|
|
313
|
-
// 1. Removal of key "c"
|
|
314
|
-
// 2. Diff of key "b" (number update: 2 -> 22 yields a set patch)
|
|
315
|
-
// 3. Insertion for the new item with key "x" (inserted after "a")
|
|
316
|
-
// 4. Insertion for the new item with key "d" (inserted after "b")
|
|
317
|
-
expect(patches).toEqual([
|
|
318
|
-
{unset: ['items[_key=="c"]']},
|
|
319
|
-
{set: {'items[_key=="b"].val': 22}},
|
|
320
|
-
{
|
|
321
|
-
insert: {
|
|
322
|
-
after: 'items[_key=="a"]',
|
|
323
|
-
items: [{_key: 'x', val: 100}],
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
insert: {
|
|
328
|
-
after: 'items[_key=="b"]',
|
|
329
|
-
items: [{_key: 'd', val: 4}],
|
|
330
|
-
},
|
|
331
|
-
},
|
|
332
|
-
])
|
|
333
|
-
expect(applyPatches(before, patches)).toEqual(after)
|
|
334
|
-
})
|
|
335
|
-
})
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import {makePatches, stringifyPatches} from '@sanity/diff-match-patch'
|
|
2
|
-
import {
|
|
3
|
-
isKeyedObject,
|
|
4
|
-
type PatchOperations as AllPatchOperations,
|
|
5
|
-
type Path,
|
|
6
|
-
type SanityDocument,
|
|
7
|
-
} from '@sanity/types'
|
|
8
|
-
|
|
9
|
-
import {stringifyPath} from './patchOperations'
|
|
10
|
-
|
|
11
|
-
export type PatchOperations = Pick<
|
|
12
|
-
AllPatchOperations,
|
|
13
|
-
'diffMatchPatch' | 'set' | 'setIfMissing' | 'unset' | 'insert'
|
|
14
|
-
>
|
|
15
|
-
|
|
16
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
17
|
-
typeof value === 'object' && !!value && !Array.isArray(value)
|
|
18
|
-
|
|
19
|
-
const ignoredKeys = ['_id', '_type', '_createdAt', '_updatedAt', '_rev']
|
|
20
|
-
|
|
21
|
-
export function diffPatch(before: SanityDocument, after: SanityDocument): PatchOperations[] {
|
|
22
|
-
return diffRecursive(before, after, [])
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Recursively diff two values given a current path.
|
|
27
|
-
*
|
|
28
|
-
* The rules are:
|
|
29
|
-
* - If the two values are identical, return no patches.
|
|
30
|
-
* - If the two values are of different types, issue a set patch for the entire path.
|
|
31
|
-
* - If both values are strings, compute a diff–match–patch patch.
|
|
32
|
-
* - If both values are numbers, booleans, null, etc., use a set patch.
|
|
33
|
-
* - If both values are arrays, delegate to diffArray.
|
|
34
|
-
* - If both values are objects then:
|
|
35
|
-
* - For each key (ignoring keys that start with `_`), if the key is missing in `after` then
|
|
36
|
-
* issue an unset patch; if extra then a set patch; otherwise, recursively diff.
|
|
37
|
-
*/
|
|
38
|
-
function diffRecursive(before: unknown, after: unknown, path: Path): PatchOperations[] {
|
|
39
|
-
if (before === after) return []
|
|
40
|
-
|
|
41
|
-
const patches: PatchOperations[] = []
|
|
42
|
-
const pathStr = stringifyPath(path)
|
|
43
|
-
|
|
44
|
-
// Handle null (remember that typeof null is "object")
|
|
45
|
-
if (before === null || after === null) {
|
|
46
|
-
if (before !== after) {
|
|
47
|
-
patches.push({set: {[pathStr]: after}})
|
|
48
|
-
}
|
|
49
|
-
return patches
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// If types differ (or one is an array and the other isn’t) – replace whole value.
|
|
53
|
-
if (typeof before !== typeof after || Array.isArray(before) !== Array.isArray(after)) {
|
|
54
|
-
patches.push({set: {[pathStr]: after}})
|
|
55
|
-
return patches
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// If both are strings, diff them using diff–match–patch.
|
|
59
|
-
if (typeof before === 'string' && typeof after === 'string') {
|
|
60
|
-
const dmpPatches = makePatches(before, after)
|
|
61
|
-
const patchStr = stringifyPatches(dmpPatches)
|
|
62
|
-
patches.push({diffMatchPatch: {[pathStr]: patchStr}})
|
|
63
|
-
return patches
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// If both are numbers then simply set the value if they differ.
|
|
67
|
-
if (typeof before === 'number' && typeof after === 'number') {
|
|
68
|
-
patches.push({set: {[pathStr]: after}})
|
|
69
|
-
return patches
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// For other primitive values (boolean, undefined, etc.) use strict comparison.
|
|
73
|
-
if (typeof before !== 'object') {
|
|
74
|
-
if (before !== after) {
|
|
75
|
-
patches.push({set: {[pathStr]: after}})
|
|
76
|
-
}
|
|
77
|
-
return patches
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// If both values are arrays, handle with diffArray.
|
|
81
|
-
if (Array.isArray(before) && Array.isArray(after)) {
|
|
82
|
-
patches.push(...diffArray(before, after, path))
|
|
83
|
-
return patches
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!isRecord(after) || !isRecord(before)) return patches
|
|
87
|
-
|
|
88
|
-
// Both are plain objects.
|
|
89
|
-
const beforeKeys = Object.keys(before).filter((k) => !ignoredKeys.includes(k))
|
|
90
|
-
const afterKeys = Object.keys(after).filter((k) => !ignoredKeys.includes(k))
|
|
91
|
-
const allKeys = new Set([...beforeKeys, ...afterKeys])
|
|
92
|
-
for (const key of allKeys) {
|
|
93
|
-
const subPath = [...path, key]
|
|
94
|
-
if (!(key in after)) {
|
|
95
|
-
// Field removed – unset it.
|
|
96
|
-
patches.push({unset: [stringifyPath(subPath)]})
|
|
97
|
-
} else if (!(key in before)) {
|
|
98
|
-
// Field added – set it.
|
|
99
|
-
patches.push({set: {[stringifyPath(subPath)]: after[key]}})
|
|
100
|
-
} else {
|
|
101
|
-
// Field exists in both – recursively diff.
|
|
102
|
-
patches.push(...diffRecursive(before[key], after[key], subPath))
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return patches
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Diff two arrays.
|
|
110
|
-
*
|
|
111
|
-
* If both arrays are “keyed” (every element is an object with a string `_key` property)
|
|
112
|
-
* then we:
|
|
113
|
-
*
|
|
114
|
-
* 1. Unset any items that were removed (using the keyed path, e.g. `items[_key=="foo"]`).
|
|
115
|
-
* 2. Recursively diff any items that exist in both arrays.
|
|
116
|
-
* 3. For any items in `after` that do not exist in `before`, group them and emit an insert patch.
|
|
117
|
-
*
|
|
118
|
-
* For non–keyed arrays we simply compare indices, unsetting “extra” items from `before` and
|
|
119
|
-
* inserting new items at the end.
|
|
120
|
-
*/
|
|
121
|
-
function diffArray(beforeArr: unknown[], afterArr: unknown[], path: Path): PatchOperations[] {
|
|
122
|
-
const pathStr = stringifyPath(path)
|
|
123
|
-
|
|
124
|
-
// Helper: determine if every element is an object with a `_key` property.
|
|
125
|
-
const isKeyedArray = (arr: unknown[]) => arr.every((item) => isKeyedObject(item))
|
|
126
|
-
|
|
127
|
-
// For keyed arrays, we collect patches in three buckets.
|
|
128
|
-
if (isKeyedArray(beforeArr) && isKeyedArray(afterArr)) {
|
|
129
|
-
// SPECIAL FIX: If the array is empty, produce an insert patch that prepends the new items.
|
|
130
|
-
if (beforeArr.length === 0 && afterArr.length > 0) {
|
|
131
|
-
return [
|
|
132
|
-
{
|
|
133
|
-
insert: {
|
|
134
|
-
before: stringifyPath([...path, 0]),
|
|
135
|
-
items: afterArr,
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
]
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const unsetPatches: PatchOperations[] = []
|
|
142
|
-
const diffPatches: PatchOperations[] = []
|
|
143
|
-
const insertPatches: PatchOperations[] = []
|
|
144
|
-
|
|
145
|
-
// Build maps from _key → {item, index}
|
|
146
|
-
const beforeMap = new Map<string, {item: unknown; index: number}>()
|
|
147
|
-
beforeArr.forEach((item, index) => {
|
|
148
|
-
// We assume item has a _key because of isKeyedArray.
|
|
149
|
-
beforeMap.set(item._key, {item, index})
|
|
150
|
-
})
|
|
151
|
-
const afterMap = new Map<string, {item: unknown; index: number}>()
|
|
152
|
-
afterArr.forEach((item, index) => {
|
|
153
|
-
afterMap.set(item._key, {item, index})
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
// 1. Unset removed items.
|
|
157
|
-
for (const [key] of beforeMap.entries()) {
|
|
158
|
-
if (!afterMap.has(key)) {
|
|
159
|
-
unsetPatches.push({unset: [stringifyPath([...path, {_key: key}])]})
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 2. Diff items that exist in both arrays.
|
|
164
|
-
for (const [key, {item: afterItem}] of afterMap.entries()) {
|
|
165
|
-
if (beforeMap.has(key)) {
|
|
166
|
-
diffPatches.push(
|
|
167
|
-
...diffRecursive(beforeMap.get(key)!.item, afterItem, [...path, {_key: key}]),
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// 3. Find contiguous “new” items in `after` and insert them.
|
|
173
|
-
let newItemsGroup: unknown[] = []
|
|
174
|
-
let insertPosition: {op: 'before' | 'after'; refKey: string} | null = null
|
|
175
|
-
for (let i = 0; i < afterArr.length; i++) {
|
|
176
|
-
const item = afterArr[i]
|
|
177
|
-
if (!beforeMap.has(item._key)) {
|
|
178
|
-
// New item.
|
|
179
|
-
if (newItemsGroup.length === 0) {
|
|
180
|
-
if (i === 0) {
|
|
181
|
-
// Look ahead for the first existing item.
|
|
182
|
-
let j = i
|
|
183
|
-
while (j < afterArr.length && !beforeMap.has(afterArr[j]._key)) {
|
|
184
|
-
j++
|
|
185
|
-
}
|
|
186
|
-
if (j < afterArr.length) {
|
|
187
|
-
insertPosition = {op: 'before', refKey: afterArr[j]._key}
|
|
188
|
-
} else if (beforeArr.length > 0) {
|
|
189
|
-
// Fallback: all items are new – use the last before item as anchor.
|
|
190
|
-
insertPosition = {op: 'after', refKey: beforeArr[beforeArr.length - 1]._key}
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
// Look backward for an existing item.
|
|
194
|
-
let j = i - 1
|
|
195
|
-
while (j >= 0 && !beforeMap.has(afterArr[j]._key)) {
|
|
196
|
-
j--
|
|
197
|
-
}
|
|
198
|
-
if (j >= 0) {
|
|
199
|
-
insertPosition = {op: 'after', refKey: afterArr[j]._key}
|
|
200
|
-
} else {
|
|
201
|
-
// Fallback – look ahead.
|
|
202
|
-
let k = i
|
|
203
|
-
while (k < afterArr.length && !beforeMap.has(afterArr[k]._key)) {
|
|
204
|
-
k++
|
|
205
|
-
}
|
|
206
|
-
if (k < afterArr.length) {
|
|
207
|
-
insertPosition = {op: 'before', refKey: afterArr[k]._key}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
newItemsGroup.push(item)
|
|
213
|
-
} else {
|
|
214
|
-
// Flush any pending group.
|
|
215
|
-
if (newItemsGroup.length > 0 && insertPosition) {
|
|
216
|
-
if (insertPosition.op === 'before') {
|
|
217
|
-
insertPatches.push({
|
|
218
|
-
insert: {
|
|
219
|
-
before: stringifyPath([...path, {_key: insertPosition.refKey}]),
|
|
220
|
-
items: newItemsGroup,
|
|
221
|
-
},
|
|
222
|
-
})
|
|
223
|
-
} else {
|
|
224
|
-
insertPatches.push({
|
|
225
|
-
insert: {
|
|
226
|
-
after: stringifyPath([...path, {_key: insertPosition.refKey}]),
|
|
227
|
-
items: newItemsGroup,
|
|
228
|
-
},
|
|
229
|
-
})
|
|
230
|
-
}
|
|
231
|
-
newItemsGroup = []
|
|
232
|
-
insertPosition = null
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
// Flush any remaining group (at the end of the array).
|
|
237
|
-
if (newItemsGroup.length > 0 && insertPosition) {
|
|
238
|
-
if (insertPosition.op === 'after') {
|
|
239
|
-
insertPatches.push({
|
|
240
|
-
insert: {
|
|
241
|
-
after: stringifyPath([...path, {_key: insertPosition.refKey}]),
|
|
242
|
-
items: newItemsGroup,
|
|
243
|
-
},
|
|
244
|
-
})
|
|
245
|
-
} else {
|
|
246
|
-
insertPatches.push({
|
|
247
|
-
insert: {
|
|
248
|
-
before: stringifyPath([...path, {_key: insertPosition.refKey}]),
|
|
249
|
-
items: newItemsGroup,
|
|
250
|
-
},
|
|
251
|
-
})
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// If every item in the "after" array is new (fallback branch) then
|
|
256
|
-
// output the insert patch before the unset patches so that when applied
|
|
257
|
-
// the insert anchor is resolved against the original document.
|
|
258
|
-
const allNew = afterArr.every((item) => !beforeMap.has(item._key))
|
|
259
|
-
if (allNew) {
|
|
260
|
-
return [...insertPatches, ...unsetPatches, ...diffPatches]
|
|
261
|
-
}
|
|
262
|
-
return [...unsetPatches, ...diffPatches, ...insertPatches]
|
|
263
|
-
} else {
|
|
264
|
-
// Non–keyed arrays: diff by index.
|
|
265
|
-
const patches: PatchOperations[] = []
|
|
266
|
-
const minLength = Math.min(beforeArr.length, afterArr.length)
|
|
267
|
-
for (let i = 0; i < minLength; i++) {
|
|
268
|
-
patches.push(...diffRecursive(beforeArr[i], afterArr[i], [...path, i]))
|
|
269
|
-
}
|
|
270
|
-
// Unset extra items from before.
|
|
271
|
-
for (let i = afterArr.length; i < beforeArr.length; i++) {
|
|
272
|
-
patches.push({unset: [stringifyPath([...path, i])]})
|
|
273
|
-
}
|
|
274
|
-
// Insert any extra items from after.
|
|
275
|
-
if (afterArr.length > beforeArr.length) {
|
|
276
|
-
const newItems = afterArr.slice(beforeArr.length)
|
|
277
|
-
if (beforeArr.length > 0) {
|
|
278
|
-
patches.push({
|
|
279
|
-
insert: {after: stringifyPath([...path, beforeArr.length - 1]), items: newItems},
|
|
280
|
-
})
|
|
281
|
-
} else {
|
|
282
|
-
// If the array was empty, simply set the whole array.
|
|
283
|
-
patches.push({set: {[pathStr]: afterArr}})
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return patches
|
|
287
|
-
}
|
|
288
|
-
}
|