@sanity/sdk 2.1.2 → 2.2.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.1.2",
3
+ "version": "2.2.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -43,7 +43,7 @@
43
43
  "prettier": "@sanity/prettier-config",
44
44
  "dependencies": {
45
45
  "@sanity/bifur-client": "^0.4.1",
46
- "@sanity/client": "^7.2.1",
46
+ "@sanity/client": "^7.10.0",
47
47
  "@sanity/comlink": "^3.0.4",
48
48
  "@sanity/diff-match-patch": "^3.2.0",
49
49
  "@sanity/diff-patch": "^6.0.0",
@@ -73,8 +73,8 @@
73
73
  "@repo/config-eslint": "0.0.0",
74
74
  "@repo/config-test": "0.0.1",
75
75
  "@repo/package.config": "0.0.1",
76
- "@repo/tsconfig": "0.0.1",
77
- "@repo/package.bundle": "3.82.0"
76
+ "@repo/package.bundle": "3.82.0",
77
+ "@repo/tsconfig": "0.0.1"
78
78
  },
79
79
  "engines": {
80
80
  "node": ">=20.0.0"
@@ -36,6 +36,41 @@ describe('projects', () => {
36
36
 
37
37
  const result = await resolveProjects(instance)
38
38
  expect(result).toEqual(projects)
39
- expect(list).toHaveBeenCalledWith({includeMembers: false})
39
+ expect(list).toHaveBeenCalledWith({includeMembers: false, organizationId: undefined})
40
+ })
41
+ })
42
+
43
+ describe('projects cache key generation', () => {
44
+ it('generates correct cache keys for different parameter combinations', async () => {
45
+ // Test the getKey function directly by creating a mock store
46
+ const mockGetKey = (
47
+ _instance: SanityInstance,
48
+ options?: {organizationId?: string; includeMembers?: boolean},
49
+ ) => {
50
+ const orgKey = options?.organizationId ? `:org:${options.organizationId}` : ''
51
+ const membersKey = options?.includeMembers === false ? ':no-members' : ''
52
+ return `projects${orgKey}${membersKey}`
53
+ }
54
+
55
+ const mockInstance = {} as SanityInstance
56
+
57
+ // Test default behavior (no options)
58
+ const defaultKey = mockGetKey(mockInstance)
59
+ expect(defaultKey).toBe('projects')
60
+
61
+ // Test with organizationId only
62
+ const orgKey = mockGetKey(mockInstance, {organizationId: 'org123'})
63
+ expect(orgKey).toBe('projects:org:org123')
64
+
65
+ // Test with includeMembers: false only
66
+ const noMembersKey = mockGetKey(mockInstance, {includeMembers: false})
67
+ expect(noMembersKey).toBe('projects:no-members')
68
+
69
+ // Test with both parameters
70
+ const bothKey = mockGetKey(mockInstance, {
71
+ organizationId: 'org123',
72
+ includeMembers: false,
73
+ })
74
+ expect(bothKey).toBe('projects:org:org123:no-members')
40
75
  })
41
76
  })
@@ -7,13 +7,25 @@ const API_VERSION = 'v2025-02-19'
7
7
 
8
8
  const projects = createFetcherStore({
9
9
  name: 'Projects',
10
- getKey: () => 'projects',
11
- fetcher: (instance) => () =>
10
+ getKey: (_instance, options?: {organizationId?: string; includeMembers?: boolean}) => {
11
+ const orgKey = options?.organizationId ? `:org:${options.organizationId}` : ''
12
+ const membersKey = options?.includeMembers === false ? ':no-members' : ''
13
+ return `projects${orgKey}${membersKey}`
14
+ },
15
+ fetcher: (instance) => (options?: {organizationId?: string; includeMembers?: boolean}) =>
12
16
  getClientState(instance, {
13
17
  apiVersion: API_VERSION,
14
18
  scope: 'global',
19
+ requestTagPrefix: 'sanity.sdk.projects',
15
20
  }).observable.pipe(
16
- switchMap((client) => client.observable.projects.list({includeMembers: false})),
21
+ switchMap((client) => {
22
+ const organizationId = options?.organizationId
23
+ return client.observable.projects.list({
24
+ // client method has a type that expects false | undefined
25
+ includeMembers: !options?.includeMembers ? false : undefined,
26
+ organizationId,
27
+ })
28
+ }),
17
29
  ),
18
30
  })
