UNPKG

@sanity/mutator

Version:

A set of models to make it easier to utilize the powerful real time collaborative features of Sanity

133 lines (114 loc) 5.03 kB
import {type Doc} from '../document/types' import {Matcher} from '../jsonpath' import {ImmutableAccessor} from './ImmutableAccessor' import {parsePatch} from './parse' import {SetIfMissingPatch} from './SetIfMissingPatch' import {SetPatch} from './SetPatch' import {type PatchTypes, type SingleDocumentPatch} from './types' export interface Patch { id: string path: string } export class Patcher { patches: PatchTypes[] constructor(patch: SingleDocumentPatch | SingleDocumentPatch[]) { this.patches = parsePatch(patch) } apply(value: Doc | null): unknown { // Apply just makes a root accessor around the provided // value, then applies the patches. Due to the use of // ImmutableAccessor it is guaranteed to return either the // exact same object it was provided (in the case of no changes), // or a completely new object. It will never mutate the object in place. const accessor = new ImmutableAccessor(value) return this.applyViaAccessor(accessor).get() } // If you want to use your own accessor implementation, you can use this method // to invoke the patcher. Since all subsequent accessors for children of this accessor // are obtained through the methods in the accessors, you retain full control of the // implementation throguhgout the application. Have a look in ImmutableAccessor // to see an example of how accessors are implemented. applyViaAccessor(accessor: ImmutableAccessor): ImmutableAccessor { let result = accessor const idAccessor = accessor.getAttribute('_id') if (!idAccessor) { throw new Error('Cannot apply patch to document with no _id') } const id = idAccessor.get() for (const patch of this.patches) { if (patch.id !== id) { // Ignore patches that are not targetted at this document continue } const matcher = Matcher.fromPath(patch.path).setPayload(patch) result = process(matcher, result) } return result } } // Recursively (depth first) follows any leads generated by the matcher, expecting // a patch to be the payload. When matchers report a delivery, the // apply(targets, accessor) is called on the patch function process(matcher: Matcher, accessor: ImmutableAccessor) { const isSetPatch = matcher.payload instanceof SetPatch || matcher.payload instanceof SetIfMissingPatch let result = accessor // Every time we execute the matcher a new set of leads is generated. Each lead // is a target (being an index, an attribute name or a range) in the form of an // Expression instance. For each lead target there is also a matcher. Our job is to obtain // accessor(s) for each target (there might be more than one, since the targets may // be ranges) and run the provided matcher on those accessors. const {leads, delivery} = matcher.match(accessor) leads.forEach((lead) => { if (lead.target.isIndexReference()) { lead.target.toIndicies().forEach((i) => { const item = result.getIndex(i) if (!item) { throw new Error('Index out of bounds') } result = result.setIndexAccessor(i, process(lead.matcher, item)) }) } else if (lead.target.isAttributeReference()) { // `set`/`setIfMissing` on a primitive value overwrites it if (isSetPatch && result.containerType() === 'primitive') { result = result.set({}) } let oldValueAccessor = result.getAttribute(lead.target.name()) // If the patch is a set/setIfMissing patch, we allow deeply setting properties, // creating missing segments as we go. if (!oldValueAccessor && isSetPatch) { result = result.setAttribute(lead.target.name(), {}) oldValueAccessor = result.getAttribute(lead.target.name()) } if (!oldValueAccessor) { // Don't follow lead, no such attribute return } const newValueAccessor = process(lead.matcher, oldValueAccessor) if (oldValueAccessor !== newValueAccessor) { result = result.setAttributeAccessor(lead.target.name(), newValueAccessor) } } else { throw new Error(`Unable to handle target ${lead.target.toString()}`) } }) // Each time we run the matcher, we might also get a delivery. This means that a // term in the jsonpath terminated here and the patch should be applied. The delivery // arrives in the form of an array of targets and a payload (which in this application // is the patch). Conveniently the patches accept an array of targets and an accessor // to do its work, so here we just pass those to the patch and we're done. if (delivery && isPatcher(delivery.payload)) { const patch = delivery.payload result = patch.apply(delivery.targets, result) } return result } function isPatcher(payload: unknown): payload is PatchTypes { return Boolean( payload && typeof payload === 'object' && payload !== null && 'apply' in payload && typeof (payload as PatchTypes).apply === 'function', ) }