Skip to content
Closed
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
12 changes: 1 addition & 11 deletions src/diffPatch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {makePatches, stringifyPatches} from '@sanity/diff-match-patch'
import {DiffError} from './diffError.js'
import {type Path, pathToString} from './paths.js'
import {type KeyedSanityObject, type Path, pathToString} from './paths.js'
import {validateProperty} from './validate.js'
import {
type Patch,
Expand Down Expand Up @@ -41,16 +41,6 @@

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

/**
* An object (record) that has a `_key` property
*
* @internal
*/
export interface KeyedSanityObject {
[key: string]: unknown
_key: string
}

/**
* An object (record) that _may_ have a `_key` property
*
Expand Down Expand Up @@ -471,7 +461,7 @@
switch (patch.op) {
case 'set':
case 'diffMatchPatch': {
// TODO: reconfigure eslint to use @typescript-eslint/no-unused-vars

Check warning on line 464 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 464 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 464 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
14 changes: 11 additions & 3 deletions src/paths.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import type {KeyedSanityObject} from './diffPatch.js'
/**
* An object (record) that has a `_key` property
*
* @internal
*/
export interface KeyedSanityObject {
[key: string]: unknown
_key: string
}

const IS_DOTTABLE_RE = /^[A-Za-z_][A-Za-z0-9_]*$/

Expand Down Expand Up @@ -54,6 +62,6 @@ export function pathToString(path: Path): string {
}, '')
}

