Skip to content
Merged
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
79 changes: 32 additions & 47 deletions src/diffPatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@
*/
const DMP_MIN_SIZE_FOR_RATIO_CHECK = 10_000

type PrimitiveValue = string | number | boolean | null | undefined

/**
* An object (record) that _may_ have a `_key` property
*
Expand Down Expand Up @@ -157,45 +155,46 @@
return serializePatches(diffItem(source, target, basePath))
}

function diffItem(itemA: unknown, itemB: unknown, path: Path = [], patches: Patch[] = []): Patch[] {
if (itemA === itemB) {
function diffItem(
source: unknown,
target: unknown,
path: Path = [],
patches: Patch[] = [],
): Patch[] {
if (source === target) {
return patches
}

const aType = Array.isArray(itemA) ? 'array' : typeof itemA
const bType = Array.isArray(itemB) ? 'array' : typeof itemB

const aIsUndefined = aType === 'undefined'
const bIsUndefined = bType === 'undefined'

if (aIsUndefined && !bIsUndefined) {
patches.push({op: 'set', path, value: itemB})
if (typeof source === 'string' && typeof target === 'string') {
diffString(source, target, path, patches)
return patches
}

if (!aIsUndefined && bIsUndefined) {
patches.push({op: 'unset', path})
if (Array.isArray(source) && Array.isArray(target)) {
diffArray(source, target, path, patches)
return patches
}

const dataType = aIsUndefined ? bType : aType
const isContainer = dataType === 'object' || dataType === 'array'
if (!isContainer) {
return diffPrimitive(itemA as PrimitiveValue, itemB as PrimitiveValue, path, patches)
if (isRecord(source) && isRecord(target)) {
diffObject(source, target, path, patches)
return patches
}

if (aType !== bType) {
// Array => Object / Object => Array
patches.push({op: 'set', path, value: itemB})
if (target === undefined) {
patches.push({op: 'unset', path})
return patches
}

return dataType === 'array'
? diffArray(itemA as unknown[], itemB as unknown[], path, patches)
: diffObject(itemA as object, itemB as object, path, patches)
patches.push({op: 'set', path, value: target})
return patches
}

function diffObject(itemA: SanityObject, itemB: SanityObject, path: Path, patches: Patch[]) {
function diffObject(
itemA: Record<string, unknown>,
itemB: Record<string, unknown>,
path: Path,
patches: Patch[],
) {
const atRoot = path.length === 0
const aKeys = Object.keys(itemA)
.filter(atRoot ? isNotIgnoredKey : yes)
Expand Down Expand Up @@ -487,12 +486,7 @@
return true
}

function getDiffMatchPatch(
source: PrimitiveValue,
target: PrimitiveValue,
path: Path,
): DiffMatchPatch | undefined {
if (typeof source !== 'string' || typeof target !== 'string') return undefined
function getDiffMatchPatch(source: string, target: string, path: Path): DiffMatchPatch | undefined {
const last = path.at(-1)
// don't use diff-match-patch for system keys
if (typeof last === 'string' && last.startsWith('_')) return undefined
Expand Down Expand Up @@ -520,22 +514,9 @@
}
}

function diffPrimitive(
itemA: PrimitiveValue,
itemB: PrimitiveValue,
path: Path,
patches: Patch[],
): Patch[] {
const dmp = getDiffMatchPatch(itemA, itemB, path)

patches.push(
dmp || {
op: 'set',
path,
value: itemB,
},
)

function diffString(source: string, target: string, path: Path, patches: Patch[]) {
const dmp = getDiffMatchPatch(source, target, path)
patches.push(dmp ?? {op: 'set', path, value: target})
return patches
}

Expand All @@ -557,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 18)

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 22)

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 Expand Up @@ -676,6 +657,10 @@
return keyToIndexMapping[targetKey]
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && !!value && !Array.isArray(value)
}

/**
* Simplify returns `null` if the value given was `undefined`. This behavior
* is the same as how `JSON.stringify` works so this is relatively expected
Expand Down