19
31
 
@@ -342,4 +342,90 @@ describe('queryStore', () => {
342
342
  ])
343
343
  unsubscribe2()
344
344
  })
345
+
346
+ it('separates cache entries by implicit perspective (instance.config)', async () => {
347
+ // Mock fetch to return different results based on perspective option
348
+ vi.mocked(fetch).mockImplementation(((_q, _p, options) => {
349
+ const perspective = (options as {perspective?: unknown})?.perspective
350
+ const result = perspective === 'published' ? [{_id: 'pub'}] : [{_id: 'drafts'}]
351
+ return of({result, syncTags: []}).pipe(delay(0)) as unknown as ReturnType<
352
+ SanityClient['observable']['fetch']
353
+ >
354
+ }) as SanityClient['observable']['fetch'])
355
+
356
+ const draftsInstance = createSanityInstance({
357
+ projectId: 'test',
358
+ dataset: 'test',
359
+ perspective: 'drafts',
360
+ })
361
+ const publishedInstance = createSanityInstance({
362
+ projectId: 'test',
363
+ dataset: 'test',
364
+ perspective: 'published',
365
+ })
366
+
367
+ // Same query/options, different implicit perspectives via instance.config
368
+ const sDrafts = getQueryState<{_id: string}[]>(draftsInstance, {query: '*[_type == "movie"]'})
369
+ const sPublished = getQueryState<{_id: string}[]>(publishedInstance, {
370
+ query: '*[_type == "movie"]',
371
+ })
372
+
373
+ const unsubDrafts = sDrafts.subscribe()
374
+ const unsubPublished = sPublished.subscribe()
375
+
376
+ const draftsResult = await firstValueFrom(
377
+ sDrafts.observable.pipe(filter((i) => i !== undefined)),
378
+ )
379
+ const publishedResult = await firstValueFrom(
380
+ sPublished.observable.pipe(filter((i) => i !== undefined)),
381
+ )
382
+
383
+ expect(draftsResult).toEqual([{_id: 'drafts'}])
384
+ expect(publishedResult).toEqual([{_id: 'pub'}])
385
+
386
+ unsubDrafts()
387
+ unsubPublished()
388
+
389
+ draftsInstance.dispose()
390
+ publishedInstance.dispose()
391
+ })
392
+
393
+ it('separates cache entries by explicit perspective in options', async () => {
394
+ vi.mocked(fetch).mockImplementation(((_q, _p, options) => {
395
+ const perspective = (options as {perspective?: unknown})?.perspective
396
+ const result = perspective === 'published' ? [{_id: 'pub'}] : [{_id: 'drafts'}]
397
+ return of({result, syncTags: []}).pipe(delay(0)) as unknown as ReturnType<
398
+ SanityClient['observable']['fetch']
399
+ >
400
+ }) as SanityClient['observable']['fetch'])
401
+
402
+ const base = createSanityInstance({projectId: 'test', dataset: 'test'})
403
+
404
+ const sDrafts = getQueryState<{_id: string}[]>(base, {
405
+ query: '*[_type == "movie"]',
406
+ perspective: 'drafts',
407
+ })
408
+ const sPublished = getQueryState<{_id: string}[]>(base, {
409
+ query: '*[_type == "movie"]',
410
+ perspective: 'published',
411
+ })
412
+
413
+ const unsubDrafts = sDrafts.subscribe()
414
+ const unsubPublished = sPublished.subscribe()
415
+
416
+ const draftsResult = await firstValueFrom(
417
+ sDrafts.observable.pipe(filter((i) => i !== undefined)),
418
+ )
419
+ const publishedResult = await firstValueFrom(
420
+ sPublished.observable.pipe(filter((i) => i !== undefined)),
421
+ )
422
+
423
+ expect(draftsResult).toEqual([{_id: 'drafts'}])
424
+ expect(publishedResult).toEqual([{_id: 'pub'}])
425
+
426
+ unsubDrafts()
427
+ unsubPublished()
428
+
429
+ base.dispose()
430
+ })
345
431
  })
