Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 36 additions & 36 deletions src/diffPatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,24 +95,24 @@
* Generates an array of mutations for Sanity, based on the differences between
* the two passed documents/trees.
*
* @param itemA - The first document/tree to compare
* @param itemB - The second document/tree to compare
* @param source - The first document/tree to compare
* @param target - The second document/tree to compare
* @param opts - Options for the diff generation
* @returns Array of mutations
* @public
*/
export function diffPatch(
itemA: DocumentStub,
itemB: DocumentStub,
source: DocumentStub,
target: DocumentStub,
options: PatchOptions = {},
): SanityPatchMutation[] {
const id = options.id || (itemA._id === itemB._id && itemA._id)
const id = options.id || (source._id === target._id && source._id)
const revisionLocked = options.ifRevisionID
const ifRevisionID = typeof revisionLocked === 'boolean' ? itemA._rev : revisionLocked
const ifRevisionID = typeof revisionLocked === 'boolean' ? source._rev : revisionLocked
const basePath = options.basePath || []
if (!id) {
throw new Error(
'_id on itemA and itemB not present or differs, specify document id the mutations should be applied to',
'_id on source and target not present or differs, specify document id the mutations should be applied to',
)
}

Expand All @@ -122,11 +122,11 @@
)
}

if (basePath.length === 0 && itemA._type !== itemB._type) {
throw new Error(`_type is immutable and cannot be changed (${itemA._type} => ${itemB._type})`)
if (basePath.length === 0 && source._type !== target._type) {
throw new Error(`_type is immutable and cannot be changed (${source._type} => ${target._type})`)
}

const operations = diffItem(itemA, itemB, basePath, [])
const operations = diffItem(source, target, basePath, [])
return serializePatches(operations).map((patchOperations, i) => ({
patch: {
id,
Expand Down Expand Up @@ -190,63 +190,63 @@
}

function diffObject(
itemA: Record<string, unknown>,
itemB: Record<string, unknown>,
source: Record<string, unknown>,
target: Record<string, unknown>,
path: Path,
patches: Patch[],
) {
const atRoot = path.length === 0
const aKeys = Object.keys(itemA)
const aKeys = Object.keys(source)
.filter(atRoot ? isNotIgnoredKey : yes)
.map((key) => validateProperty(key, itemA[key], path))
.map((key) => validateProperty(key, source[key], path))

const aKeysLength = aKeys.length
const bKeys = Object.keys(itemB)
const bKeys = Object.keys(target)
.filter(atRoot ? isNotIgnoredKey : yes)
.map((key) => validateProperty(key, itemB[key], path))
.map((key) => validateProperty(key, target[key], path))

const bKeysLength = bKeys.length

// Check for deleted items
for (let i = 0; i < aKeysLength; i++) {
const key = aKeys[i]
if (!(key in itemB)) {
if (!(key in target)) {
patches.push({op: 'unset', path: path.concat(key)})
}
}

// Check for changed items
for (let i = 0; i < bKeysLength; i++) {
const key = bKeys[i]
diffItem(itemA[key], itemB[key], path.concat([key]), patches)
diffItem(source[key], target[key], path.concat([key]), patches)
}

return patches
}

function diffArray(itemA: unknown[], itemB: unknown[], path: Path, patches: Patch[]) {
if (isUniquelyKeyed(itemA) && isUniquelyKeyed(itemB)) {
return diffArrayByKey(itemA, itemB, path, patches)
function diffArray(source: unknown[], target: unknown[], path: Path, patches: Patch[]) {
if (isUniquelyKeyed(source) && isUniquelyKeyed(target)) {
return diffArrayByKey(source, target, path, patches)
}

return diffArrayByIndex(itemA, itemB, path, patches)
return diffArrayByIndex(source, target, path, patches)
}

function diffArrayByIndex(itemA: unknown[], itemB: unknown[], path: Path, patches: Patch[]) {
function diffArrayByIndex(source: unknown[], target: unknown[], path: Path, patches: Patch[]) {
// Check for new items
if (itemB.length > itemA.length) {
if (target.length > source.length) {
patches.push({
op: 'insert',
position: 'after',
path: path.concat([-1]),
items: itemB.slice(itemA.length).map(nullifyUndefined),
items: target.slice(source.length).map(nullifyUndefined),
})
}

// Check for deleted items
if (itemB.length < itemA.length) {
const isSingle = itemA.length - itemB.length === 1
const unsetItems = itemA.slice(itemB.length)
if (target.length < source.length) {
const isSingle = source.length - target.length === 1
const unsetItems = source.slice(target.length)

// If we have unique array keys, we'll want to unset by key, as this is
// safer in a realtime, collaborative setting
Expand All @@ -259,21 +259,21 @@
} else {
patches.push({
op: 'unset',
path: path.concat([isSingle ? itemB.length : [itemB.length, '']]),
path: path.concat([isSingle ? target.length : [target.length, '']]),
})
}
}

// Check for illegal array contents
for (let i = 0; i < itemB.length; i++) {
if (Array.isArray(itemB[i])) {
throw new DiffError('Multi-dimensional arrays not supported', path.concat(i), itemB[i])
for (let i = 0; i < target.length; i++) {
if (Array.isArray(target[i])) {
throw new DiffError('Multi-dimensional arrays not supported', path.concat(i), target[i])
}
}

const overlapping = Math.min(itemA.length, itemB.length)
const segmentA = itemA.slice(0, overlapping)
const segmentB = itemB.slice(0, overlapping)
const overlapping = Math.min(source.length, target.length)
const segmentA = source.slice(0, overlapping)
const segmentB = target.slice(0, overlapping)

for (let i = 0; i < segmentA.length; i++) {
diffItem(segmentA[i], nullifyUndefined(segmentB[i]), path.concat(i), patches)
Expand Down Expand Up @@ -494,7 +494,7 @@

try {
// Using `makePatches(string, string)` directly instead of the multi-step approach e.g.
// `stringifyPatches(makePatches(cleanupEfficiency(makeDiff(itemA, itemB))))`.
// `stringifyPatches(makePatches(cleanupEfficiency(makeDiff(source, target))))`.
// this is because `makePatches` internally handles diff generation and
// automatically applies both `cleanupSemantic()` and `cleanupEfficiency()`
// when beneficial, resulting in cleaner code with near identical performance and
Expand Down Expand Up @@ -538,7 +538,7 @@
switch (patch.op) {
case 'set':
case 'diffMatchPatch': {
// TODO: reconfigure eslint to use @typescript-eslint/no-unused-vars

Check warning on line 541 in src/diffPatch.ts

View workflow job for this annotation

GitHub Actions / test (node 22)

Unexpected 'todo' comment: 'TODO: reconfigure eslint to use...'

Check warning on line 541 in src/diffPatch.ts

View workflow job for this annotation

GitHub Actions / test (node 20)

Unexpected 'todo' comment: 'TODO: reconfigure eslint to use...'

Check warning on line 541 in src/diffPatch.ts

View workflow job for this annotation

GitHub Actions / test (node 18)

Unexpected 'todo' comment: 'TODO: reconfigure eslint to use...'
// eslint-disable-next-line no-unused-vars
type CurrentOp = Extract<SanityPatchOperation, {[K in typeof patch.op]: {}}>
const emptyOp = {[patch.op]: {}} as CurrentOp
Expand Down
2 changes: 1 addition & 1 deletion test/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('module api', () => {
test('throws if ids do not match', () => {
const b = {...setAndUnset.b, _id: 'zing'}
expect(() => diffPatch(setAndUnset.a, b)).toThrowError(
`_id on itemA and itemB not present or differs, specify document id the mutations should be applied to`,
`_id on source and target not present or differs, specify document id the mutations should be applied to`,
)
})

Expand Down