@sanity/diff-patch 5.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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/index.cjs +251 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +299 -0
- package/dist/index.d.ts +299 -0
- package/dist/index.js +251 -0
- package/dist/index.js.map +1 -0
- package/package.json +90 -0
- package/src/diffError.ts +23 -0
- package/src/diffPatch.ts +575 -0
- package/src/index.ts +20 -0
- package/src/patches.ts +126 -0
- package/src/paths.ts +59 -0
- package/src/validate.ts +71 -0
package/src/diffPatch.ts
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import {cleanupEfficiency, makeDiff, makePatches, stringifyPatches} from '@sanity/diff-match-patch'
|
|
2
|
+
import {DiffError} from './diffError.js'
|
|
3
|
+
import {type Path, pathToString} from './paths.js'
|
|
4
|
+
import {validateProperty} from './validate.js'
|
|
5
|
+
import {
|
|
6
|
+
type Patch,
|
|
7
|
+
type SetPatch,
|
|
8
|
+
type UnsetPatch,
|
|
9
|
+
type InsertAfterPatch,
|
|
10
|
+
type DiffMatchPatch,
|
|
11
|
+
type SanityInsertPatch,
|
|
12
|
+
type SanityPatch,
|
|
13
|
+
type SanitySetPatch,
|
|
14
|
+
type SanityUnsetPatch,
|
|
15
|
+
type SanityDiffMatchPatch,
|
|
16
|
+
type SanityPatchMutation,
|
|
17
|
+
} from './patches.js'
|
|
18
|
+
|
|
19
|
+
const ignoredKeys = ['_id', '_type', '_createdAt', '_updatedAt', '_rev']
|
|
20
|
+
|
|
21
|
+
type PrimitiveValue = string | number | boolean | null | undefined
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An object (record) that has a `_key` property
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export interface KeyedSanityObject {
|
|
29
|
+
[key: string]: unknown
|
|
30
|
+
_key: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* An object (record) that _may_ have a `_key` property
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export type SanityObject = KeyedSanityObject | Partial<KeyedSanityObject>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Represents a partial Sanity document (eg a "stub").
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export interface DocumentStub {
|
|
46
|
+
_id?: string
|
|
47
|
+
_type?: string
|
|
48
|
+
_rev?: string
|
|
49
|
+
_createdAt?: string
|
|
50
|
+
_updatedAt?: string
|
|
51
|
+
[key: string]: unknown
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Options for the diff-match-patch algorithm.
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
export interface DiffMatchPatchOptions {
|
|
60
|
+
/**
|
|
61
|
+
* Whether or not to use diff-match-patch at all
|
|
62
|
+
*
|
|
63
|
+
* @defaultValue `true`
|
|
64
|
+
*/
|
|
65
|
+
enabled: boolean
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Threshold at which to start using diff-match-patch instead of a regular `set` patch.
|
|
69
|
+
*
|
|
70
|
+
* @defaultValue `30`
|
|
71
|
+
*/
|
|
72
|
+
lengthThresholdAbsolute: number
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Only use generated diff-match-patch if the patch length is less than or equal to
|
|
76
|
+
* (targetString * relative). Example: A 100 character target with a relative factor
|
|
77
|
+
* of 1.2 will allow a 120 character diff-match-patch. If larger than this number,
|
|
78
|
+
* it will fall back to a regular `set` patch.
|
|
79
|
+
*
|
|
80
|
+
* @defaultValue `1.2`
|
|
81
|
+
*/
|
|
82
|
+
lengthThresholdRelative: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Options for the patch generator
|
|
87
|
+
*
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
export interface PatchOptions {
|
|
91
|
+
/**
|
|
92
|
+
* Document ID to apply the patch to.
|
|
93
|
+
*
|
|
94
|
+
* @defaultValue `undefined` - tries to extract `_id` from passed document
|
|
95
|
+
*/
|
|
96
|
+
id?: string
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Base path to apply the patch to - useful if diffing sub-branches of a document.
|
|
100
|
+
*
|
|
101
|
+
* @defaultValue `[]` - eg root of the document
|
|
102
|
+
*/
|
|
103
|
+
basePath?: Path
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Only apply the patch if the document revision matches this value.
|
|
107
|
+
* If the property is the boolean value `true`, it will attempt to extract
|
|
108
|
+
* the revision from the document `_rev` property.
|
|
109
|
+
*
|
|
110
|
+
* @defaultValue `undefined` (do not apply revision check)
|
|
111
|
+
*/
|
|
112
|
+
ifRevisionID?: string | true
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether or not to hide warnings during the diff process.
|
|
116
|
+
*
|
|
117
|
+
* @defaultValue `false`
|
|
118
|
+
*/
|
|
119
|
+
hideWarnings?: boolean
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Options for the diff-match-patch algorithm.
|
|
123
|
+
*/
|
|
124
|
+
diffMatchPatch?: Partial<DiffMatchPatchOptions>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Options for diff generation, where all DMP properties are required
|
|
129
|
+
*
|
|
130
|
+
* @public
|
|
131
|
+
*/
|
|
132
|
+
export type DiffOptions = PatchOptions & {diffMatchPatch: Required<DiffMatchPatchOptions>}
|
|
133
|
+
|
|
134
|
+
const defaultOptions = {
|
|
135
|
+
hideWarnings: false,
|
|
136
|
+
diffMatchPatch: {
|
|
137
|
+
enabled: true,
|
|
138
|
+
|
|
139
|
+
// Only use diff-match-patch if target string is longer than this threshold
|
|
140
|
+
lengthThresholdAbsolute: 30,
|
|
141
|
+
|
|
142
|
+
// Only use generated diff-match-patch if the patch length is less than or equal to
|
|
143
|
+
// (targetString * relative). Example: A 100 character target with a relative factor
|
|
144
|
+
// of 1.2 will allow a 120 character diff-match-patch. If larger than this number,
|
|
145
|
+
// it will fall back to a regular `set` patch.
|
|
146
|
+
lengthThresholdRelative: 1.2,
|
|
147
|
+
},
|
|
148
|
+
} satisfies DiffOptions
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Merges the default options with the passed in options.
|
|
152
|
+
*
|
|
153
|
+
* @param options - Options to merge with the defaults
|
|
154
|
+
* @returns Merged options
|
|
155
|
+
*/
|
|
156
|
+
function mergeOptions(options: PatchOptions): DiffOptions {
|
|
157
|
+
return {
|
|
158
|
+
...defaultOptions,
|
|
159
|
+
...options,
|
|
160
|
+
diffMatchPatch: {...defaultOptions.diffMatchPatch, ...(options.diffMatchPatch || {})},
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Generates an array of mutations for Sanity, based on the differences between
|
|
166
|
+
* the two passed documents/trees.
|
|
167
|
+
*
|
|
168
|
+
* @param itemA - The first document/tree to compare
|
|
169
|
+
* @param itemB - The second document/tree to compare
|
|
170
|
+
* @param opts - Options for the diff generation
|
|
171
|
+
* @returns Array of mutations
|
|
172
|
+
* @public
|
|
173
|
+
*/
|
|
174
|
+
export function diffPatch(
|
|
175
|
+
itemA: DocumentStub,
|
|
176
|
+
itemB: DocumentStub,
|
|
177
|
+
opts?: PatchOptions,
|
|
178
|
+
): SanityPatchMutation[] {
|
|
179
|
+
const options = mergeOptions(opts || {})
|
|
180
|
+
const id = options.id || (itemA._id === itemB._id && itemA._id)
|
|
181
|
+
const revisionLocked = options.ifRevisionID
|
|
182
|
+
const ifRevisionID = typeof revisionLocked === 'boolean' ? itemA._rev : revisionLocked
|
|
183
|
+
const basePath = options.basePath || []
|
|
184
|
+
if (!id) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
'_id on itemA and itemB not present or differs, specify document id the mutations should be applied to',
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (revisionLocked === true && !ifRevisionID) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
'`ifRevisionID` is set to `true`, but no `_rev` was passed in item A. Either explicitly set `ifRevisionID` to a revision, or pass `_rev` as part of item A.',
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (basePath.length === 0 && itemA._type !== itemB._type) {
|
|
197
|
+
throw new Error(`_type is immutable and cannot be changed (${itemA._type} => ${itemB._type})`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const operations = diffItem(itemA, itemB, options, basePath, [])
|
|
201
|
+
return serializePatches(operations, {id, ifRevisionID: revisionLocked ? ifRevisionID : undefined})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Diffs two items and returns an array of patches.
|
|
206
|
+
* Note that this is different from `diffPatch`, which generates _mutations_.
|
|
207
|
+
*
|
|
208
|
+
* @param itemA - The first item to compare
|
|
209
|
+
* @param itemB - The second item to compare
|
|
210
|
+
* @param opts - Options for the diff generation
|
|
211
|
+
* @param path - Path to the current item
|
|
212
|
+
* @param patches - Array of patches to append the results to. Note that this is MUTATED.
|
|
213
|
+
* @returns Array of patches
|
|
214
|
+
* @public
|
|
215
|
+
*/
|
|
216
|
+
export function diffItem(
|
|
217
|
+
itemA: unknown,
|
|
218
|
+
itemB: unknown,
|
|
219
|
+
opts: DiffOptions = defaultOptions,
|
|
220
|
+
path: Path = [],
|
|
221
|
+
patches: Patch[] = [],
|
|
222
|
+
): Patch[] {
|
|
223
|
+
if (itemA === itemB) {
|
|
224
|
+
return patches
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const aType = Array.isArray(itemA) ? 'array' : typeof itemA
|
|
228
|
+
const bType = Array.isArray(itemB) ? 'array' : typeof itemB
|
|
229
|
+
|
|
230
|
+
const aIsUndefined = aType === 'undefined'
|
|
231
|
+
const bIsUndefined = bType === 'undefined'
|
|
232
|
+
|
|
233
|
+
if (aIsUndefined && !bIsUndefined) {
|
|
234
|
+
patches.push({op: 'set', path, value: itemB})
|
|
235
|
+
return patches
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!aIsUndefined && bIsUndefined) {
|
|
239
|
+
patches.push({op: 'unset', path})
|
|
240
|
+
return patches
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const options = mergeOptions(opts)
|
|
244
|
+
const dataType = aIsUndefined ? bType : aType
|
|
245
|
+
const isContainer = dataType === 'object' || dataType === 'array'
|
|
246
|
+
if (!isContainer) {
|
|
247
|
+
return diffPrimitive(itemA as PrimitiveValue, itemB as PrimitiveValue, options, path, patches)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (aType !== bType) {
|
|
251
|
+
// Array => Object / Object => Array
|
|
252
|
+
patches.push({op: 'set', path, value: itemB})
|
|
253
|
+
return patches
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return dataType === 'array'
|
|
257
|
+
? diffArray(itemA as unknown[], itemB as unknown[], options, path, patches)
|
|
258
|
+
: diffObject(itemA as object, itemB as object, options, path, patches)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function diffObject(
|
|
262
|
+
itemA: SanityObject,
|
|
263
|
+
itemB: SanityObject,
|
|
264
|
+
options: DiffOptions,
|
|
265
|
+
path: Path,
|
|
266
|
+
patches: Patch[],
|
|
267
|
+
) {
|
|
268
|
+
const atRoot = path.length === 0
|
|
269
|
+
const aKeys = Object.keys(itemA)
|
|
270
|
+
.filter(atRoot ? isNotIgnoredKey : yes)
|
|
271
|
+
.map((key) => validateProperty(key, itemA[key], path))
|
|
272
|
+
|
|
273
|
+
const aKeysLength = aKeys.length
|
|
274
|
+
const bKeys = Object.keys(itemB)
|
|
275
|
+
.filter(atRoot ? isNotIgnoredKey : yes)
|
|
276
|
+
.map((key) => validateProperty(key, itemB[key], path))
|
|
277
|
+
|
|
278
|
+
const bKeysLength = bKeys.length
|
|
279
|
+
|
|
280
|
+
// Check for deleted items
|
|
281
|
+
for (let i = 0; i < aKeysLength; i++) {
|
|
282
|
+
const key = aKeys[i]
|
|
283
|
+
if (!(key in itemB)) {
|
|
284
|
+
patches.push({op: 'unset', path: path.concat(key)})
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Check for changed items
|
|
289
|
+
for (let i = 0; i < bKeysLength; i++) {
|
|
290
|
+
const key = bKeys[i]
|
|
291
|
+
diffItem(itemA[key], itemB[key], options, path.concat([key]), patches)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return patches
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function diffArray(
|
|
298
|
+
itemA: unknown[],
|
|
299
|
+
itemB: unknown[],
|
|
300
|
+
options: DiffOptions,
|
|
301
|
+
path: Path,
|
|
302
|
+
patches: Patch[],
|
|
303
|
+
) {
|
|
304
|
+
// Check for new items
|
|
305
|
+
if (itemB.length > itemA.length) {
|
|
306
|
+
patches.push({
|
|
307
|
+
op: 'insert',
|
|
308
|
+
after: path.concat([-1]),
|
|
309
|
+
items: itemB.slice(itemA.length).map((item, i) => nullifyUndefined(item, path, i, options)),
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check for deleted items
|
|
314
|
+
if (itemB.length < itemA.length) {
|
|
315
|
+
const isSingle = itemA.length - itemB.length === 1
|
|
316
|
+
const unsetItems = itemA.slice(itemB.length)
|
|
317
|
+
|
|
318
|
+
// If we're revision locked, we can safely unset ranges (eg 5:<end-of-array>).
|
|
319
|
+
// Also, if we don't have unique array keys, we can't use any better approach
|
|
320
|
+
// than array indexes. If we _do_ have unique array keys, we'll want to unset
|
|
321
|
+
// by key, as this is safer in a realtime, collaborative setting
|
|
322
|
+
if (isRevisionLocked(options) || !isUniquelyKeyed(unsetItems)) {
|
|
323
|
+
patches.push({
|
|
324
|
+
op: 'unset',
|
|
325
|
+
path: path.concat([isSingle ? itemB.length : [itemB.length, '']]),
|
|
326
|
+
})
|
|
327
|
+
} else {
|
|
328
|
+
patches.push(
|
|
329
|
+
...unsetItems.map(
|
|
330
|
+
(item): UnsetPatch => ({op: 'unset', path: path.concat({_key: item._key})}),
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Check for illegal array contents
|
|
337
|
+
for (let i = 0; i < itemB.length; i++) {
|
|
338
|
+
if (Array.isArray(itemB[i])) {
|
|
339
|
+
throw new DiffError('Multi-dimensional arrays not supported', path.concat(i), itemB[i])
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const overlapping = Math.min(itemA.length, itemB.length)
|
|
344
|
+
const segmentA = itemA.slice(0, overlapping)
|
|
345
|
+
const segmentB = itemB.slice(0, overlapping)
|
|
346
|
+
|
|
347
|
+
return isUniquelyKeyed(segmentA) && isUniquelyKeyed(segmentB)
|
|
348
|
+
? diffArrayByKey(segmentA, segmentB, options, path, patches)
|
|
349
|
+
: diffArrayByIndex(segmentA, segmentB, options, path, patches)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function diffArrayByIndex(
|
|
353
|
+
itemA: unknown[],
|
|
354
|
+
itemB: unknown[],
|
|
355
|
+
options: DiffOptions,
|
|
356
|
+
path: Path,
|
|
357
|
+
patches: Patch[],
|
|
358
|
+
) {
|
|
359
|
+
for (let i = 0; i < itemA.length; i++) {
|
|
360
|
+
diffItem(
|
|
361
|
+
itemA[i],
|
|
362
|
+
nullifyUndefined(itemB[i], path, i, options),
|
|
363
|
+
options,
|
|
364
|
+
path.concat(i),
|
|
365
|
+
patches,
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return patches
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function diffArrayByKey(
|
|
373
|
+
itemA: KeyedSanityObject[],
|
|
374
|
+
itemB: KeyedSanityObject[],
|
|
375
|
+
options: DiffOptions,
|
|
376
|
+
path: Path,
|
|
377
|
+
patches: Patch[],
|
|
378
|
+
) {
|
|
379
|
+
const keyedA = indexByKey(itemA)
|
|
380
|
+
const keyedB = indexByKey(itemB)
|
|
381
|
+
|
|
382
|
+
// There's a bunch of hard/semi-hard problems related to using keys
|
|
383
|
+
// Unless we have the exact same order, just use indexes for now
|
|
384
|
+
if (!arrayIsEqual(keyedA.keys, keyedB.keys)) {
|
|
385
|
+
return diffArrayByIndex(itemA, itemB, options, path, patches)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (let i = 0; i < keyedB.keys.length; i++) {
|
|
389
|
+
const key = keyedB.keys[i]
|
|
390
|
+
const valueA = keyedA.index[key]
|
|
391
|
+
const valueB = nullifyUndefined(keyedB.index[key], path, i, options)
|
|
392
|
+
diffItem(valueA, valueB, options, path.concat({_key: key}), patches)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return patches
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function getDiffMatchPatch(
|
|
399
|
+
itemA: PrimitiveValue,
|
|
400
|
+
itemB: PrimitiveValue,
|
|
401
|
+
options: DiffOptions,
|
|
402
|
+
path: Path,
|
|
403
|
+
): DiffMatchPatch | undefined {
|
|
404
|
+
const {enabled, lengthThresholdRelative, lengthThresholdAbsolute} = options.diffMatchPatch
|
|
405
|
+
const segment = path[path.length - 1]
|
|
406
|
+
if (
|
|
407
|
+
!enabled ||
|
|
408
|
+
// Don't use for anything but strings
|
|
409
|
+
typeof itemA !== 'string' ||
|
|
410
|
+
typeof itemB !== 'string' ||
|
|
411
|
+
// Don't use for `_key`, `_ref` etc
|
|
412
|
+
(typeof segment === 'string' && segment[0] === '_') ||
|
|
413
|
+
// Don't use on short strings
|
|
414
|
+
itemB.length < lengthThresholdAbsolute
|
|
415
|
+
) {
|
|
416
|
+
return undefined
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let strPatch = ''
|
|
420
|
+
try {
|
|
421
|
+
const patch = makeDiff(itemA, itemB)
|
|
422
|
+
const diff = cleanupEfficiency(patch)
|
|
423
|
+
strPatch = stringifyPatches(makePatches(diff))
|
|
424
|
+
} catch (err) {
|
|
425
|
+
// Fall back to using regular set patch
|
|
426
|
+
return undefined
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Don't use patch if it's longer than allowed relative threshold.
|
|
430
|
+
// Allow a 120 character patch for a 100 character string,
|
|
431
|
+
// but don't allow a 800 character patch for a 500 character value.
|
|
432
|
+
return strPatch.length > itemB.length * lengthThresholdRelative
|
|
433
|
+
? undefined
|
|
434
|
+
: {op: 'diffMatchPatch', path, value: strPatch}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function diffPrimitive(
|
|
438
|
+
itemA: PrimitiveValue,
|
|
439
|
+
itemB: PrimitiveValue,
|
|
440
|
+
options: DiffOptions,
|
|
441
|
+
path: Path,
|
|
442
|
+
patches: Patch[],
|
|
443
|
+
): Patch[] {
|
|
444
|
+
const dmp = getDiffMatchPatch(itemA, itemB, options, path)
|
|
445
|
+
|
|
446
|
+
patches.push(
|
|
447
|
+
dmp || {
|
|
448
|
+
op: 'set',
|
|
449
|
+
path,
|
|
450
|
+
value: itemB,
|
|
451
|
+
},
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return patches
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function isNotIgnoredKey(key: string) {
|
|
458
|
+
return ignoredKeys.indexOf(key) === -1
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function serializePatches(
|
|
462
|
+
patches: Patch[],
|
|
463
|
+
options: {id: string; ifRevisionID?: string},
|
|
464
|
+
): SanityPatchMutation[] {
|
|
465
|
+
if (patches.length === 0) {
|
|
466
|
+
return []
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const {id, ifRevisionID} = options
|
|
470
|
+
const set = patches.filter((patch): patch is SetPatch => patch.op === 'set')
|
|
471
|
+
const unset = patches.filter((patch): patch is UnsetPatch => patch.op === 'unset')
|
|
472
|
+
const insert = patches.filter((patch): patch is InsertAfterPatch => patch.op === 'insert')
|
|
473
|
+
const dmp = patches.filter((patch): patch is DiffMatchPatch => patch.op === 'diffMatchPatch')
|
|
474
|
+
|
|
475
|
+
const withSet =
|
|
476
|
+
set.length > 0 &&
|
|
477
|
+
set.reduce(
|
|
478
|
+
(patch: SanitySetPatch, item: SetPatch) => {
|
|
479
|
+
const path = pathToString(item.path)
|
|
480
|
+
patch.set[path] = item.value
|
|
481
|
+
return patch
|
|
482
|
+
},
|
|
483
|
+
{id, set: {}},
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
const withUnset =
|
|
487
|
+
unset.length > 0 &&
|
|
488
|
+
unset.reduce(
|
|
489
|
+
(patch: SanityUnsetPatch, item: UnsetPatch) => {
|
|
490
|
+
const path = pathToString(item.path)
|
|
491
|
+
patch.unset.push(path)
|
|
492
|
+
return patch
|
|
493
|
+
},
|
|
494
|
+
{id, unset: []},
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
const withInsert = insert.reduce((acc: SanityInsertPatch[], item: InsertAfterPatch) => {
|
|
498
|
+
const after = pathToString(item.after)
|
|
499
|
+
return acc.concat({id, insert: {after, items: item.items}})
|
|
500
|
+
}, [])
|
|
501
|
+
|
|
502
|
+
const withDmp =
|
|
503
|
+
dmp.length > 0 &&
|
|
504
|
+
dmp.reduce(
|
|
505
|
+
(patch: SanityDiffMatchPatch, item: DiffMatchPatch) => {
|
|
506
|
+
const path = pathToString(item.path)
|
|
507
|
+
patch.diffMatchPatch[path] = item.value
|
|
508
|
+
return patch
|
|
509
|
+
},
|
|
510
|
+
{id, diffMatchPatch: {}},
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
const patchSet: SanityPatch[] = [withUnset, withSet, withDmp, ...withInsert].filter(
|
|
514
|
+
(item): item is SanityPatch => item !== false,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
return patchSet.map((patch, i) => ({
|
|
518
|
+
patch: ifRevisionID && i === 0 ? {...patch, ifRevisionID} : patch,
|
|
519
|
+
}))
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function isUniquelyKeyed(arr: unknown[]): arr is KeyedSanityObject[] {
|
|
523
|
+
const keys = []
|
|
524
|
+
|
|
525
|
+
for (let i = 0; i < arr.length; i++) {
|
|
526
|
+
const key = getKey(arr[i])
|
|
527
|
+
if (!key || keys.indexOf(key) !== -1) {
|
|
528
|
+
return false
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
keys.push(key)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return true
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function getKey(obj: unknown) {
|
|
538
|
+
return typeof obj === 'object' && obj !== null && (obj as KeyedSanityObject)._key
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function indexByKey(arr: KeyedSanityObject[]) {
|
|
542
|
+
return arr.reduce(
|
|
543
|
+
(acc, item) => {
|
|
544
|
+
acc.keys.push(item._key)
|
|
545
|
+
acc.index[item._key] = item
|
|
546
|
+
return acc
|
|
547
|
+
},
|
|
548
|
+
{keys: [] as string[], index: {} as {[key: string]: KeyedSanityObject}},
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function arrayIsEqual(itemA: unknown[], itemB: unknown[]) {
|
|
553
|
+
return itemA.length === itemB.length && itemA.every((item, i) => itemB[i] === item)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function nullifyUndefined(item: unknown, path: Path, index: number, options: PatchOptions) {
|
|
557
|
+
if (typeof item !== 'undefined') {
|
|
558
|
+
return item
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (!options.hideWarnings) {
|
|
562
|
+
const serializedPath = pathToString(path.concat(index))
|
|
563
|
+
console.warn(`undefined value in array converted to null (at '${serializedPath}')`)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return null
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function isRevisionLocked(options: PatchOptions): boolean {
|
|
570
|
+
return Boolean(options.ifRevisionID)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function yes(_: unknown) {
|
|
574
|
+
return true
|
|
575
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export {diffPatch, diffItem} from './diffPatch.js'
|
|
2
|
+
export {DiffError} from './diffError.js'
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
DiffMatchPatch,
|
|
6
|
+
InsertAfterPatch,
|
|
7
|
+
Patch,
|
|
8
|
+
SanityDiffMatchPatch,
|
|
9
|
+
SanityInsertPatch,
|
|
10
|
+
SanityPatch,
|
|
11
|
+
SanityPatchMutation,
|
|
12
|
+
SanitySetPatch,
|
|
13
|
+
SanityUnsetPatch,
|
|
14
|
+
SetPatch,
|
|
15
|
+
UnsetPatch,
|
|
16
|
+
} from './patches.js'
|
|
17
|
+
|
|
18
|
+
export type {DiffMatchPatchOptions, DiffOptions, DocumentStub, PatchOptions} from './diffPatch.js'
|
|
19
|
+
|
|
20
|
+
export type {Path, PathSegment} from './paths.js'
|
package/src/patches.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type {Path} from './paths.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A `set` operation
|
|
5
|
+
* Replaces the current path, does not merge
|
|
6
|
+
* Note: NOT a serializable mutation, see {@link SanitySetPatch} for that
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export interface SetPatch {
|
|
11
|
+
op: 'set'
|
|
12
|
+
path: Path
|
|
13
|
+
value: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A `unset` operation
|
|
18
|
+
* Unsets the entire value of the given path
|
|
19
|
+
* Note: NOT a serializable mutation, see {@link SanityUnsetPatch} for that
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export interface UnsetPatch {
|
|
24
|
+
op: 'unset'
|
|
25
|
+
path: Path
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A `insert` operation
|
|
30
|
+
* Inserts the given items _after_ the given path
|
|
31
|
+
* Note: NOT a serializable mutation, see {@link SanityInsertPatch} for that
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export interface InsertAfterPatch {
|
|
36
|
+
op: 'insert'
|
|
37
|
+
after: Path
|
|
38
|
+
items: any[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A `diffMatchPatch` operation
|
|
43
|
+
* Applies the given `value` (unidiff format) to the given path. Must be a string.
|
|
44
|
+
* Note: NOT a serializable mutation, see {@link SanityDiffMatchPatch} for that
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
export interface DiffMatchPatch {
|
|
49
|
+
op: 'diffMatchPatch'
|
|
50
|
+
path: Path
|
|
51
|
+
value: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A patch containing either a Sanity set, unset, insert or diffMatchPatch operation
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
export type Patch = SetPatch | UnsetPatch | InsertAfterPatch | DiffMatchPatch
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A Sanity `set` patch mutation operation
|
|
63
|
+
* Replaces the current path, does not merge
|
|
64
|
+
*
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
export interface SanitySetPatch {
|
|
68
|
+
id: string
|
|
69
|
+
set: {[key: string]: any}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A Sanity `unset` patch mutation operation
|
|
74
|
+
* Unsets the entire value of the given path
|
|
75
|
+
*
|
|
76
|
+
* @public
|
|
77
|
+
*/
|
|
78
|
+
export interface SanityUnsetPatch {
|
|
79
|
+
id: string
|
|
80
|
+
unset: string[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A Sanity `insert` patch mutation operation
|
|
85
|
+
* Inserts the given items at the given path (before/after)
|
|
86
|
+
*
|
|
87
|
+
* @public
|
|
88
|
+
*/
|
|
89
|
+
export interface SanityInsertPatch {
|
|
90
|
+
id: string
|
|
91
|
+
insert:
|
|
92
|
+
| {before: string; items: any[]}
|
|
93
|
+
| {after: string; items: any[]}
|
|
94
|
+
| {replace: string; items: any[]}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A Sanity `diffMatchPatch` patch mutation operation
|
|
99
|
+
* Patches the given path with the given unidiff string.
|
|
100
|
+
*
|
|
101
|
+
* @public
|
|
102
|
+
*/
|
|
103
|
+
export interface SanityDiffMatchPatch {
|
|
104
|
+
id: string
|
|
105
|
+
diffMatchPatch: {[key: string]: string}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A patch containing either a set, unset, insert or diffMatchPatch operation
|
|
110
|
+
*
|
|
111
|
+
* @public
|
|
112
|
+
*/
|
|
113
|
+
export type SanityPatch =
|
|
114
|
+
| SanitySetPatch
|
|
115
|
+
| SanityUnsetPatch
|
|
116
|
+
| SanityInsertPatch
|
|
117
|
+
| SanityDiffMatchPatch
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A mutation containing a single patch
|
|
121
|
+
*
|
|
122
|
+
* @public
|
|
123
|
+
*/
|
|
124
|
+
export interface SanityPatchMutation {
|
|
125
|
+
patch: SanityPatch
|
|
126
|
+
}
|