@@ -34,7 +34,11 @@ import {
34
34
  import {type StoreState} from '../store/createStoreState'
35
35
  import {defineStore, type StoreContext} from '../store/defineStore'
36
36
  import {insecureRandomId} from '../utils/ids'
37
- import {QUERY_STATE_CLEAR_DELAY, QUERY_STORE_API_VERSION} from './queryStoreConstants'
37
+ import {
38
+ QUERY_STATE_CLEAR_DELAY,
39
+ QUERY_STORE_API_VERSION,
40
+ QUERY_STORE_DEFAULT_PERSPECTIVE,
41
+ } from './queryStoreConstants'
38
42
  import {
39
43
  addSubscriber,
40
44
  cancelQuery,
@@ -77,6 +81,28 @@ export const getQueryKey = (options: QueryOptions): string => JSON.stringify(opt
77
81
  /** @beta */
78
82
  export const parseQueryKey = (key: string): QueryOptions => JSON.parse(key)
79
83
 
84
+ /**
85
+ * Ensures the query key includes an effective perspective so that
86
+ * implicit differences (e.g. different instance.config.perspective)
87
+ * don't collide in the dataset-scoped store.
88
+ *
89
+ * Since perspectives are unique, we can depend on the release stacks
90
+ * to be correct when we retrieve the results.
91
+ *
92
+ */
93
+ function normalizeOptionsWithPerspective(
94
+ instance: SanityInstance,
95
+ options: QueryOptions,
96
+ ): QueryOptions {
97
+ if (options.perspective !== undefined) return options
98
+ const instancePerspective = instance.config.perspective
99
+ return {
100
+ ...options,
101
+ perspective:
102
+ instancePerspective !== undefined ? instancePerspective : QUERY_STORE_DEFAULT_PERSPECTIVE,
103
+ }
104
+ }
105
+
80
106
  const queryStore = defineStore<QueryStoreState>({
81
107
  name: 'QueryStore',
82
108
  getInitialState: () => ({queries: {}}),
@@ -255,16 +281,16 @@ export function getQueryState(
255
281
  const _getQueryState = bindActionByDataset(
256
282
  queryStore,
257
283
  createStateSourceAction({
258
- selector: ({state}: SelectorContext<QueryStoreState>, options: QueryOptions) => {
284
+ selector: ({state, instance}: SelectorContext<QueryStoreState>, options: QueryOptions) => {
259
285
  if (state.error) throw state.error
260
- const key = getQueryKey(options)
286
+ const key = getQueryKey(normalizeOptionsWithPerspective(instance, options))
261
287
  const queryState = state.queries[key]
262
288
  if (queryState?.error) throw queryState.error
263
289
  return queryState?.result
264
290
  },
265
- onSubscribe: ({state}, options: QueryOptions) => {
291
+ onSubscribe: ({state, instance}, options: QueryOptions) => {
266
292
  const subscriptionId = insecureRandomId()
267
- const key = getQueryKey(options)
293
+ const key = getQueryKey(normalizeOptionsWithPerspective(instance, options))
268
294
 
269
295
  state.set('addSubscriber', addSubscriber(key, subscriptionId))
270
296
 
@@ -314,8 +340,9 @@ export function resolveQuery(...args: Parameters<typeof _resolveQuery>): Promise
314
340
  const _resolveQuery = bindActionByDataset(
315
341
  queryStore,
316
342
  ({state, instance}, {signal, ...options}: ResolveQueryOptions) => {
317
- const {getCurrent} = getQueryState(instance, options)
318
- const key = getQueryKey(options)
343
+ const normalized = normalizeOptionsWithPerspective(instance, options)
344
+ const {getCurrent} = getQueryState(instance, normalized)
345
+ const key = getQueryKey(normalized)
319
346
 
320
347
  const aborted$ = signal
321
348
  ? new Observable<void>((observer) => {
@@ -7,3 +7,4 @@
7
7
  */
8
8
  export const QUERY_STATE_CLEAR_DELAY = 1000
9
9
  export const QUERY_STORE_API_VERSION = 'v2025-05-06'
10
+ export const QUERY_STORE_DEFAULT_PERSPECTIVE = 'drafts'