@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -46,6 +46,7 @@
46
46
  "@sanity/comlink": "^3.0.4",
47
47
  "@sanity/diff-match-patch": "^3.2.0",
48
48
  "@sanity/diff-patch": "^6.0.0",
49
+ "@sanity/json-match": "^1.0.5",
49
50
  "@sanity/message-protocol": "^0.12.0",
50
51
  "@sanity/mutate": "^0.12.4",
51
52
  "@sanity/types": "^3.83.0",
@@ -68,11 +69,11 @@
68
69
  "typescript": "^5.8.3",
69
70
  "vite": "^6.3.4",
70
71
  "vitest": "^3.1.2",
71
- "@repo/package.bundle": "3.82.0",
72
72
  "@repo/config-eslint": "0.0.0",
73
+ "@repo/config-test": "0.0.1",
73
74
  "@repo/package.config": "0.0.1",
74
75
  "@repo/tsconfig": "0.0.1",
75
- "@repo/config-test": "0.0.1"
76
+ "@repo/package.bundle": "3.82.0"
76
77
  },
77
78
  "engines": {
78
79
  "node": ">=20.0.0"
@@ -100,7 +100,7 @@ export {
100
100
  type TransactionAcceptedEvent,
101
101
  type TransactionRevertedEvent,
102
102
  } from '../document/events'
103
- export {type JsonMatch, jsonMatch} from '../document/patchOperations'
103
+ export {type JsonMatch} from '../document/patchOperations'
104
104
  export {type DocumentPermissionsResult, type PermissionDeniedReason} from '../document/permissions'
105
105
  export type {FavoriteStatusResponse} from '../favorites/favorites'
106
106
  export {getFavoritesState, resolveFavoritesState} from '../favorites/favorites'
@@ -137,4 +137,12 @@ export {getUsersState, loadMoreUsers, resolveUsers} from '../users/usersStore'
137
137
  export {type FetcherStore, type FetcherStoreState} from '../utils/createFetcherStore'
138
138
  export {createGroqSearchFilter} from '../utils/createGroqSearchFilter'
139
139
  export {CORE_SDK_VERSION} from '../version'
140
+ export {
141
+ getIndexForKey,
142
+ getPathDepth,
143
+ joinPaths,
144
+ jsonMatch,
145
+ slicePath,
146
+ stringifyPath,
147
+ } from '@sanity/json-match'
140
148
  export type {CurrentUser, Role, SanityDocument} from '@sanity/types'
@@ -1,5 +1,6 @@
1
1
  import {type Action} from '@sanity/client'
2
2
  import {getPublishedId} from '@sanity/client/csm'
3
+ import {jsonMatch} from '@sanity/json-match'
3
4
  import {type SanityDocument} from 'groq'
4
5
  import {type ExprNode} from 'groq-js'
5
6
  import {
@@ -36,7 +37,7 @@ import {type DocumentAction} from './actions'
36
37
  import {API_VERSION, INITIAL_OUTGOING_THROTTLE_TIME} from './documentConstants'
37
38
  import {type DocumentEvent, getDocumentEvents} from './events'
38
39
  import {listen, OutOfSyncError} from './listen'
39
- import {type JsonMatch, jsonMatch} from './patchOperations'
40
+ import {type JsonMatch} from './patchOperations'
40
41
  import {calculatePermissions, createGrantsLookup, type DatasetAcl, type Grant} from './permissions'
41
42
  import {ActionError} from './processActions'
42
43
  import {
@@ -192,8 +193,11 @@ const _getDocumentState = bindActionByDataset(
192
193
  // wait for draft and published to be loaded before returning a value
193
194
  if (draft === undefined || published === undefined) return undefined
194
195
  const document = draft ?? published
195
- if (path) return jsonMatch(document, path).at(0)?.value
196
- return document
196
+ if (!path) return document
197
+ const result = jsonMatch(document, path).next()
198
+ if (result.done) return undefined
199
+ const {value} = result.value
200
+ return value
197
201
  },
198
202
  onSubscribe: (context, options: DocumentOptions<string | undefined>) =>
199
203
  manageSubscriberIds(context, options.documentId),
@@ -5,356 +5,16 @@ import {
5
5
  diffMatchPatch,
6
6
  ensureArrayKeysDeep,
7
7
  getDeep,
8
- getIndexForKey,
9
8
  ifRevisionID,
10
9
  inc,
11
10
  insert,
12
- jsonMatch,
13
- parsePath,
14
11
  set,
15
12
  setDeep,
16
13
  setIfMissing,
17
- stringifyPath,
18
14
  unset,
19
15
  unsetDeep,
20
16
  } from './patchOperations'
21
17
 
22
- describe('parsePath', () => {
23
- it('parses an empty string into an empty path', () => {
24
- expect(parsePath('')).toEqual([])
25
- })
26
-
27
- it('parses simple dot notation (simple descent)', () => {
28
- expect(parsePath('friend.name')).toEqual(['friend', 'name'])
29
- })
30
-
31
- it('parses a single property', () => {
32
- expect(parsePath('foo')).toEqual(['foo'])
33
- })
34
-
35
- it('parses a property with a single numeric index', () => {
36
- expect(parsePath('items[0]')).toEqual(['items', 0])
37
- })
38
-
39
- it('parses a property with a negative index', () => {
40
- expect(parsePath('items[-1]')).toEqual(['items', -1])
41
- })
42
-
43
- it('parses a property with a keyed segment using double quotes', () => {
44
- expect(parsePath('items[_key=="value"]')).toEqual(['items', {_key: 'value'}])
45
- })
46
-
47
- it('parses a property with a keyed segment using single quotes', () => {
48
- expect(parsePath("items[_key=='value']")).toEqual(['items', {_key: 'value'}])
49
- })
50
-
51
- it('parses an array range that selects the whole thing (e.g. [:])', () => {
52
- expect(parsePath('array[:]')).toEqual(['array', ['', '']])
53
- })
54
-
55
- it('parses an array range with both start and end (e.g. [1:9])', () => {
56
- expect(parsePath('array[1:9]')).toEqual(['array', [1, 9]])
57
- })
58
-
59
- it('parses an array range with missing end (e.g. [4:])', () => {
60
- expect(parsePath('array[4:]')).toEqual(['array', [4, '']])
61
- })
62
-
63
- it('parses an array range with missing start (e.g. [:4])', () => {
64
- expect(parsePath('array[:4]')).toEqual(['array', ['', 4]])
65
- })
66
-
67
- it('parses multiple bracket expressions in one segment', () => {
68
- expect(parsePath('foo[1][_key=="bar"][2:9]')).toEqual(['foo', 1, {_key: 'bar'}, [2, 9]])
69
- })
70
-
71
- it('parses segments that mix dot and bracket notation', () => {
72
- // "a.b[0].c" splits into: ["a"] + parseSegment("b[0]") + ["c"] → ["a", "b", 0, "c"]
73
- expect(parsePath('a.b[0].c')).toEqual(['a', 'b', 0, 'c'])
74
- })
75
-
76
- it('parses a segment with text before and after a bracket expression', () => {
77
- // "foo[1]bar" → ["foo", 1, "bar"]
78
- expect(parsePath('foo[1]bar')).toEqual(['foo', 1, 'bar'])
79
- })
80
-
81
- it('parses a segment that is only a bracket expression', () => {
82
- expect(parsePath('[0]')).toEqual([0])
83
- })
84
-
85
- it('parses a segment that consists solely of bracket expressions', () => {
86
- // "[1][_key=="a"]" → [1, { _key: "a" }]
87
- expect(parsePath('[1][_key=="a"]')).toEqual([1, {_key: 'a'}])
88
- })
89
-
90
- it('ignores a trailing dot', () => {
91
- // "foo." → ["foo"]
92
- expect(parsePath('foo.')).toEqual(['foo'])
93
- })
94
-
95
- it('ignores a leading dot', () => {
96
- // ".foo" → ["foo"]
97
- expect(parsePath('.foo')).toEqual(['foo'])
98
- })
99
-
100
- it('throws an error when a bracket is not closed', () => {
101
- expect(() => parsePath('foo[1')).toThrowError('Unmatched "[" in segment: "foo[1"')
102
- })
103
-
104
- it('throws an error when bracket content is invalid', () => {
105
- expect(() => parsePath('a[invalid]')).toThrowError('Invalid bracket content: "[invalid]"')
106
- })
107
- })
108
-
109
- describe('stringifyPath', () => {
110
- it('stringifies a single string segment', () => {
111
- expect(stringifyPath(['foo'])).toBe('foo')
112
- })
113
-
114
- it('stringifies multiple string segments using dot notation', () => {
115
- expect(stringifyPath(['friend', 'name'])).toBe('friend.name')
116
- })
117
-
118
- it('stringifies a path with a single numeric index', () => {
119
- expect(stringifyPath(['items', 0])).toBe('items[0]')
120
- })
121
-
122
- it('stringifies a path with a negative index', () => {
123
- expect(stringifyPath(['items', -1])).toBe('items[-1]')
124
- })
125
-
126
- it('stringifies a keyed segment', () => {
127
- expect(stringifyPath(['items', {_key: 'value'}])).toBe('items[_key=="value"]')
128
- })
129
-
130
- it('stringifies an array range with both start and end', () => {
131
- expect(stringifyPath(['array', [1, 9]])).toBe('array[1:9]')
132
- })
133
-
134
- it('stringifies an array range with missing end', () => {
135
- expect(stringifyPath(['array', [4, '']])).toBe('array[4:]')
136
- })
137
-
138
- it('stringifies an array range with missing start', () => {
139
- expect(stringifyPath(['array', ['', 4]])).toBe('array[:4]')
140
- })
141
-
142
- it('stringifies an array range with both start and end missing (i.e. ":")', () => {
143
- expect(stringifyPath(['array', ['', '']])).toBe('array[:]')
144
- })
145
-
146
- it('stringifies a complex combination of segments', () => {
147
- // ["foo", 1, { _key: "bar" }, [2, 9]] → "foo[1][_key=="bar"][2:9]"
148
- expect(stringifyPath(['foo', 1, {_key: 'bar'}, [2, 9]])).toBe('foo[1][_key=="bar"][2:9]')
149
- })
150
-
151
- it('stringifies a path that starts with a non-string segment', () => {
152
- // [0, "foo"] → "[0].foo"
153
- expect(stringifyPath([0, 'foo'])).toBe('[0].foo')
154
- })
155
-
156
- it('stringifies consecutive non-string segments without adding extra dots', () => {
157
- // [0, -1] → "[0][-1]"
158
- expect(stringifyPath([0, -1])).toBe('[0][-1]')
159
- })
160
-
161
- it('stringifies an empty path as an empty string', () => {
162
- expect(stringifyPath([])).toBe('')
163
- })
164
- })
165
-
166
- describe('getIndexForKey', () => {
167
- it('returns undefined when input is not an array', () => {
168
- expect(getIndexForKey(null, 'a')).toBeUndefined()
169
- expect(getIndexForKey(123, 'a')).toBeUndefined()
170
- expect(getIndexForKey('string', 'a')).toBeUndefined()
171
- expect(getIndexForKey({}, 'a')).toBeUndefined()
172
- })
173
-
174
- it('returns undefined when given an empty array', () => {
175
- const arr: unknown[] = []
176
- expect(getIndexForKey(arr, 'any')).toBeUndefined()
177
- })
178
-
179
- it('returns undefined when array items do not have a _key property', () => {
180
- const arr = [{notKey: 'a'}, {foo: 'bar'}]
181
- expect(getIndexForKey(arr, 'a')).toBeUndefined()
182
- })
183
-
184
- it('returns the correct index for keyed segments', () => {
185
- const arr = [{_key: 'a'}, {_key: 'b'}, {_key: 'c'}]
186
- expect(getIndexForKey(arr, 'a')).toBe(0)
187
- expect(getIndexForKey(arr, 'b')).toBe(1)
188
- expect(getIndexForKey(arr, 'c')).toBe(2)
189
- })
190
-
191
- it('returns undefined when the key is not found in the array', () => {
192
- const arr = [{_key: 'a'}, {_key: 'b'}]
193
- expect(getIndexForKey(arr, 'c')).toBeUndefined()
194
- })
195
-
196
- it('returns the index for the last occurrence when duplicate keys exist', () => {
197
- const arr = [{_key: 'dup'}, {_key: 'dup'}]
198
- // Because the lookup overwrites earlier values,
199
- // the index for "dup" should be the index of the last occurrence.
200
- expect(getIndexForKey(arr, 'dup')).toBe(1)
201
- })
202
-
203
- it('ignores items that are not objects or that lack a _key property', () => {
204
- const arr = [{_key: 'a'}, 42, 'hello', {_key: 'b'}]
205
- expect(getIndexForKey(arr, 'a')).toBe(0)
206
- expect(getIndexForKey(arr, 'b')).toBe(3)
207
- // Even though 42 and "hello" are in the array, they are ignored.
208
- expect(getIndexForKey(arr, '42')).toBeUndefined()
209
- })
210
-
211
- it('caches the index lookup even if the array is mutated after the first call', () => {
212
- const arr = [{_key: 'a'}, {_key: 'b'}]
213
- // First call creates the cache.
214
- expect(getIndexForKey(arr, 'a')).toBe(0)
215
- // Mutate the array element _key.
216
- arr[0]._key = 'changed'
217
- // The cache still returns the original index for "a"
218
- expect(getIndexForKey(arr, 'a')).toBe(0)
219
- // Lookup for the new key is undefined because it was not in the original lookup.
220
- expect(getIndexForKey(arr, 'changed')).toBeUndefined()
221
- })
222
- })
223
-
224
- describe('jsonMatch', () => {
225
- it('returns the input when the path expression is empty', () => {
226
- const input = 42
227
- const result = jsonMatch(input, '')
228
- expect(result).toEqual([{value: 42, path: []}])
229
- })
230
-
231
- it('matches object properties (simple descent)', () => {
232
- const input = {friend: {name: 'Alice'}}
233
- const result = jsonMatch(input, 'friend.name')
234
- expect(result).toEqual([{value: 'Alice', path: ['friend', 'name']}])
235
- })
236
-
237
- it('returns a match with undefined for a missing property', () => {
238
- const input = {friend: {}}
239
- const result = jsonMatch(input, 'friend.name')
240
- // Even though friend.name does not exist, the match is returned with value undefined.
241
- expect(result).toEqual([{value: undefined, path: ['friend', 'name']}])
242
- })
243
-
244
- it('matches an array element by a positive numeric index', () => {
245
- const input = [10, 20, 30]
246
- const result = jsonMatch(input, '[1]')
247
- expect(result).toEqual([{value: 20, path: [1]}])
248
- })
249
-
250
- it('matches an array element by a negative numeric index', () => {
251
- const input = [10, 20, 30]
252
- const result = jsonMatch(input, '[-1]')
253
- expect(result).toEqual([{value: 30, path: [-1]}])
254
- })
255
-
256
- it('returns no match when a numeric index is used on a non-array', () => {
257
- const input = {not: 'an array'}
258
- const result = jsonMatch(input, '[1]')
259
- expect(result).toEqual([])
260
- })
261
-
262
- it('matches multiple elements using an index tuple (range) with both start and end', () => {
263
- const input = ['a', 'b', 'c', 'd', 'e']
264
- const result = jsonMatch(input, '[1:4]')
265
- // The range [1:4] will match indices 1, 2, and 3.
266
- expect(result).toEqual([
267
- {value: 'b', path: [1]},
268
- {value: 'c', path: [2]},
269
- {value: 'd', path: [3]},
270
- ])
271
- })
272
-
273
- it('matches a range with a missing start ([:3])', () => {
274
- const input = ['a', 'b', 'c', 'd']
275
- const result = jsonMatch(input, '[:3]')
276
- // Missing start means start at index 0.
277
- expect(result).toEqual([
278
- {value: 'a', path: [0]},
279
- {value: 'b', path: [1]},
280
- {value: 'c', path: [2]},
281
- ])
282
- })
283
-
284
- it('matches a range with a missing end ([2:])', () => {
285
- const input = ['a', 'b', 'c', 'd', 'e']
286
- const result = jsonMatch(input, '[2:]')
287
- // Missing end means go until the end of the array.
288
- expect(result).toEqual([
289
- {value: 'c', path: [2]},
290
- {value: 'd', path: [3]},
291
- {value: 'e', path: [4]},
292
- ])
293
- })
294
-
295
- it('returns no match for an index tuple when the input is not an array', () => {
296
- const input = {not: 'an array'}
297
- const result = jsonMatch(input, '[1:3]')
298
- expect(result).toEqual([])
299
- })
300
-
301
- it('matches an element in an array by keyed segment', () => {
302
- const input = [{_key: 'bar'}, {_key: 'foo'}, {_key: 'baz'}]
303
- const result = jsonMatch(input, '[_key=="foo"]')
304
- expect(result).toEqual([{value: {_key: 'foo'}, path: [{_key: 'foo'}]}])
305
- })
306
-
307
- it('returns no match for a keyed segment when the input is not an array', () => {
308
- const input = {_key: 'foo'}
309
- const result = jsonMatch(input, '[_key=="foo"]')
310
- expect(result).toEqual([])
311
- })
312
-
313
- it('returns no match for a keyed segment when the key is not found', () => {
314
- const input = [{_key: 'a'}]
315
- const result = jsonMatch(input, '[_key=="b"]')
316
- expect(result).toEqual([])
317
- })
318
-
319
- it('matches a complex nested structure with a range and negative index', () => {
320
- const input = {
321
- friends: [
322
- {name: 'Alice', scores: [10, 20, 30]},
323
- {name: 'Bob', scores: [15, 25, 35]},
324
- {name: 'Charlie', scores: [20, 30, 40]},
325
- ],
326
- }
327
- const result = jsonMatch(input, 'friends[0:2].scores[-1]')
328
- // For friends[0:2], the range selects friend indices 0 and 1.
329
- // For each friend, scores[-1] picks the last element in the scores array.
330
- expect(result).toEqual([
331
- {value: 30, path: ['friends', 0, 'scores', -1]},
332
- {value: 35, path: ['friends', 1, 'scores', -1]},
333
- ])
334
- })
335
-
336
- it('returns no match when trying to access a property on a non-object', () => {
337
- const input = {name: 'John'}
338
- // Here, "name" is a string so it does not have a property "first"
339
- const result = jsonMatch(input, 'name.first')
340
- expect(result).toEqual([])
341
- })
342
-
343
- it('returns a match with undefined for an out-of-bound numeric index', () => {
344
- const input = [1, 2, 3]
345
- // Index 5 is out of bounds; .at(5) returns undefined.
346
- const result = jsonMatch(input, '[5]')
347
- expect(result).toEqual([{value: undefined, path: [5]}])
348
- })
349
-
350
- it('returns an empty match for an index tuple with an empty range', () => {
351
- const input = ['a', 'b']
352
- // A range like [2:2] selects no indices.
353
- const result = jsonMatch(input, '[2:2]')
354
- expect(result).toEqual([])
355
- })
356
- })
357
-
358
18
  describe('getDeep', () => {
359
19
  it('returns the input when the path is empty', () => {
360
20
  const input = {a: 1, b: 2}
@@ -608,10 +268,16 @@ describe('set', () => {
608
268
  })
609
269
  })
610
270
 
611
- it('leaves input unchanged if the path expression matches nothing', () => {
271
+ it('allows setting deeper even if the path expression matches nothing currently', () => {
612
272
  const input = {a: 1}
613
273
  const output = set(input, {'nonexistent.path': 999})
614
- expect(output).toEqual({a: 1})
274
+ expect(output).toEqual({a: 1, nonexistent: {path: 999}})
275
+ })
276
+
277
+ it('creates an item from a key constraint if the key is not present', () => {
278
+ const input = {items: [{_key: 'item1'}]}
279
+ const output = set(input, {'items[_key=="item2"]': {_key: 'item2'}})
280
+ expect(output).toEqual({items: [{_key: 'item1'}, {_key: 'item2'}]})
615
281
  })
616
282
  })
617
283