@sanity/sdk 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,10 @@
1
1
  import {applyPatches, parsePatch} from '@sanity/diff-match-patch'
2
+ import {getIndexForKey, jsonMatch, slicePath, stringifyPath} from '@sanity/json-match'
2
3
  import {
3
4
  type IndexTuple,
4
5
  type InsertPatch,
5
6
  isKeyedObject,
6
7
  isKeySegment,
7
- type KeyedSegment,
8
- type Path,
9
8
  type PathSegment,
10
9
  } from '@sanity/types'
11
10
 
@@ -113,235 +112,6 @@ type DeepGet<TValue, TPath extends readonly (string | number)[]> = TPath extends
113
112
  */
114
113
  export type JsonMatch<TDocument, TPath extends string> = DeepGet<TDocument, PathParts<TPath>>
115
114
 
116
- function parseBracketContent(content: string): PathSegment {
117
- // 1) Range match: ^(\d*):(\d*)$
118
- // - start or end can be empty (meaning "start" or "end" of array)
119
- const rangeMatch = content.match(/^(\d*):(\d*)$/)
120
- if (rangeMatch) {
121
- const startStr = rangeMatch[1]
122
- const endStr = rangeMatch[2]
123
- const start: number | '' = startStr === '' ? '' : parseInt(startStr, 10)
124
- const end: number | '' = endStr === '' ? '' : parseInt(endStr, 10)
125
- return [start, end]
126
- }
127
-
128
- // 2) Keyed segment match: ^_key==["'](.*)["']$
129
- // (We allow either double or single quotes for the value)
130
- const keyedMatch = content.match(/^_key==["'](.+)["']$/)
131
- if (keyedMatch) {
132
- return {_key: keyedMatch[1]}
133
- }
134
-
135
- // 3) Single index (positive or negative)
136
- const index = parseInt(content, 10)
137
- if (!isNaN(index)) {
138
- return index
139
- }
140
-
141
- throw new Error(`Invalid bracket content: "[${content}]"`)
142
- }
143
-
144
- function parseSegment(segment: string): PathSegment[] {
145
- // Each "segment" can contain:
146
- // - A leading property name (optional).
147
- // - Followed by zero or more bracket expressions, e.g. foo[1][_key=="bar"][2:9].
148
- //
149
- // We'll collect these into an array of path segments.
150
-
151
- const segments: PathSegment[] = []
152
- let idx = 0
153
-
154
- // Helper to push a string if it's not empty
155
- function pushIfNotEmpty(text: string) {
156
- if (text) {
157
- segments.push(text)
158
- }
159
- }
160
-
161
- while (idx < segment.length) {
162
- // Look for the next '['
163
- const openIndex = segment.indexOf('[', idx)
164
- if (openIndex === -1) {
165
- // No more brackets – whatever remains is a plain string key
166
- const remaining = segment.slice(idx)
167
- pushIfNotEmpty(remaining)
168
- break
169
- }
170
-
171
- // Push text before this bracket (as a string key) if not empty
172
- const before = segment.slice(idx, openIndex)
173
- pushIfNotEmpty(before)
174
-
175
- // Find the closing bracket
176
- const closeIndex = segment.indexOf(']', openIndex)
177
- if (closeIndex === -1) {
178
- throw new Error(`Unmatched "[" in segment: "${segment}"`)
179
- }
180
-
181
- // Extract the bracket content
182
- const bracketContent = segment.slice(openIndex + 1, closeIndex)
183
- segments.push(parseBracketContent(bracketContent))
184
-
185
- // Move past the bracket
186
- idx = closeIndex + 1
187
- }
188
-
189
- return segments
190
- }
191
-
192
- export function parsePath(path: string): Path {
193
- // We want to split on '.' outside of brackets. A simple approach is
194
- // to track "are we in bracket or not?" while scanning.
195
- const result: Path = []
196
- let buffer = ''
197
- let bracketDepth = 0
198
-
199
- for (let i = 0; i < path.length; i++) {
200
- const ch = path[i]
201
- if (ch === '[') {
202
- bracketDepth++
203
- buffer += ch
204
- } else if (ch === ']') {
205
- bracketDepth--
206
- buffer += ch
207
- } else if (ch === '.' && bracketDepth === 0) {
208
- // We hit a dot at the top level → this ends one segment
209
- if (buffer) {
210
- result.push(...parseSegment(buffer))
211
- buffer = ''
212
- }
213
- } else {
214
- buffer += ch
215
- }
216
- }
217
-
218
- // If there's anything left in the buffer, parse it
219
- if (buffer) {
220
- result.push(...parseSegment(buffer))
221
- }
222
-
223
- return result
224
- }
225
-
226
- export function stringifyPath(path: Path): string {
227
- let result = ''
228
- for (let i = 0; i < path.length; i++) {
229
- const segment = path[i]
230
-
231
- if (typeof segment === 'string') {
232
- // If not the first segment and the previous segment was
233
- // not a bracket form, we add a dot
234
- if (result) {
235
- result += '.'
236
- }
237
- result += segment
238
- } else if (typeof segment === 'number') {
239
- // Single index
240
- result += `[${segment}]`
241
- } else if (Array.isArray(segment)) {
242
- // Index tuple
243
- const [start, end] = segment
244
- const startStr = start === '' ? '' : String(start)
245
- const endStr = end === '' ? '' : String(end)
246
- result += `[${startStr}:${endStr}]`
247
- } else {
248
- // Keyed segment
249
- // e.g. {_key: "someValue"} => [_key=="someValue"]
250
- result += `[_key=="${segment._key}"]`
251
- }
252
- }
253
- return result
254
- }
255
-
256
- type MatchEntry<T = unknown> = {
257
- value: T
258
- path: SingleValuePath
259
- }
260
-
261
- /**
262
- * A very simplified implementation of [JSONMatch][0] that only supports:
263
- * - descent e.g. `friend.name`
264
- * - array index e.g. `items[-1]`
265
- * - array matching with `_key` e.g. `items[_key=="dd9efe09"]`
266
- * - array matching with a range e.g. `items[4:]`
267
- *
268
- * E.g. `friends[_key=="dd9efe09"].address.zip`
269
- *
270
- * [0]: https://www.sanity.io/docs/json-match
271
- *
272
- * @beta
273
- */
274
- export function jsonMatch<TDocument, TPath extends string>(
275
- input: TDocument,
276
- path: TPath,
277
- ): MatchEntry<JsonMatch<TDocument, TPath>>[]
278
- /** @beta */
279
- export function jsonMatch<TValue>(input: unknown, path: string): MatchEntry<TValue>[]
280
- /** @beta */
281
- export function jsonMatch(input: unknown, pathExpression: string): MatchEntry[] {
282
- return matchRecursive(input, parsePath(pathExpression), [])
283
- }
284
-
285
- function matchRecursive(value: unknown, path: Path, currentPath: SingleValuePath): MatchEntry[] {
286
- // If we've consumed the entire path, return the final match
287
- if (path.length === 0) {
288
- return [{value, path: currentPath}]
289
- }
290
-
291
- const [head, ...rest] = path
292
-
293
- // 1) String segment => object property
294
- if (typeof head === 'string') {
295
- if (value && typeof value === 'object' && !Array.isArray(value)) {
296
- const obj = value as Record<string, unknown>
297
- const nextValue = obj[head]
298
- return matchRecursive(nextValue, rest, [...currentPath, head])
299
- }
300
- // If not an object with that property, no match
301
- return []
302
- }
303
-
304
- // 2) Numeric segment => array index
305
- if (typeof head === 'number') {
306
- if (Array.isArray(value)) {
307
- const nextValue = value.at(head)
308
- return matchRecursive(nextValue, rest, [...currentPath, head])
309
- }
310
- // If not an array, no match
311
- return []
312
- }
313
-
314
- // 3) Index tuple => multiple indices
315
- if (Array.isArray(head)) {
316
- // This is a range: [start, end]
317
- if (!Array.isArray(value)) {
318
- // If not an array, no match
319
- return []
320
- }
321
-
322
- const [start, end] = head
323
- // Convert empty strings '' to the start/end of the array
324
- const startIndex = start === '' ? 0 : start
325
- const endIndex = end === '' ? value.length : end
326
-
327
- // We'll accumulate all matches from each index in the range
328
- return value
329
- .slice(startIndex, endIndex)
330
- .flatMap((item, i) => matchRecursive(item, rest, [...currentPath, i + startIndex]))
331
- }
332
-
333
- // 4) Keyed segment => find index in array
334
- // e.g. {_key: 'foo'}
335
- const keyed = head as KeyedSegment
336
- const arrIndex = getIndexForKey(value, keyed._key)
337
- if (arrIndex === undefined || !Array.isArray(value)) {
338
- return []
339
- }
340
-
341
- const nextVal = value[arrIndex]
342
- return matchRecursive(nextVal, rest, [...currentPath, {_key: keyed._key}])
343
- }
344
-
345
115
  // this is a similar array key to the studio:
346
116
  // https://github.com/sanity-io/sanity/blob/v3.74.1/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/createProtoArrayValue.ts
347
117
  function generateArrayKey(length: number = 12): string {
@@ -423,7 +193,7 @@ export function set<R>(input: unknown, pathExpressionValues: Record<string, unkn
423
193
  export function set(input: unknown, pathExpressionValues: Record<string, unknown>): unknown {
424
194
  const result = Object.entries(pathExpressionValues)
425
195
  .flatMap(([pathExpression, replacementValue]) =>
426
- jsonMatch(input, pathExpression).map((matchEntry) => ({
196
+ Array.from(jsonMatch(input, pathExpression)).map((matchEntry) => ({
427
197
  ...matchEntry,
428
198
  replacementValue,
429
199
  })),
@@ -455,7 +225,7 @@ export function setIfMissing(
455
225
  ): unknown {
456
226
  const result = Object.entries(pathExpressionValues)
457
227
  .flatMap(([pathExpression, replacementValue]) => {
458
- return jsonMatch(input, pathExpression).map((matchEntry) => ({
228
+ return Array.from(jsonMatch(input, pathExpression)).map((matchEntry) => ({
459
229
  ...matchEntry,
460
230
  replacementValue,
461
231
  }))
@@ -483,7 +253,7 @@ export function setIfMissing(
483
253
  export function unset<R>(input: unknown, pathExpressions: string[]): R
484
254
  export function unset(input: unknown, pathExpressions: string[]): unknown {
485
255
  const result = pathExpressions
486
- .flatMap((pathExpression) => jsonMatch(input, pathExpression))
256
+ .flatMap((pathExpression) => Array.from(jsonMatch(input, pathExpression)))
487
257
  // ensure that we remove in the reverse order the paths were found in
488
258
  // this is necessary for array unsets so the indexes don't change as we unset
489
259
  .reverse()
@@ -595,18 +365,15 @@ export function insert(input: unknown, {items, ...insertPatch}: InsertPatch): un
595
365
  if (!operation) return input
596
366
  if (typeof pathExpression !== 'string') return input
597
367
 
598
- const parsedPath = parsePath(pathExpression)
599
368
  // in order to do an insert patch, you need to provide at least one path segment
600
- if (!parsedPath.length) return input
369
+ if (!pathExpression.length) return input
601
370
 
602
- const arrayPath = stringifyPath(parsedPath.slice(0, -1))
603
- const positionPath = stringifyPath(parsedPath.slice(-1))
604
-
605
- const arrayMatches = jsonMatch<unknown[]>(input, arrayPath)
371
+ const arrayPath = slicePath(pathExpression, 0, -1)
372
+ const positionPath = slicePath(pathExpression, -1)
606
373
 
607
374
  let result = input
608
375
 
609
- for (const {path, value} of arrayMatches) {
376
+ for (const {path, value} of jsonMatch(input, arrayPath)) {
610
377
  if (!Array.isArray(value)) continue
611
378
  let arr = value
612
379
 
@@ -725,7 +492,7 @@ export function inc<R>(input: unknown, pathExpressionValues: Record<string, numb
725
492
  export function inc(input: unknown, pathExpressionValues: Record<string, number>): unknown {
726
493
  const result = Object.entries(pathExpressionValues)
727
494
  .flatMap(([pathExpression, valueToAdd]) =>
728
- jsonMatch(input, pathExpression).map((matchEntry) => ({
495
+ Array.from(jsonMatch(input, pathExpression)).map((matchEntry) => ({
729
496
  ...matchEntry,
730
497
  valueToAdd,
731
498
  })),
@@ -789,7 +556,9 @@ export function diffMatchPatch(
789
556
  pathExpressionValues: Record<string, string>,
790
557
  ): unknown {
791
558
  const result = Object.entries(pathExpressionValues)
792
- .flatMap(([pathExpression, dmp]) => jsonMatch(input, pathExpression).map((m) => ({...m, dmp})))
559
+ .flatMap(([pathExpression, dmp]) =>
560
+ Array.from(jsonMatch(input, pathExpression)).map((m) => ({...m, dmp})),
561
+ )
793
562
  .filter((i) => i.value !== undefined)
794
563
  .map(({path, value, dmp}) => {
795
564
  if (typeof value !== 'string') {
@@ -832,21 +601,6 @@ export function ifRevisionID(input: unknown, revisionId: string): unknown {
832
601
  return input
833
602
  }
834
603
 
835
- const indexCache = new WeakMap<KeyedSegment[], Record<string, number | undefined>>()
836
- export function getIndexForKey(input: unknown, key: string): number | undefined {
837
- if (!Array.isArray(input)) return undefined
838
- const cached = indexCache.get(input)
839
- if (cached) return cached[key]
840
-
841
- const lookup = input.reduce<Record<string, number | undefined>>((acc, next, index) => {
842
- if (typeof next?._key === 'string') acc[next._key] = index
843
- return acc
844
- }, {})
845
-
846
- indexCache.set(input, lookup)
847
- return lookup[key]
848
- }
849
-
850
604
  /**
851
605
  * Gets a value deep inside of an object given a path. If the path does not
852
606
  * exist in the object, `undefined` will be returned.
@@ -915,7 +669,7 @@ export function setDeep(input: unknown, path: SingleValuePath, value: unknown):
915
669
  if (Array.isArray(input)) {
916
670
  let index: number | undefined
917
671
  if (isKeySegment(currentSegment)) {
918
- index = getIndexForKey(input, currentSegment._key)
672
+ index = getIndexForKey(input, currentSegment._key) ?? input.length
919
673
  } else if (typeof currentSegment === 'number') {
920
674
  // Support negative indexes by computing the proper positive index.
921
675
  index = currentSegment < 0 ? input.length + currentSegment : currentSegment