@sanity/mutator
Version:
A set of models to make it easier to utilize the powerful real time collaborative features of Sanity
1 lines • 149 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../src/document/debug.ts","../src/patch/ImmutableAccessor.ts","../src/util.ts","../src/jsonpath/arrayToJSONMatchPath.ts","../src/jsonpath/descend.ts","../src/jsonpath/tokenize.ts","../src/jsonpath/parse.ts","../src/jsonpath/toPath.ts","../src/jsonpath/Expression.ts","../src/jsonpath/Descender.ts","../src/jsonpath/Matcher.ts","../src/jsonpath/PlainProbe.ts","../src/jsonpath/extractAccessors.ts","../src/jsonpath/extract.ts","../src/jsonpath/extractWithPath.ts","../src/patch/DiffMatchPatch.ts","../src/patch/IncPatch.ts","../src/patch/util.ts","../src/patch/InsertPatch.ts","../src/patch/SetIfMissingPatch.ts","../src/patch/SetPatch.ts","../src/patch/UnsetPatch.ts","../src/patch/parse.ts","../src/patch/Patcher.ts","../src/document/luid.ts","../src/document/Mutation.ts","../src/document/Document.ts","../src/document/SquashingBuffer.ts","../src/document/BufferedDocument.ts"],"sourcesContent":["import debugIt from 'debug'\n\nexport const debug = debugIt('mutator-document')\n","import {type Probe} from '../jsonpath/Probe'\n\n/**\n * An immutable probe/writer for plain JS objects that will never mutate\n * the provided _value in place. Each setter returns a new (wrapped) version\n * of the value.\n */\nexport class ImmutableAccessor implements Probe {\n _value: unknown\n path: (string | number)[]\n\n constructor(value: unknown, path?: (string | number)[]) {\n this._value = value\n this.path = path || []\n }\n\n containerType(): 'array' | 'object' | 'primitive' {\n if (Array.isArray(this._value)) {\n return 'array'\n } else if (this._value !== null && typeof this._value === 'object') {\n return 'object'\n }\n return 'primitive'\n }\n\n // Common reader, supported by all containers\n get(): unknown {\n return this._value\n }\n\n // Array reader\n length(): number {\n if (!Array.isArray(this._value)) {\n throw new Error(\"Won't return length of non-indexable _value\")\n }\n\n return this._value.length\n }\n\n getIndex(i: number): ImmutableAccessor | false | null {\n if (!Array.isArray(this._value)) {\n return false\n }\n\n if (i >= this.length()) {\n return null\n }\n\n return new ImmutableAccessor(this._value[i], this.path.concat(i))\n }\n\n // Object reader\n hasAttribute(key: string): boolean {\n return isRecord(this._value) ? this._value.hasOwnProperty(key) : false\n }\n\n attributeKeys(): string[] {\n return isRecord(this._value) ? Object.keys(this._value) : []\n }\n\n getAttribute(key: string): ImmutableAccessor | null {\n if (!isRecord(this._value)) {\n throw new Error('getAttribute only applies to plain objects')\n }\n\n if (!this.hasAttribute(key)) {\n return null\n }\n\n return new ImmutableAccessor(this._value[key], this.path.concat(key))\n }\n\n // Common writer, supported by all containers\n set(value: unknown): ImmutableAccessor {\n return value === this._value ? this : new ImmutableAccessor(value, this.path)\n }\n\n // array writer interface\n setIndex(i: number, value: unknown): ImmutableAccessor {\n if (!Array.isArray(this._value)) {\n throw new Error('setIndex only applies to arrays')\n }\n\n if (Object.is(value, this._value[i])) {\n return this\n }\n\n const nextValue = this._value.slice()\n nextValue[i] = value\n return new ImmutableAccessor(nextValue, this.path)\n }\n\n setIndexAccessor(i: number, accessor: ImmutableAccessor): ImmutableAccessor {\n return this.setIndex(i, accessor.get())\n }\n\n unsetIndices(indices: number[]): ImmutableAccessor {\n if (!Array.isArray(this._value)) {\n throw new Error('unsetIndices only applies to arrays')\n }\n\n const length = this._value.length\n const nextValue = []\n // Copy every _value _not_ in the indices array over to the newValue\n for (let i = 0; i < length; i++) {\n if (indices.indexOf(i) === -1) {\n nextValue.push(this._value[i])\n }\n }\n return new ImmutableAccessor(nextValue, this.path)\n }\n\n insertItemsAt(pos: number, items: unknown[]): ImmutableAccessor {\n if (!Array.isArray(this._value)) {\n throw new Error('insertItemsAt only applies to arrays')\n }\n\n let nextValue\n if (this._value.length === 0 && pos === 0) {\n nextValue = items\n } else {\n nextValue = this._value.slice(0, pos).concat(items).concat(this._value.slice(pos))\n }\n\n return new ImmutableAccessor(nextValue, this.path)\n }\n\n // Object writer interface\n setAttribute(key: string, value: unknown): ImmutableAccessor {\n if (!isRecord(this._value)) {\n throw new Error('Unable to set attribute of non-object container')\n }\n\n if (Object.is(value, this._value[key])) {\n return this\n }\n\n const nextValue = Object.assign({}, this._value, {[key]: value})\n return new ImmutableAccessor(nextValue, this.path)\n }\n\n setAttributeAccessor(key: string, accessor: ImmutableAccessor): ImmutableAccessor {\n return this.setAttribute(key, accessor.get())\n }\n\n unsetAttribute(key: string): ImmutableAccessor {\n if (!isRecord(this._value)) {\n throw new Error('Unable to unset attribute of non-object container')\n }\n\n const nextValue = Object.assign({}, this._value)\n delete nextValue[key]\n return new ImmutableAccessor(nextValue, this.path)\n }\n}\n\nfunction isRecord(value: unknown): value is {[key: string]: unknown} {\n return value !== null && typeof value === 'object'\n}\n","export function isRecord(value: unknown): value is {[key: string]: unknown} {\n return value !== null && typeof value === 'object'\n}\n","import {type Path, type PathSegment} from '@sanity/types'\n\nimport {isRecord} from '../util'\n\nconst IS_DOTTABLE = /^[a-z_$]+/\n\n/**\n * Converts a path in array form to a JSONPath string\n *\n * @param pathArray - Array of path segments\n * @returns String representation of the path\n * @internal\n */\nexport function arrayToJSONMatchPath(pathArray: Path): string {\n let path = ''\n pathArray.forEach((segment, index) => {\n path += stringifySegment(segment, index === 0)\n })\n return path\n}\n\n// Converts an array of simple values (strings, numbers only) to a jsonmatch path string.\nfunction stringifySegment(\n segment: PathSegment | Record<string, unknown>,\n hasLeading: boolean,\n): string {\n if (typeof segment === 'number') {\n return `[${segment}]`\n }\n\n if (isRecord(segment)) {\n const seg = segment as Record<string, unknown>\n return Object.keys(segment)\n .map((key) => (isPrimitiveValue(seg[key]) ? `[${key}==\"${seg[key]}\"]` : ''))\n .join('')\n }\n\n if (typeof segment === 'string' && IS_DOTTABLE.test(segment)) {\n return hasLeading ? segment : `.${segment}`\n }\n\n return `['${segment}']`\n}\n\nfunction isPrimitiveValue(val: unknown): val is string | number | boolean {\n switch (typeof val) {\n case 'number':\n case 'string':\n case 'boolean':\n return true\n default:\n return false\n }\n}\n","import {type Expr, type PathExpr} from './types'\n\n/**\n * Splits an expression into a set of heads, tails. A head is the next leaf node to\n * check for matches, and a tail is everything that follows it. Matching is done by\n * matching heads, then proceedint to the matching value, splitting the tail into\n * heads and tails and checking the heads against the new value, and so on.\n */\nexport function descend(tail: Expr): [Expr | null, PathExpr | null][] {\n const [head, newTail] = splitIfPath(tail)\n if (!head) {\n throw new Error('Head cannot be null')\n }\n\n return spreadIfUnionHead(head, newTail)\n}\n\n// Split path in [head, tail]\nfunction splitIfPath(tail: Expr): [Expr | null, PathExpr | null] {\n if (tail.type !== 'path') {\n return [tail, null]\n }\n\n const nodes = tail.nodes\n if (nodes.length === 0) {\n return [null, null]\n }\n\n if (nodes.length === 1) {\n return [nodes[0], null]\n }\n\n return [nodes[0], {type: 'path', nodes: nodes.slice(1)}]\n}\n\nfunction concatPaths(path1: PathExpr | null, path2: PathExpr | null): PathExpr | null {\n if (!path1 && !path2) {\n return null\n }\n\n const nodes1 = path1 ? path1.nodes : []\n const nodes2 = path2 ? path2.nodes : []\n return {\n type: 'path',\n nodes: nodes1.concat(nodes2),\n }\n}\n\n// Spreads a union head into several heads/tails\nfunction spreadIfUnionHead(head: Expr, tail: PathExpr | null): [Expr | null, PathExpr | null][] {\n if (head.type !== 'union') {\n return [[head, tail]]\n }\n\n return head.nodes.map((node) => {\n if (node.type === 'path') {\n const [subHead, subTail] = splitIfPath(node)\n return [subHead, concatPaths(subTail, tail)]\n }\n\n return [node, tail]\n })\n}\n","import {\n type IdentifierToken,\n type NumberToken,\n type QuotedToken,\n type SymbolClass,\n type SymbolToken,\n type Token,\n} from './types'\n\n// TODO: Support '*'\n\nconst digitChar = /[0-9]/\nconst attributeCharMatcher = /^[a-zA-Z0-9_]$/\nconst attributeFirstCharMatcher = /^[a-zA-Z_]$/\n\nconst symbols: Record<SymbolClass, string[]> = {\n // NOTE: These are compared against in order of definition,\n // thus '==' must come before '=', '>=' before '>', etc.\n operator: ['..', '.', ',', ':', '?'],\n comparator: ['>=', '<=', '<', '>', '==', '!='],\n keyword: ['$', '@'],\n boolean: ['true', 'false'],\n paren: ['[', ']'],\n}\n\nconst symbolClasses = Object.keys(symbols) as SymbolClass[]\n\ntype TokenizerFn = () => Token | null\n\n/**\n * Tokenizes a jsonpath2 expression\n */\nclass Tokenizer {\n source: string\n i: number\n length: number\n tokenizers: TokenizerFn[]\n\n constructor(path: string) {\n this.source = path\n this.length = path.length\n this.i = 0\n this.tokenizers = [\n this.tokenizeSymbol,\n this.tokenizeIdentifier,\n this.tokenizeNumber,\n this.tokenizeQuoted,\n ].map((fn) => fn.bind(this))\n }\n\n tokenize(): Token[] {\n const result: Token[] = []\n while (!this.EOF()) {\n this.chompWhitespace()\n let token: Token | null = null\n // @todo refactor into a simpler `.find()`?\n const found = this.tokenizers.some((tokenizer) => {\n token = tokenizer()\n return Boolean(token)\n })\n if (!found || !token) {\n throw new Error(`Invalid tokens in jsonpath '${this.source}' @ ${this.i}`)\n }\n result.push(token)\n }\n return result\n }\n\n takeWhile(fn: (character: string) => string | null): string | null {\n const start = this.i\n let result = ''\n while (!this.EOF()) {\n const nextChar = fn(this.source[this.i])\n if (nextChar === null) {\n break\n }\n result += nextChar\n this.i++\n }\n if (this.i === start) {\n return null\n }\n return result\n }\n\n EOF(): boolean {\n return this.i >= this.length\n }\n\n peek(): string | null {\n if (this.EOF()) {\n return null\n }\n return this.source[this.i]\n }\n\n consume(str: string) {\n if (this.i + str.length > this.length) {\n throw new Error(`Expected ${str} at end of jsonpath`)\n }\n if (str === this.source.slice(this.i, this.i + str.length)) {\n this.i += str.length\n } else {\n throw new Error(`Expected \"${str}\", but source contained \"${this.source.slice()}`)\n }\n }\n\n // Tries to match the upcoming bit of string with the provided string. If it matches, returns\n // the string, then advances the read pointer to the next bit. If not, returns null and nothing\n // happens.\n tryConsume(str: string) {\n if (this.i + str.length > this.length) {\n return null\n }\n if (str === this.source.slice(this.i, this.i + str.length)) {\n // When checking symbols that consist of valid attribute characters, we\n // need to make sure we don't inadvertently treat an attribute as a\n // symbol. For example, an attribute 'trueCustomerField' should not be\n // scanned as the boolean symbol \"true\".\n if (str[0].match(attributeCharMatcher)) {\n // check that char following the symbol match is not also an attribute char\n if (this.length > this.i + str.length) {\n const nextChar = this.source[this.i + str.length]\n if (nextChar && nextChar.match(attributeCharMatcher)) {\n return null\n }\n }\n }\n this.i += str.length\n return str\n }\n return null\n }\n\n chompWhitespace(): void {\n this.takeWhile((char): string | null => {\n return char === ' ' ? '' : null\n })\n }\n\n tokenizeQuoted(): QuotedToken | null {\n const quote = this.peek()\n if (quote === \"'\" || quote === '\"') {\n this.consume(quote)\n let escape = false\n const inner = this.takeWhile((char) => {\n if (escape) {\n escape = false\n return char\n }\n if (char === '\\\\') {\n escape = true\n return ''\n }\n if (char != quote) {\n return char\n }\n return null\n })\n this.consume(quote)\n return {\n type: 'quoted',\n value: inner,\n quote: quote === '\"' ? 'double' : 'single',\n }\n }\n return null\n }\n\n tokenizeIdentifier(): IdentifierToken | null {\n let first = true\n const identifier = this.takeWhile((char) => {\n if (first) {\n first = false\n return char.match(attributeFirstCharMatcher) ? char : null\n }\n return char.match(attributeCharMatcher) ? char : null\n })\n if (identifier !== null) {\n return {\n type: 'identifier',\n name: identifier,\n }\n }\n return null\n }\n\n tokenizeNumber(): NumberToken | null {\n const start = this.i\n let dotSeen = false\n let digitSeen = false\n let negative = false\n if (this.peek() === '-') {\n negative = true\n this.consume('-')\n }\n const number = this.takeWhile((char) => {\n if (char === '.' && !dotSeen && digitSeen) {\n dotSeen = true\n return char\n }\n digitSeen = true\n return char.match(digitChar) ? char : null\n })\n if (number !== null) {\n return {\n type: 'number',\n value: negative ? -number : +number,\n raw: negative ? `-${number}` : number,\n }\n }\n // No number, rewind\n this.i = start\n return null\n }\n\n tokenizeSymbol(): SymbolToken | null {\n for (const symbolClass of symbolClasses) {\n const patterns = symbols[symbolClass]\n const symbol = patterns.find((pattern) => this.tryConsume(pattern))\n if (symbol) {\n return {\n type: symbolClass,\n symbol,\n }\n }\n }\n\n return null\n }\n}\n\nexport function tokenize(jsonpath: string): Token[] {\n return new Tokenizer(jsonpath).tokenize()\n}\n","// Converts a string into an abstract syntax tree representation\n\nimport {tokenize} from './tokenize'\nimport {\n type AliasExpr,\n type AttributeExpr,\n type BooleanExpr,\n type ConstraintExpr,\n type IndexExpr,\n type NumberExpr,\n type PathExpr,\n type RangeExpr,\n type RecursiveExpr,\n type StringExpr,\n type Token,\n type UnionExpr,\n} from './types'\n\n// TODO: Support '*'\n\nclass Parser {\n tokens: Token[]\n length: number\n i: number\n\n constructor(path: string) {\n this.tokens = tokenize(path)\n this.length = this.tokens.length\n this.i = 0\n }\n\n parse() {\n return this.parsePath()\n }\n\n EOF() {\n return this.i >= this.length\n }\n\n // Look at upcoming token\n peek() {\n if (this.EOF()) {\n return null\n }\n return this.tokens[this.i]\n }\n\n consume() {\n const result = this.peek()\n this.i += 1\n return result\n }\n\n // Return next token if it matches the pattern\n probe(pattern: Record<string, unknown>): Token | null {\n const token = this.peek()\n if (!token) {\n return null\n }\n\n const record = token as unknown as Record<string, unknown>\n const match = Object.keys(pattern).every((key) => {\n return key in token && pattern[key] === record[key]\n })\n\n return match ? token : null\n }\n\n // Return and consume next token if it matches the pattern\n match(pattern: Partial<Token>): Token | null {\n return this.probe(pattern) ? this.consume() : null\n }\n\n parseAttribute(): AttributeExpr | null {\n const token = this.match({type: 'identifier'})\n if (token && token.type === 'identifier') {\n return {\n type: 'attribute',\n name: token.name,\n }\n }\n const quoted = this.match({type: 'quoted', quote: 'single'})\n if (quoted && quoted.type === 'quoted') {\n return {\n type: 'attribute',\n name: quoted.value || '',\n }\n }\n return null\n }\n\n parseAlias(): AliasExpr | null {\n if (this.match({type: 'keyword', symbol: '@'}) || this.match({type: 'keyword', symbol: '$'})) {\n return {\n type: 'alias',\n target: 'self',\n }\n }\n return null\n }\n\n parseNumber(): NumberExpr | null {\n const token = this.match({type: 'number'})\n if (token && token.type === 'number') {\n return {\n type: 'number',\n value: token.value,\n }\n }\n return null\n }\n\n parseNumberValue(): number | null {\n const expr = this.parseNumber()\n if (expr) {\n return expr.value\n }\n return null\n }\n\n parseSliceSelector(): RangeExpr | IndexExpr | null {\n const start = this.i\n const rangeStart = this.parseNumberValue()\n\n const colon1 = this.match({type: 'operator', symbol: ':'})\n if (!colon1) {\n if (rangeStart === null) {\n // Rewind, this was actually nothing\n this.i = start\n return null\n }\n\n // Unwrap, this was just a single index not followed by colon\n return {type: 'index', value: rangeStart}\n }\n\n const result: RangeExpr = {\n type: 'range',\n start: rangeStart,\n end: this.parseNumberValue(),\n }\n\n const colon2 = this.match({type: 'operator', symbol: ':'})\n if (colon2) {\n result.step = this.parseNumberValue()\n }\n\n if (result.start === null && result.end === null) {\n // rewind, this wasnt' a slice selector\n this.i = start\n return null\n }\n\n return result\n }\n\n parseValueReference(): AttributeExpr | RangeExpr | IndexExpr | null {\n return this.parseAttribute() || this.parseSliceSelector()\n }\n\n parseLiteralValue(): StringExpr | BooleanExpr | NumberExpr | null {\n const literalString = this.match({type: 'quoted', quote: 'double'})\n if (literalString && literalString.type === 'quoted') {\n return {\n type: 'string',\n value: literalString.value || '',\n }\n }\n const literalBoolean = this.match({type: 'boolean'})\n if (literalBoolean && literalBoolean.type === 'boolean') {\n return {\n type: 'boolean',\n value: literalBoolean.symbol === 'true',\n }\n }\n return this.parseNumber()\n }\n\n // TODO: Reorder constraints so that literal value is always on rhs, and variable is always\n // on lhs.\n parseFilterExpression(): ConstraintExpr | null {\n const start = this.i\n const expr = this.parseAttribute() || this.parseAlias()\n if (!expr) {\n return null\n }\n\n if (this.match({type: 'operator', symbol: '?'})) {\n return {\n type: 'constraint',\n operator: '?',\n lhs: expr,\n }\n }\n\n const binOp = this.match({type: 'comparator'})\n if (!binOp || binOp.type !== 'comparator') {\n // No expression, rewind!\n this.i = start\n return null\n }\n\n const lhs = expr\n const rhs = this.parseLiteralValue()\n if (!rhs) {\n throw new Error(`Operator ${binOp.symbol} needs a literal value at the right hand side`)\n }\n\n return {\n type: 'constraint',\n operator: binOp.symbol,\n lhs: lhs,\n rhs: rhs,\n }\n }\n\n parseExpression(): ConstraintExpr | AttributeExpr | RangeExpr | IndexExpr | null {\n return this.parseFilterExpression() || this.parseValueReference()\n }\n\n parseUnion(): UnionExpr | null {\n if (!this.match({type: 'paren', symbol: '['})) {\n return null\n }\n\n const terms = []\n let expr = this.parseFilterExpression() || this.parsePath() || this.parseValueReference()\n while (expr) {\n terms.push(expr)\n // End of union?\n if (this.match({type: 'paren', symbol: ']'})) {\n break\n }\n\n if (!this.match({type: 'operator', symbol: ','})) {\n throw new Error('Expected ]')\n }\n\n expr = this.parseFilterExpression() || this.parsePath() || this.parseValueReference()\n if (!expr) {\n throw new Error(\"Expected expression following ','\")\n }\n }\n\n return {\n type: 'union',\n nodes: terms,\n }\n }\n\n parseRecursive(): RecursiveExpr | null {\n if (!this.match({type: 'operator', symbol: '..'})) {\n return null\n }\n\n const subpath = this.parsePath()\n if (!subpath) {\n throw new Error(\"Expected path following '..' operator\")\n }\n\n return {\n type: 'recursive',\n term: subpath,\n }\n }\n\n parsePath(): PathExpr | AttributeExpr | UnionExpr | RecursiveExpr | null {\n const nodes: (AttributeExpr | UnionExpr | RecursiveExpr)[] = []\n const expr = this.parseAttribute() || this.parseUnion() || this.parseRecursive()\n if (!expr) {\n return null\n }\n\n nodes.push(expr)\n while (!this.EOF()) {\n if (this.match({type: 'operator', symbol: '.'})) {\n const attr = this.parseAttribute()\n if (!attr) {\n throw new Error(\"Expected attribute name following '.\")\n }\n nodes.push(attr)\n continue\n } else if (this.probe({type: 'paren', symbol: '['})) {\n const union = this.parseUnion()\n if (!union) {\n throw new Error(\"Expected union following '['\")\n }\n nodes.push(union)\n } else {\n const recursive = this.parseRecursive()\n if (recursive) {\n nodes.push(recursive)\n }\n break\n }\n }\n\n if (nodes.length === 1) {\n return nodes[0]\n }\n\n return {\n type: 'path',\n nodes: nodes,\n }\n }\n}\n\nexport function parseJsonPath(path: string): PathExpr | AttributeExpr | UnionExpr | RecursiveExpr {\n const parsed = new Parser(path).parse()\n if (!parsed) {\n throw new Error(`Failed to parse JSON path \"${path}\"`)\n }\n return parsed\n}\n","import {type Expr} from './types'\n\n/**\n * Converts a parsed expression back into jsonpath2, roughly -\n * mostly for use with tests.\n *\n * @param expr - Expression to convert to path\n * @returns a string representation of the path\n * @internal\n */\nexport function toPath(expr: Expr): string {\n return toPathInner(expr, false)\n}\n\nfunction toPathInner(expr: Expr, inUnion: boolean): string {\n switch (expr.type) {\n case 'attribute':\n return expr.name\n case 'alias':\n return expr.target === 'self' ? '@' : '$'\n case 'number':\n return `${expr.value}`\n case 'range': {\n const result = []\n if (!inUnion) {\n result.push('[')\n }\n if (expr.start) {\n result.push(`${expr.start}`)\n }\n result.push(':')\n if (expr.end) {\n result.push(`${expr.end}`)\n }\n if (expr.step) {\n result.push(`:${expr.step}`)\n }\n if (!inUnion) {\n result.push(']')\n }\n return result.join('')\n }\n case 'index':\n if (inUnion) {\n return `${expr.value}`\n }\n\n return `[${expr.value}]`\n case 'constraint': {\n const rhs = expr.rhs ? ` ${toPathInner(expr.rhs, false)}` : ''\n const inner = `${toPathInner(expr.lhs, false)} ${expr.operator}${rhs}`\n\n if (inUnion) {\n return inner\n }\n\n return `[${inner}]`\n }\n case 'string':\n return JSON.stringify(expr.value)\n case 'path': {\n const result = []\n const nodes = expr.nodes.slice()\n while (nodes.length > 0) {\n const node = nodes.shift()\n if (node) {\n result.push(toPath(node))\n }\n\n const upcoming = nodes[0]\n if (upcoming && toPathInner(upcoming, false)[0] !== '[') {\n result.push('.')\n }\n }\n return result.join('')\n }\n case 'union':\n return `[${expr.nodes.map((e) => toPathInner(e, true)).join(',')}]`\n default:\n throw new Error(`Unknown node type ${expr.type}`)\n case 'recursive':\n return `..${toPathInner(expr.term, false)}`\n }\n}\n","// A utility wrapper class to process parsed jsonpath expressions\n\nimport {descend} from './descend'\nimport {parseJsonPath} from './parse'\nimport {type Probe} from './Probe'\nimport {toPath} from './toPath'\nimport {type Expr, type HeadTail} from './types'\n\nexport interface Range {\n start: number\n end: number\n step: number\n}\n\nexport class Expression {\n expr: Expr\n\n constructor(expr: Expr | Expression | null) {\n if (!expr) {\n throw new Error('Attempted to create Expression from null-value')\n }\n\n // This is a wrapped expr\n if ('expr' in expr) {\n this.expr = expr.expr\n } else {\n this.expr = expr\n }\n\n if (!('type' in this.expr)) {\n throw new Error('Attempt to create Expression for expression with no type')\n }\n }\n\n isPath(): boolean {\n return this.expr.type === 'path'\n }\n\n isUnion(): boolean {\n return this.expr.type === 'union'\n }\n\n isCollection(): boolean {\n return this.isPath() || this.isUnion()\n }\n\n isConstraint(): boolean {\n return this.expr.type === 'constraint'\n }\n\n isRecursive(): boolean {\n return this.expr.type === 'recursive'\n }\n\n isExistenceConstraint(): boolean {\n return this.expr.type === 'constraint' && this.expr.operator === '?'\n }\n\n isIndex(): boolean {\n return this.expr.type === 'index'\n }\n\n isRange(): boolean {\n return this.expr.type === 'range'\n }\n\n expandRange(probe?: Probe): Range {\n const probeLength = () => {\n if (!probe) {\n throw new Error('expandRange() required a probe that was not passed')\n }\n\n return probe.length()\n }\n\n let start = 'start' in this.expr ? this.expr.start || 0 : 0\n start = interpretNegativeIndex(start, probe)\n let end = 'end' in this.expr ? this.expr.end || probeLength() : probeLength()\n end = interpretNegativeIndex(end, probe)\n const step = 'step' in this.expr ? this.expr.step || 1 : 1\n return {start, end, step}\n }\n\n isAttributeReference(): boolean {\n return this.expr.type === 'attribute'\n }\n\n // Is a range or index -> something referencing indexes\n isIndexReference(): boolean {\n return this.isIndex() || this.isRange()\n }\n\n name(): string {\n return 'name' in this.expr ? this.expr.name : ''\n }\n\n isSelfReference(): boolean {\n return this.expr.type === 'alias' && this.expr.target === 'self'\n }\n\n constraintTargetIsSelf(): boolean {\n return (\n this.expr.type === 'constraint' &&\n this.expr.lhs.type === 'alias' &&\n this.expr.lhs.target === 'self'\n )\n }\n\n constraintTargetIsAttribute(): boolean {\n return this.expr.type === 'constraint' && this.expr.lhs.type === 'attribute'\n }\n\n testConstraint(probe: Probe): boolean {\n const expr = this.expr\n\n if (expr.type === 'constraint' && expr.lhs.type === 'alias' && expr.lhs.target === 'self') {\n if (probe.containerType() !== 'primitive') {\n return false\n }\n\n if (expr.type === 'constraint' && expr.operator === '?') {\n return true\n }\n\n const lhs = probe.get()\n const rhs = expr.rhs && 'value' in expr.rhs ? expr.rhs.value : undefined\n return testBinaryOperator(lhs, expr.operator, rhs)\n }\n\n if (expr.type !== 'constraint') {\n return false\n }\n\n const lhs = expr.lhs\n if (!lhs) {\n throw new Error('No LHS of expression')\n }\n\n if (lhs.type !== 'attribute') {\n throw new Error(`Constraint target ${lhs.type} not supported`)\n }\n\n if (probe.containerType() !== 'object') {\n return false\n }\n\n const lhsValue = probe.getAttribute(lhs.name)\n if (lhsValue === undefined || lhsValue === null || lhsValue.containerType() !== 'primitive') {\n // LHS is void and empty, or it is a collection\n return false\n }\n\n if (this.isExistenceConstraint()) {\n // There is no rhs, and if we're here the key did exist\n return true\n }\n\n const rhs = expr.rhs && 'value' in expr.rhs ? expr.rhs.value : undefined\n return testBinaryOperator(lhsValue.get(), expr.operator, rhs)\n }\n\n pathNodes(): Expr[] {\n return this.expr.type === 'path' ? this.expr.nodes : [this.expr]\n }\n\n prepend(node: Expression): Expression {\n if (!node) {\n return this\n }\n\n return new Expression({\n type: 'path',\n nodes: node.pathNodes().concat(this.pathNodes()),\n })\n }\n\n concat(other: Expression | null): Expression {\n return other ? other.prepend(this) : this\n }\n\n descend(): HeadTail[] {\n return descend(this.expr).map((headTail) => {\n const [head, tail] = headTail\n return {\n head: head ? new Expression(head) : null,\n tail: tail ? new Expression(tail) : null,\n }\n })\n }\n\n unwrapRecursive(): Expression {\n if (this.expr.type !== 'recursive') {\n throw new Error(`Attempt to unwrap recursive on type ${this.expr.type}`)\n }\n\n return new Expression(this.expr.term)\n }\n\n toIndicies(probe?: Probe): number[] {\n if (this.expr.type !== 'index' && this.expr.type !== 'range') {\n throw new Error('Node cannot be converted to indexes')\n }\n\n if (this.expr.type === 'index') {\n return [interpretNegativeIndex(this.expr.value, probe)]\n }\n\n const result: number[] = []\n const range = this.expandRange(probe)\n let {start, end} = range\n if (range.step < 0) {\n ;[start, end] = [end, start]\n }\n\n for (let i = start; i < end; i++) {\n result.push(i)\n }\n\n return result\n }\n\n toFieldReferences(): number[] | string[] {\n if (this.isIndexReference()) {\n return this.toIndicies()\n }\n if (this.expr.type === 'attribute') {\n return [this.expr.name]\n }\n throw new Error(`Can't convert ${this.expr.type} to field references`)\n }\n\n toString(): string {\n return toPath(this.expr)\n }\n\n static fromPath(path: string): Expression {\n const parsed = parseJsonPath(path)\n if (!parsed) {\n throw new Error(`Failed to parse path \"${path}\"`)\n }\n\n return new Expression(parsed)\n }\n\n static attributeReference(name: string): Expression {\n return new Expression({\n type: 'attribute',\n name: name,\n })\n }\n\n static indexReference(i: number): Expression {\n return new Expression({\n type: 'index',\n value: i,\n })\n }\n}\n\n// Tests an operator on two given primitive values\nfunction testBinaryOperator(lhsValue: any, operator: string, rhsValue: any) {\n switch (operator) {\n case '>':\n return lhsValue > rhsValue\n case '>=':\n return lhsValue >= rhsValue\n case '<':\n return lhsValue < rhsValue\n case '<=':\n return lhsValue <= rhsValue\n case '==':\n return lhsValue === rhsValue\n case '!=':\n return lhsValue !== rhsValue\n default:\n throw new Error(`Unsupported binary operator ${operator}`)\n }\n}\n\nfunction interpretNegativeIndex(index: number, probe?: Probe): number {\n if (index >= 0) {\n return index\n }\n\n if (!probe) {\n throw new Error('interpretNegativeIndex() must have a probe when < 0')\n }\n\n return index + probe.length()\n}\n","import {flatten} from 'lodash'\n\nimport {Expression} from './Expression'\nimport {type Probe} from './Probe'\n\n/**\n * Descender models the state of one partial jsonpath evaluation. Head is the\n * next thing to match, tail is the upcoming things once the head is matched.\n */\nexport class Descender {\n head: Expression | null\n tail: Expression | null\n\n constructor(head: Expression | null, tail: Expression | null) {\n this.head = head\n this.tail = tail\n }\n\n // Iterate this descender once processing any constraints that are\n // resolvable on the current value. Returns an array of new descenders\n // that are guaranteed to be without constraints in the head\n iterate(probe: Probe): Descender[] {\n let result: Descender[] = [this]\n if (this.head && this.head.isConstraint()) {\n let anyConstraints = true\n // Keep rewriting constraints until there are none left\n while (anyConstraints) {\n result = flatten(\n result.map((descender) => {\n return descender.iterateConstraints(probe)\n }),\n )\n anyConstraints = result.some((descender) => {\n return descender.head && descender.head.isConstraint()\n })\n }\n }\n return result\n }\n\n isRecursive(): boolean {\n return Boolean(this.head && this.head.isRecursive())\n }\n\n hasArrived(): boolean {\n return this.head === null && this.tail === null\n }\n\n extractRecursives(): Descender[] {\n if (this.head && this.head.isRecursive()) {\n const term = this.head.unwrapRecursive()\n return new Descender(null, term.concat(this.tail)).descend()\n }\n return []\n }\n\n iterateConstraints(probe: Probe): Descender[] {\n const head = this.head\n if (head === null || !head.isConstraint()) {\n // Not a constraint, no rewrite\n return [this]\n }\n\n const result: Descender[] = []\n\n if (probe.containerType() === 'primitive' && head.constraintTargetIsSelf()) {\n if (head.testConstraint(probe)) {\n result.push(...this.descend())\n }\n return result\n }\n\n // The value is an array\n if (probe.containerType() === 'array') {\n const length = probe.length()\n for (let i = 0; i < length; i++) {\n // Push new descenders with constraint translated to literal indices\n // where they match\n const constraint = probe.getIndex(i)\n if (constraint && head.testConstraint(constraint)) {\n result.push(new Descender(new Expression({type: 'index', value: i}), this.tail))\n }\n }\n return result\n }\n\n // The value is an object\n if (probe.containerType() === 'object') {\n if (head.constraintTargetIsSelf()) {\n // There are no matches for target self ('@') on a plain object\n return []\n }\n\n if (head.testConstraint(probe)) {\n return this.descend()\n }\n\n return result\n }\n\n return result\n }\n\n descend(): Descender[] {\n if (!this.tail) {\n return [new Descender(null, null)]\n }\n\n return this.tail.descend().map((ht) => {\n return new Descender(ht.head, ht.tail)\n })\n }\n\n toString(): string {\n const result = ['<']\n if (this.head) {\n result.push(this.head.toString())\n }\n result.push('|')\n if (this.tail) {\n result.push(this.tail.toString())\n }\n result.push('>')\n return result.join('')\n }\n}\n","import {Descender} from './Descender'\nimport {Expression} from './Expression'\nimport {parseJsonPath} from './parse'\nimport {type Probe} from './Probe'\n\ninterface Result<P = unknown> {\n leads: {\n target: Expression\n matcher: Matcher\n }[]\n\n delivery?: {\n targets: Expression[]\n payload: P\n }\n}\n\n/**\n * @internal\n */\nexport class Matcher {\n active: Descender[]\n recursives: Descender[]\n payload: unknown\n\n constructor(active: Descender[], parent?: Matcher) {\n this.active = active || []\n if (parent) {\n this.recursives = parent.recursives\n this.payload = parent.payload\n } else {\n this.recursives = []\n }\n this.extractRecursives()\n }\n\n setPayload(payload: unknown): this {\n this.payload = payload\n return this\n }\n\n // Moves any recursive descenders onto the recursive track, removing them from\n // the active set\n extractRecursives(): void {\n this.active = this.active.filter((descender) => {\n if (descender.isRecursive()) {\n this.recursives.push(...descender.extractRecursives())\n return false\n }\n return true\n })\n }\n\n // Find recursives that are relevant now and should be considered part of the active set\n activeRecursives(probe: Probe): Descender[] {\n return this.recursives.filter((descender) => {\n const head = descender.head\n if (!head) {\n return false\n }\n\n // Constraints are always relevant\n if (head.isConstraint()) {\n return true\n }\n\n // Index references are only relevant for indexable values\n if (probe.containerType() === 'array' && head.isIndexReference()) {\n return true\n }\n\n // Attribute references are relevant for plain objects\n if (probe.containerType() === 'object') {\n return head.isAttributeReference() && probe.hasAttribute(head.name())\n }\n\n return false\n })\n }\n\n match(probe: Probe): Result {\n return this.iterate(probe).extractMatches(probe)\n }\n\n iterate(probe: Probe): Matcher {\n const newActiveSet: Descender[] = []\n this.active.concat(this.activeRecursives(probe)).forEach((descender) => {\n newActiveSet.push(...descender.iterate(probe))\n })\n return new Matcher(newActiveSet, this)\n }\n\n // Returns true if any of the descenders in the active or recursive set\n // consider the current state a final destination\n isDestination(): boolean {\n return this.active.some((descender) => descender.hasArrived())\n }\n\n hasRecursives(): boolean {\n return this.recursives.length > 0\n }\n\n // Returns any payload delivieries and leads that needs to be followed to complete\n // the process.\n extractMatches(probe: Probe): Result {\n const leads: {target: Expression; matcher: Matcher}[] = []\n const targets: Expression[] = []\n this.active.forEach((descender) => {\n if (descender.hasArrived()) {\n // This was already arrived, so matches this value, not descenders\n targets.push(\n new Expression({\n type: 'alias',\n target: 'self',\n }),\n )\n return\n }\n\n const descenderHead = descender.head\n if (!descenderHead) {\n return\n }\n\n if (probe.containerType() === 'array' && !descenderHead.isIndexReference()) {\n // This descender does not match an indexable value\n return\n }\n\n if (probe.containerType() === 'object' && !descenderHead.isAttributeReference()) {\n // This descender never match a plain object\n return\n }\n\n if (descender.tail) {\n // Not arrived yet\n const matcher = new Matcher(descender.descend(), this)\n descenderHead.toFieldReferences().forEach(() => {\n leads.push({\n target: descenderHead,\n matcher: matcher,\n })\n })\n } else {\n // arrived\n targets.push(descenderHead)\n }\n })\n\n // If there are recursive terms, we need to add a lead for every descendant ...\n if (this.hasRecursives()) {\n // The recustives matcher will have no active set, only inherit recursives from this\n const recursivesMatcher = new Matcher([], this)\n if (probe.containerType() === 'array') {\n const length = probe.length()\n for (let i = 0; i < length; i++) {\n leads.push({\n target: Expression.indexReference(i),\n matcher: recursivesMatcher,\n })\n }\n } else if (probe.containerType() === 'object') {\n probe.attributeKeys().forEach((name) => {\n leads.push({\n target: Expression.attributeReference(name),\n matcher: recursivesMatcher,\n })\n })\n }\n }\n\n return targets.length > 0\n ? {leads: leads, delivery: {targets, payload: this.payload}}\n : {leads: leads}\n }\n\n static fromPath(jsonpath: string): Matcher {\n const path = parseJsonPath(jsonpath)\n if (!path) {\n throw new Error(`Failed to parse path from \"${jsonpath}\"`)\n }\n\n const descender = new Descender(null, new Expression(path))\n return new Matcher(descender.descend())\n }\n}\n","import {isRecord} from '../util'\nimport {type Probe} from './Probe'\n\n// A default implementation of a probe for vanilla JS _values\nexport class PlainProbe implements Probe {\n _value: unknown\n path: (string | number)[]\n\n constructor(value: unknown, path?: (string | number)[]) {\n this._value = value\n this.path = path || []\n }\n\n containerType(): 'array' | 'object' | 'primitive' {\n if (Array.isArray(this._value)) {\n return 'array'\n } else if (this._value !== null && typeof this._value === 'object') {\n return 'object'\n }\n return 'primitive'\n }\n\n length(): number {\n if (!Array.isArray(this._value)) {\n throw new Error(\"Won't return length of non-indexable _value\")\n }\n\n return this._value.length\n }\n\n getIndex(i: number): false | null | PlainProbe {\n if (!Array.isArray(this._value)) {\n return false\n }\n\n if (i >= this.length()) {\n return null\n }\n\n return new PlainProbe(this._value[i], this.path.concat(i))\n }\n\n hasAttribute(key: string): boolean {\n if (!isRecord(this._value)) {\n return false\n }\n\n return this._value.hasOwnProperty(key)\n }\n\n attributeKeys(): string[] {\n return isRecord(this._value) ? Object.keys(this._value) : []\n }\n\n getAttribute(key: string): null | PlainProbe {\n if (!isRecord(this._value)) {\n throw new Error('getAttribute only applies to plain objects')\n }\n\n if (!this.hasAttribute(key)) {\n return null\n }\n\n return new PlainProbe(this._value[key], this.path.concat(key))\n }\n\n get(): unknown {\n return this._value\n }\n}\n","import {compact} from 'lodash'\n\nimport {type Expression} from './Expression'\nimport {Matcher} from './Matcher'\nimport {PlainProbe} from './PlainProbe'\nimport {type Probe} from './Probe'\n\nexport function extractAccessors(path: string, value: unknown): Probe[] {\n const result: Probe[] = []\n const matcher = Matcher.fromPath(path).setPayload(function appendResult(values: Probe[]) {\n result.push(...values)\n })\n const accessor = new PlainProbe(value)\n descend(matcher, accessor)\n return result\n}\n\nfunction descend(matcher: Matcher, accessor: Probe) {\n const {leads, delivery} = matcher.match(accessor)\n\n leads.forEach((lead) => {\n accessorsFromTarget(lead.target, accessor).forEach((childAccessor) => {\n descend(lead.matcher, childAccessor)\n })\n })\n\n if (delivery) {\n delivery.targets.forEach((target) => {\n if (typeof delivery.payload === 'function') {\n delivery.payload(accessorsFromTarget(target, accessor))\n }\n })\n }\n}\n\nfunction accessorsFromTarget(target: Expression, accessor: Probe) {\n const result = []\n if (target.isIndexReference()) {\n target.toIndicies(accessor).forEach((i) => {\n result.push(accessor.getIndex(i))\n })\n } else if (target.isAttributeReference()) {\n result.push(accessor.getAttribute(target.name()))\n } else if (target.isSelfReference()) {\n result.push(accessor)\n } else {\n throw new Error(`Unable to derive accessor for target ${target.toString()}`)\n }\n return compact(result)\n}\n","import {extractAccessors} from './extractAccessors'\n\n/**\n * Extracts values matching the given JsonPath\n *\n * @param path - Path to extract\n * @param value - Value to extract from\n * @returns An array of values matching the given path\n * @public\n */\nexport function extract(path: string, value: unknown): unknown[] {\n const accessors = extractAccessors(path, value)\n return accessors.map((acc) => acc.get())\n}\n","import {extractAccessors} from './extractAccessors'\n\n/**\n * Extracts a value for the given JsonPath, and includes the specific path of where it was found\n *\n * @param path - Path to extract\n * @param value - Value to extract from\n * @returns An array of objects with `path` and `value` keys\n * @internal\n */\nexport function extractWithPath(\n path: string,\n value: unknown,\n): {path: (string | number)[]; value: unknown}[] {\n const accessors = extractAccessors(path, value)\n return accessors.map((acc) => ({path: acc.path, value: acc.get()}))\n}\n","import {applyPatches, parsePatch, type Patch} from '@sanity/diff-match-patch'\n\nimport {type Expression} from '../jsonpath'\nimport {type ImmutableAccessor} from './ImmutableAccessor'\n\nfunction applyPatch(patch: Patch[], oldValue: unknown) {\n // Silently avoid patching if the value type is not string\n if (typeof oldValue !== 'string') return oldValue\n const [result] = applyPatches(patch, oldValue, {allowExceedingIndices: true})\n return result\n}\n\nexport class DiffMatchPatch {\n path: string\n dmpPatch: Patch[]\n id: string\n\n constructor(id: string, path: string, dmpPatchSrc: string) {\n this.id = id\n this.path = path\n this.dmpPatch = parsePatch(dmpPatchSrc)\n }\n\n apply(targets: Expression[], accessor: ImmutableAccessor): ImmutableAccessor {\n let result = accessor\n\n // The target must be a container type\n if (result.containerType() === 'primitive') {\n return result\n }\n\n for (const target of targets) {\n if (target.isIndexReference()) {\n for (const index of target.toIndicies(accessor)) {\n // Skip patching unless the index actually currently exists\n const item = result.getIndex(index)\n if (!item) {\n continue\n }\n\n const oldValue = item.get()\n const nextValue = applyPatch(this.dmpPatch, oldValue)\n result = result.setIndex(index, nextValue)\n }\n\n continue\n }\n\n if (target.isAttributeReference() && result.hasAttribute(target.name())) {\n const attribute = result.getAttribute(target.name())\n if (!attribute) {\n continue\n }\n\n const oldValue = attribute.get()\n const nextValue = applyPatch(this.dmpPatch, oldValue)\n result = result.setAttribute(target.name(), nextValue)\n continue\n }\n\n throw new Error(`Unable to apply diffMatchPatch to target ${target.toString()}`)\n }\n\n return result\n }\n}\n","import {type Expression} from '../jsonpath'\nimport {type ImmutableAccessor} from './ImmutableAccessor'\n\nfunction performIncrement(previousValue: unknown, delta: number): number {\n if (typeof previousValue !== 'number' || !Number.isFinite(previousValue)) {\n return previousValue as number\n }\n\n return previousValue + delta\n}\n\nexport class IncPatch {\n path: string\n value: number\n id: string\n\n constructor(id: string, path: string, value: number) {\n this.path = path\n this.value = value\n this.id = id\n }\n\n apply(targets: Expression[], accessor: ImmutableAccessor): ImmutableAccessor {\n let result = accessor\n\n // The target must be a container type\n if (result.containerType() === 'primitive') {\n return result\n }\n\n for (const target of targets) {\n if (target.isIndexReference()) {\n for (const index of target.toIndicies(accessor)) {\n // Skip patching unless the index actually currently exists\n const item = result.getIndex(index)\n if (!item) {\n continue\n }\n\n const previousValue = item.get()\n result = result.setIndex(index, performIncrement(previousValue, this.value))\n }\n\n continue\n }\n\n if (target.isAttributeReference()) {\n const attribute = result.getAttribute(target.name())\n if (!attribute) {\n continue\n }\n\n const previousValue = attribute.get()\n result = result.setAttribute(target.name(), performIncrement(previousValue, this.value))\n continue\n }\n\n throw new Error(`Unable to apply to target ${target.toString()}`)\n }\n\n return result\n }\n}\n","import {type Expression} from '../jsonpath'\nimport {type ImmutableAccessor} from './ImmutableAccessor'\n\nexport function targetsToIndicies(targets: Expression[], accessor: ImmutableAccessor): number[] {\n const result: number[] = []\n targets.forEach((target) => {\n if (target.isIndexReference()) {\n result.push(...target.toIndicies(accessor))\n }\n })\n return result.sort()\n}\n","import {max, min} from 'lodash'\n\nimport {type Expression} from '../jsonpath'\nimport {type ImmutableAccessor} from './ImmutableAccessor'\nimport {targetsToIndicies} from './util'\n\nexport class InsertPatch {\n location: string\n path: string\n items: unknown[]\n id: string\n\n constructor(id: string, location: string, path: string, items: unknown[]) {\n this.id = id\n this.location = location\n this.path = path\n this.items = items\n }\n\n apply(targets: Expression[], accessor: ImmutableAccessor): ImmutableAccessor {\n let result = accessor\n if (accessor.containerType() !== 'array') {\n throw new Error('Attempt to apply insert patch to non-array value')\n }\n\n switch (this.location) {\n case 'before': {\n