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
101 changes: 52 additions & 49 deletions src/diffPatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ import {type Path, pathToString} from './paths.js'
import {validateProperty} from './validate.js'
import {
type Patch,
type SetPatch,
type UnsetPatch,
type InsertAfterPatch,
type DiffMatchPatch,
type SanityInsertPatch,
type SanityPatch,
type SanitySetPatch,
type SanityUnsetPatch,
type SanityDiffMatchPatch,
type SanityPatchMutation,
type SanityPatchOperations,
type SanitySetPatchOperation,
type SanityUnsetPatchOperation,
type SanityInsertPatchOperation,
type SanityDiffMatchPatchOperation,
} from './patches.js'

/**
Expand Down Expand Up @@ -140,27 +138,35 @@ export function diffPatch(
}

const operations = diffItem(itemA, itemB, basePath, [])
return serializePatches(operations, {id, ifRevisionID: revisionLocked ? ifRevisionID : undefined})
return serializePatches(operations).map((patchOperations, i) => ({
patch: {
id,
// only add `ifRevisionID` to the first patch
...(i === 0 && ifRevisionID && {ifRevisionID}),
...patchOperations,
},
}))
}

/**
* Diffs two items and returns an array of patches.
* Note that this is different from `diffPatch`, which generates _mutations_.
* Generates an array of patch operation objects for Sanity, based on the
* differences between the two passed values
*
* @param itemA - The first item to compare
* @param itemB - The second item to compare
* @param opts - Options for the diff generation
* @param path - Path to the current item
* @param patches - Array of patches to append the results to. Note that this is MUTATED.
* @returns Array of patches
* @param source - The source value to start off with
* @param target - The target value that the patch operations will aim to create
* @param basePath - An optional path that will be prefixed to all subsequent patch operations
* @returns Array of mutations
* @public
*/
export function diffItem(
itemA: unknown,
itemB: unknown,
path: Path = [],
patches: Patch[] = [],
): Patch[] {
export function diffValue(
source: unknown,
target: unknown,
basePath: Path = [],
): SanityPatchOperations[] {
return serializePatches(diffItem(source, target, basePath))
}

function diffItem(itemA: unknown, itemB: unknown, path: Path = [], patches: Patch[] = []): Patch[] {
if (itemA === itemB) {
return patches
}
Expand Down Expand Up @@ -451,65 +457,62 @@ function isNotIgnoredKey(key: string) {
return SYSTEM_KEYS.indexOf(key) === -1
}

function serializePatches(
patches: Patch[],
options: {id: string; ifRevisionID?: string},
): SanityPatchMutation[] {
// mutually exclusive operations
type SanityPatchOperation =
| SanitySetPatchOperation
| SanityUnsetPatchOperation
| SanityInsertPatchOperation
| SanityDiffMatchPatchOperation

function serializePatches(patches: Patch[]): SanityPatchOperation[] {
if (patches.length === 0) {
return []
}

const {id, ifRevisionID} = options
const set = patches.filter((patch): patch is SetPatch => patch.op === 'set')
const unset = patches.filter((patch): patch is UnsetPatch => patch.op === 'unset')
const insert = patches.filter((patch): patch is InsertAfterPatch => patch.op === 'insert')
const dmp = patches.filter((patch): patch is DiffMatchPatch => patch.op === 'diffMatchPatch')
const set = patches.filter((patch) => patch.op === 'set')
const unset = patches.filter((patch) => patch.op === 'unset')
const insert = patches.filter((patch) => patch.op === 'insert')
const dmp = patches.filter((patch) => patch.op === 'diffMatchPatch')

const withSet =
set.length > 0 &&
set.reduce(
(patch: SanitySetPatch, item: SetPatch) => {
set.reduce<SanitySetPatchOperation>(
(patch, item) => {
const path = pathToString(item.path)
patch.set[path] = item.value
return patch
},
{id, set: {}},
{set: {}},
)

const withUnset =
unset.length > 0 &&
unset.reduce(
(patch: SanityUnsetPatch, item: UnsetPatch) => {
unset.reduce<SanityUnsetPatchOperation>(
(patch, item) => {
const path = pathToString(item.path)
patch.unset.push(path)
return patch
},
{id, unset: []},
{unset: []},
)

const withInsert = insert.reduce((acc: SanityInsertPatch[], item: InsertAfterPatch) => {
const withInsert = insert.reduce<SanityInsertPatchOperation[]>((acc, item) => {
const after = pathToString(item.after)
return acc.concat({id, insert: {after, items: item.items}})
return acc.concat({insert: {after, items: item.items}})
}, [])

const withDmp =
dmp.length > 0 &&
dmp.reduce(
(patch: SanityDiffMatchPatch, item: DiffMatchPatch) => {
dmp.reduce<SanityDiffMatchPatchOperation>(
(patch, item) => {
const path = pathToString(item.path)
patch.diffMatchPatch[path] = item.value
return patch
},
{id, diffMatchPatch: {}},
{diffMatchPatch: {}},
)

const patchSet: SanityPatch[] = [withUnset, withSet, withDmp, ...withInsert].filter(
(item): item is SanityPatch => item !== false,
)

return patchSet.map((patch, i) => ({
patch: ifRevisionID && i === 0 ? {...patch, ifRevisionID} : patch,
}))
return [withUnset, withSet, withDmp, ...withInsert].filter((i) => i !== false)
}

function isUniquelyKeyed(arr: unknown[]): arr is KeyedSanityObject[] {
Expand Down
16 changes: 2 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
export {diffPatch, diffItem} from './diffPatch.js'
export {diffPatch, diffValue} from './diffPatch.js'
export {DiffError} from './diffError.js'

export type {
DiffMatchPatch,
InsertAfterPatch,
Patch,
SanityDiffMatchPatch,
SanityInsertPatch,
SanityPatch,
SanityPatchMutation,
SanitySetPatch,
SanityUnsetPatch,
SetPatch,
UnsetPatch,
} from './patches.js'
export type {SanityPatch, SanityPatchMutation, SanityPatchOperations} from './patches.js'

export type {DocumentStub, PatchOptions} from './diffPatch.js'

Expand Down
64 changes: 35 additions & 29 deletions src/patches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import type {Path} from './paths.js'
/**
* A `set` operation
* Replaces the current path, does not merge
* Note: NOT a serializable mutation, see {@link SanitySetPatch} for that
*
* @public
* @internal
*/
export interface SetPatch {
op: 'set'
Expand All @@ -16,9 +15,8 @@ export interface SetPatch {
/**
* A `unset` operation
* Unsets the entire value of the given path
* Note: NOT a serializable mutation, see {@link SanityUnsetPatch} for that
*
* @public
* @internal
*/
export interface UnsetPatch {
op: 'unset'
Expand All @@ -28,9 +26,8 @@ export interface UnsetPatch {
/**
* A `insert` operation
* Inserts the given items _after_ the given path
* Note: NOT a serializable mutation, see {@link SanityInsertPatch} for that
*
* @public
* @internal
*/
export interface InsertAfterPatch {
op: 'insert'
Expand All @@ -41,9 +38,8 @@ export interface InsertAfterPatch {
/**
* A `diffMatchPatch` operation
* Applies the given `value` (unidiff format) to the given path. Must be a string.
* Note: NOT a serializable mutation, see {@link SanityDiffMatchPatch} for that
*
* @public
* @internal
*/
export interface DiffMatchPatch {
op: 'diffMatchPatch'
Expand All @@ -52,9 +48,9 @@ export interface DiffMatchPatch {
}

/**
* A patch containing either a Sanity set, unset, insert or diffMatchPatch operation
* Internal patch representation used during diff generation
*
* @public
* @internal
*/
export type Patch = SetPatch | UnsetPatch | InsertAfterPatch | DiffMatchPatch

Expand All @@ -64,9 +60,8 @@ export type Patch = SetPatch | UnsetPatch | InsertAfterPatch | DiffMatchPatch
*
* @public
*/
export interface SanitySetPatch {
id: string
set: {[key: string]: any}
export interface SanitySetPatchOperation {
set: Record<string, unknown>
}

/**
Expand All @@ -75,8 +70,7 @@ export interface SanitySetPatch {
*
* @public
*/
export interface SanityUnsetPatch {
id: string
export interface SanityUnsetPatchOperation {
unset: string[]
}

Expand All @@ -86,12 +80,11 @@ export interface SanityUnsetPatch {
*
* @public
*/
export interface SanityInsertPatch {
id: string
export interface SanityInsertPatchOperation {
insert:
| {before: string; items: any[]}
| {after: string; items: any[]}
| {replace: string; items: any[]}
| {before: string; items: unknown[]}
| {after: string; items: unknown[]}
| {replace: string; items: unknown[]}
}

/**
Expand All @@ -100,21 +93,34 @@ export interface SanityInsertPatch {
*
* @public
*/
export interface SanityDiffMatchPatch {
id: string
diffMatchPatch: {[key: string]: string}
export interface SanityDiffMatchPatchOperation {
diffMatchPatch: Record<string, string>
}

/**
* A patch containing either a set, unset, insert or diffMatchPatch operation
* Serializable patch operations that can be applied to a Sanity document.
*
* @public
*/
export type SanityPatch =
| SanitySetPatch
| SanityUnsetPatch
| SanityInsertPatch
| SanityDiffMatchPatch
export type SanityPatchOperations = Partial<
SanitySetPatchOperation &
SanityUnsetPatchOperation &
SanityInsertPatchOperation &
SanityDiffMatchPatchOperation
>

/**
* Meant to be used as the body of a {@link SanityPatchMutation}'s `patch` key.
*
* Contains additional properties to target a particular ID and optionally add
* an optimistic lock via [`ifRevisionID`](https://www.sanity.io/docs/content-lake/transactions#k29b2c75639d5).
*
* @public
*/
export interface SanityPatch extends SanityPatchOperations {
id: string
ifRevisionID?: string
}

/**
* A mutation containing a single patch
Expand Down