function isKeyedObject(obj: any): obj is KeyedSanityObject {
return typeof obj === 'object' && typeof obj._key === 'string'
export function isKeyedObject(obj: unknown): obj is KeyedSanityObject {
return typeof obj === 'object' && !!obj && '_key' in obj && typeof obj._key === 'string'
}
19 changes: 14 additions & 5 deletions test/data-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {describe, test, expect} from 'vitest'
import {diffPatch} from '../src'
import * as dataTypes from './fixtures/data-types'
import * as typeChange from './fixtures/type-change'
import {applyPatches} from './helpers/applyPatches'

describe('diff data types', () => {
test('same data type', () => {
expect(diffPatch(dataTypes.a, dataTypes.b)).toEqual([
const patches = diffPatch(dataTypes.a, dataTypes.b)
expect(patches).toEqual([
{
patch: {
id: dataTypes.a._id,
Expand Down Expand Up @@ -35,10 +37,12 @@ describe('diff data types', () => {
},
},
])
expect(applyPatches(dataTypes.a, patches)).toEqual(dataTypes.b)
})

test('different data type', () => {
expect(diffPatch(dataTypes.a, dataTypes.c)).toEqual([
const patches = diffPatch(dataTypes.a, dataTypes.c)
expect(patches).toEqual([
{
patch: {
id: dataTypes.a._id,
Expand All @@ -53,24 +57,28 @@ describe('diff data types', () => {
},
},
])
expect(applyPatches(dataTypes.a, patches)).toEqual(dataTypes.c)
})

test('different data type (object => array)', () => {
expect(diffPatch(dataTypes.a, dataTypes.d)).toEqual([
const patches = diffPatch(dataTypes.a, dataTypes.d)
expect(patches).toEqual([
{
patch: {
id: dataTypes.a._id,
set: {slug: ['die-hard-with-a-vengeance']},
},
},
])
expect(applyPatches(dataTypes.a, patches)).toEqual(dataTypes.d)
})

test('type changes', () => {
expect(diffPatch(typeChange.a, typeChange.b)).toEqual([
const patches = diffPatch(typeChange.a, typeChange.b)
expect(patches).toEqual([
{patch: {id: 'abc123', unset: ['unset']}},
{patch: {id: 'abc123', set: {number: 1337}}},
{patch: {diffMatchPatch: {string: '@@ -1,3 +1,3 @@\n-foo\n+bar\n'}, id: 'abc123'}},
{patch: {id: 'abc123', diffMatchPatch: {string: '@@ -1,3 +1,3 @@\n-foo\n+bar\n'}}},
{patch: {id: 'abc123', set: {'array[0]': 0, 'array[1]': 'one', bool: false}}},
{patch: {id: 'abc123', unset: ['array[2].two.levels.deep']}},
{
Expand All @@ -80,5 +88,6 @@ describe('diff data types', () => {
},
},
])
expect(applyPatches(typeChange.a, patches)).toEqual(typeChange.b)
})
})
29 changes: 19 additions & 10 deletions test/diff-match-patch.test.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
import {describe, test, expect} from 'vitest'
import {diffPatch} from '../src'
import * as dmp from './fixtures/dmp'
import {applyPatches} from './helpers/applyPatches'

describe('diff match patch', () => {
test('respects absolute length threshold', () => {
expect(diffPatch(dmp.absoluteIn, dmp.absoluteOut)).toMatchSnapshot()
const patches = diffPatch(dmp.absoluteIn, dmp.absoluteOut)
expect(patches).toMatchSnapshot()
expect(applyPatches(dmp.absoluteIn, patches)).toEqual(dmp.absoluteOut)
})

test('respects relative length threshold', () => {
expect(diffPatch(dmp.relativeOverIn, dmp.relativeOverOut)).toMatchSnapshot()
const patches = diffPatch(dmp.relativeOverIn, dmp.relativeOverOut)
expect(patches).toMatchSnapshot()
expect(applyPatches(dmp.relativeOverIn, patches)).toEqual(dmp.relativeOverOut)
})

test('respects relative length threshold (allowed)', () => {
expect(diffPatch(dmp.relativeUnderIn, dmp.relativeUnderOut)).toMatchSnapshot()
const patches = diffPatch(dmp.relativeUnderIn, dmp.relativeUnderOut)
expect(patches).toMatchSnapshot()
expect(applyPatches(dmp.relativeUnderIn, patches)).toEqual(dmp.relativeUnderOut)
})

test('does not use dmp for "privates" (underscore-prefixed keys)', () => {
expect(diffPatch(dmp.privateChangeIn, dmp.privateChangeOut)).toMatchSnapshot()
const patches = diffPatch(dmp.privateChangeIn, dmp.privateChangeOut)
expect(patches).toMatchSnapshot()
expect(applyPatches(dmp.privateChangeIn, patches)).toEqual(dmp.privateChangeOut)
})

test('does not use dmp for "type changes" (number => string)', () => {
expect(diffPatch(dmp.typeChangeIn, dmp.typeChangeOut)).toMatchSnapshot()
const patches = diffPatch(dmp.typeChangeIn, dmp.typeChangeOut)
expect(patches).toMatchSnapshot()
expect(applyPatches(dmp.typeChangeIn, patches)).toEqual(dmp.typeChangeOut)
})

test('handles patching with unicode surrogate pairs', () => {
expect(
diffPatch(dmp.unicodeChangeIn, dmp.unicodeChangeOut, {
diffMatchPatch: {lengthThresholdAbsolute: 1, lengthThresholdRelative: 3},
}),
).toMatchSnapshot()
const patches = diffPatch(dmp.unicodeChangeIn, dmp.unicodeChangeOut)
expect(patches).toMatchSnapshot()
expect(applyPatches(dmp.unicodeChangeIn, patches)).toEqual(dmp.unicodeChangeOut)
})
})
2 changes: 1 addition & 1 deletion test/fixtures/set-and-unset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import * as nested from './nested'

export const a = {...nested.a, year: 1995, slug: {auto: true, ...nested.a.slug}, arr: [1, 2]}

export const b = {...nested.b, arr: [1, undefined]}
export const b = {...nested.b, arr: [1, null]}
27 changes: 27 additions & 0 deletions test/helpers/applyPatches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type {DocumentStub} from '../../src/diffPatch.js'
import type {SanityPatchMutation} from '../../src/patches.js'
import {ifRevisionID, set, unset, insert, diffMatchPatch} from './patchOperations.js'

const operations = {
ifRevisionID,
set,
unset,
insert,
diffMatchPatch,
}

export function applyPatches(source: DocumentStub, patches: SanityPatchMutation[]): DocumentStub {
let target = source

for (const {patch} of patches) {
const {id, ...patchOperations} = patch

for (const [op, fn] of Object.entries(operations)) {
if (patchOperations[op]) {
target = fn(target, patchOperations[op])
}
}
}

return target
}
Loading