@sanity/sdk 2.7.0 → 3.0.0-rc.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.
Files changed (99) hide show
  1. package/dist/index.d.ts +228 -239
  2. package/dist/index.js +287 -454
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -4
  5. package/src/_exports/index.ts +16 -17
  6. package/src/agent/agentActions.test.ts +60 -16
  7. package/src/agent/agentActions.ts +29 -20
  8. package/src/auth/authMode.test.ts +0 -25
  9. package/src/auth/authMode.ts +3 -6
  10. package/src/auth/authStore.test.ts +129 -66
  11. package/src/auth/authStore.ts +9 -11
  12. package/src/auth/dashboardAuth.ts +2 -2
  13. package/src/auth/getOrganizationVerificationState.test.ts +10 -11
  14. package/src/auth/handleAuthCallback.test.ts +0 -12
  15. package/src/auth/handleAuthCallback.ts +9 -3
  16. package/src/auth/logout.test.ts +0 -6
  17. package/src/auth/refreshStampedToken.test.ts +121 -17
  18. package/src/auth/standaloneAuth.ts +9 -3
  19. package/src/auth/studioAuth.ts +35 -8
  20. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +9 -3
  21. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -1
  22. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +0 -2
  23. package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
  24. package/src/auth/utils.ts +33 -0
  25. package/src/client/clientStore.test.ts +14 -61
  26. package/src/client/clientStore.ts +52 -28
  27. package/src/comlink/controller/actions/destroyController.test.ts +1 -4
  28. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +1 -4
  29. package/src/comlink/controller/actions/getOrCreateController.test.ts +1 -4
  30. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  31. package/src/comlink/controller/comlinkControllerStore.test.ts +1 -4
  32. package/src/comlink/node/actions/getOrCreateNode.test.ts +1 -4
  33. package/src/comlink/node/actions/releaseNode.test.ts +1 -4
  34. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  35. package/src/comlink/node/getNodeState.test.ts +1 -1
  36. package/src/config/__tests__/handles.test.ts +12 -18
  37. package/src/config/handles.ts +7 -25
  38. package/src/config/sanityConfig.ts +99 -52
  39. package/src/datasets/datasets.test.ts +2 -2
  40. package/src/datasets/datasets.ts +4 -10
  41. package/src/document/actions.test.ts +33 -4
  42. package/src/document/actions.ts +3 -10
  43. package/src/document/applyDocumentActions.test.ts +17 -18
  44. package/src/document/applyDocumentActions.ts +9 -12
  45. package/src/document/documentStore.test.ts +303 -133
  46. package/src/document/documentStore.ts +70 -61
  47. package/src/document/permissions.test.ts +44 -8
  48. package/src/document/processActions.test.ts +77 -7
  49. package/src/document/reducers.test.ts +35 -3
  50. package/src/document/sharedListener.test.ts +13 -13
  51. package/src/document/sharedListener.ts +8 -3
  52. package/src/favorites/favorites.test.ts +10 -2
  53. package/src/presence/presenceStore.test.ts +34 -9
  54. package/src/presence/presenceStore.ts +29 -13
  55. package/src/preview/previewProjectionUtils.test.ts +192 -0
  56. package/src/preview/previewProjectionUtils.ts +88 -0
  57. package/src/preview/{previewStore.ts → types.ts} +6 -25
  58. package/src/project/project.test.ts +1 -1
  59. package/src/project/project.ts +14 -20
  60. package/src/projection/getProjectionState.test.ts +4 -2
  61. package/src/projection/getProjectionState.ts +2 -21
  62. package/src/projection/projectionQuery.ts +2 -3
  63. package/src/projection/projectionStore.test.ts +3 -3
  64. package/src/projection/resolveProjection.test.ts +2 -1
  65. package/src/projection/resolveProjection.ts +2 -18
  66. package/src/projection/subscribeToStateAndFetchBatches.test.ts +2 -2
  67. package/src/projection/subscribeToStateAndFetchBatches.ts +23 -36
  68. package/src/projection/types.ts +1 -9
  69. package/src/projects/projects.test.ts +1 -1
  70. package/src/query/queryStore.test.ts +86 -28
  71. package/src/query/queryStore.ts +23 -38
  72. package/src/releases/getPerspectiveState.test.ts +14 -13
  73. package/src/releases/getPerspectiveState.ts +6 -6
  74. package/src/releases/releasesStore.test.ts +21 -6
  75. package/src/releases/releasesStore.ts +18 -8
  76. package/src/store/createActionBinder.test.ts +114 -111
  77. package/src/store/createActionBinder.ts +52 -101
  78. package/src/store/createSanityInstance.test.ts +13 -83
  79. package/src/store/createSanityInstance.ts +2 -78
  80. package/src/store/createStateSourceAction.test.ts +2 -2
  81. package/src/store/createStateSourceAction.ts +5 -5
  82. package/src/store/createStoreInstance.test.ts +2 -4
  83. package/src/users/reducers.test.ts +1 -6
  84. package/src/users/reducers.ts +2 -2
  85. package/src/users/types.ts +4 -4
  86. package/src/users/usersStore.test.ts +12 -15
  87. package/src/utils/createFetcherStore.test.ts +1 -1
  88. package/src/utils/logger.test.ts +0 -12
  89. package/src/utils/logger.ts +3 -8
  90. package/src/preview/getPreviewState.test.ts +0 -120
  91. package/src/preview/getPreviewState.ts +0 -91
  92. package/src/preview/previewQuery.test.ts +0 -236
  93. package/src/preview/previewQuery.ts +0 -153
  94. package/src/preview/previewStore.test.ts +0 -36
  95. package/src/preview/resolvePreview.test.ts +0 -47
  96. package/src/preview/resolvePreview.ts +0 -20
  97. package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
  98. package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
  99. package/src/preview/util.ts +0 -13
@@ -24,13 +24,13 @@ describe('projectionStore', () => {
24
24
  new Observable(subscriber).subscribe(),
25
25
  )
26
26
 
27
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
27
+ const instance = createSanityInstance()
28
28
 
29
29
  const {state, dispose} = createStoreInstance(
30
30
  instance,
31
31
  {
32
32
  name: 'p.d',
33
- source: {projectId: 'p', dataset: 'd'},
33
+ resource: {projectId: 'p', dataset: 'd'},
34
34
  perspective: 'drafts',
35
35
  },
36
36
  projectionStore,
@@ -42,7 +42,7 @@ describe('projectionStore', () => {
42
42
  state,
43
43
  key: {
44
44
  name: 'p.d',
45
- source: {projectId: 'p', dataset: 'd'},
45
+ resource: {projectId: 'p', dataset: 'd'},
46
46
  perspective: 'drafts',
47
47
  },
48
48
  })
@@ -23,7 +23,7 @@ describe('resolveProjection', () => {
23
23
  } as ProjectionValuePending<Record<string, unknown>>),
24
24
  } as StateSource<ProjectionValuePending<Record<string, unknown>>>)
25
25
 
26
- instance = createSanityInstance({projectId: 'p', dataset: 'd'})
26
+ instance = createSanityInstance()
27
27
  })
28
28
 
29
29
  afterEach(() => {
@@ -34,6 +34,7 @@ describe('resolveProjection', () => {
34
34
  const docHandle = createDocumentHandle({
35
35
  documentId: 'doc123',
36
36
  documentType: 'movie',
37
+ resource: {projectId: 'p', dataset: 'd'},
37
38
  })
38
39
  const projection = '{title}'
39
40
 
@@ -1,27 +1,11 @@
1
- import {type SanityProjectionResult} from 'groq'
2
1
  import {filter, firstValueFrom} from 'rxjs'
3
2
 
4
- import {bindActionBySourceAndPerspective} from '../store/createActionBinder'
3
+ import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
5
4
  import {type SanityInstance} from '../store/createSanityInstance'
6
5
  import {getProjectionState, type ProjectionOptions} from './getProjectionState'
7
6
  import {projectionStore} from './projectionStore'
8
7
  import {type ProjectionValuePending} from './types'
9
8
 
10
- /** @beta */
11
- export function resolveProjection<
12
- TProjection extends string = string,
13
- TDocumentType extends string = string,
14
- TDataset extends string = string,
15
- TProjectId extends string = string,
16
- >(
17
- instance: SanityInstance,
18
- options: ProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
19
- ): Promise<
20
- ProjectionValuePending<
21
- SanityProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
22
- >
23
- >
24
-
25
9
  /** @beta */
26
10
  export function resolveProjection<TData extends object>(
27
11
  instance: SanityInstance,
@@ -38,7 +22,7 @@ export function resolveProjection(
38
22
  /**
39
23
  * @beta
40
24
  */
41
- const _resolveProjection = bindActionBySourceAndPerspective(
25
+ const _resolveProjection = bindActionByResourceAndPerspective(
42
26
  projectionStore,
43
27
  (
44
28
  {instance}: {instance: SanityInstance},
@@ -17,13 +17,13 @@ describe('subscribeToStateAndFetchBatches', () => {
17
17
  let state: StoreState<ProjectionStoreState>
18
18
  const key = {
19
19
  name: 'test.test:drafts',
20
- source: {projectId: 'test', dataset: 'test'},
20
+ resource: {projectId: 'test', dataset: 'test'},
21
21
  perspective: 'drafts' as const,
22
22
  }
23
23
 
24
24
  beforeEach(() => {
25
25
  vi.clearAllMocks()
26
- instance = createSanityInstance({projectId: 'test', dataset: 'test'})
26
+ instance = createSanityInstance()
27
27
  state = createStoreState<ProjectionStoreState>({
28
28
  documentProjections: {},
29
29
  documentStatuses: {},
@@ -16,7 +16,6 @@ import {
16
16
  tap,
17
17
  } from 'rxjs'
18
18
 
19
- import {isDatasetSource} from '../config/sanityConfig'
20
19
  import {getQueryState, resolveQuery} from '../query/queryStore'
21
20
  import {type BoundPerspectiveKey} from '../store/createActionBinder'
22
21
  import {type StoreContext} from '../store/defineStore'
@@ -42,7 +41,7 @@ interface StatusQueryResult {
42
41
  export const subscribeToStateAndFetchBatches = ({
43
42
  state,
44
43
  instance,
45
- key: {source, perspective},
44
+ key: {resource, perspective},
46
45
  }: StoreContext<ProjectionStoreState, BoundPerspectiveKey>): Subscription => {
47
46
  const documentProjections$ = state.observable.pipe(
48
47
  map((s) => s.documentProjections),
@@ -106,29 +105,23 @@ export const subscribeToStateAndFetchBatches = ({
106
105
 
107
106
  const projectionQuery$ = new Observable<ProjectionQueryResult[]>((observer) => {
108
107
  const {getCurrent, observable} = getQueryState<ProjectionQueryResult[]>(instance, {
109
- ...{
110
- query,
111
- params,
112
- tag: PROJECTION_TAG,
113
- perspective,
114
- },
115
- // temporary guard here until we're ready for everything to be queried via global API
116
- ...(source && !isDatasetSource(source) ? {source} : {}),
108
+ query,
109
+ params,
110
+ tag: PROJECTION_TAG,
111
+ perspective,
112
+ resource,
117
113
  })
118
114
 
119
115
  const querySource$ = defer(() => {
120
116
  if (getCurrent() === undefined) {
121
117
  return from(
122
118
  resolveQuery<ProjectionQueryResult[]>(instance, {
123
- ...{
124
- query,
125
- params,
126
- tag: PROJECTION_TAG,
127
- signal: controller.signal,
128
- perspective,
129
- },
130
- // temporary guard here until we're ready for everything to be queried via global API in v3
131
- ...(source && !isDatasetSource(source) ? {source} : {}),
119
+ query,
120
+ params,
121
+ tag: PROJECTION_TAG,
122
+ signal: controller.signal,
123
+ perspective,
124
+ resource,
132
125
  }),
133
126
  ).pipe(switchMap(() => observable))
134
127
  }
@@ -147,29 +140,23 @@ export const subscribeToStateAndFetchBatches = ({
147
140
 
148
141
  const statusQuery$ = new Observable<StatusQueryResult[]>((observer) => {
149
142
  const {getCurrent, observable} = getQueryState<StatusQueryResult[]>(instance, {
150
- ...{
151
- query: statusQuery,
152
- params: statusParams,
153
- tag: PROJECTION_TAG,
154
- perspective: 'raw',
155
- },
156
- // temporary guard here until we're ready for everything to be queried via global API
157
- ...(source && !isDatasetSource(source) ? {source} : {}),
143
+ query: statusQuery,
144
+ params: statusParams,
145
+ tag: PROJECTION_TAG,
146
+ perspective: 'raw',
147
+ resource,
158
148
  })
159
149
 
160
150
  const statusQuerySource$ = defer(() => {
161
151
  if (getCurrent() === undefined) {
162
152
  return from(
163
153
  resolveQuery<StatusQueryResult[]>(instance, {
164
- ...{
165
- query: statusQuery,
166
- params: statusParams,
167
- tag: PROJECTION_TAG,
168
- signal: controller.signal,
169
- perspective: 'raw',
170
- },
171
- // temporary guard here until we're ready for everything to be queried via global API
172
- ...(source && !isDatasetSource(source) ? {source} : {}),
154
+ query: statusQuery,
155
+ params: statusParams,
156
+ tag: PROJECTION_TAG,
157
+ signal: controller.signal,
158
+ perspective: 'raw',
159
+ resource,
173
160
  }),
174
161
  ).pipe(switchMap(() => observable))
175
162
  }
@@ -11,14 +11,6 @@ export interface DocumentProjectionValues<TValue extends object = object> {
11
11
  [projectionHash: string]: ProjectionValuePending<TValue>
12
12
  }
13
13
 
14
- /**
15
- * @public
16
- * @deprecated
17
- * Template literals are a bit too limited, so this type is deprecated.
18
- * Use `string` instead. Projection strings are validated at runtime.
19
- */
20
- export type ValidProjection = string
21
-
22
14
  export interface DocumentProjections {
23
15
  [projectionHash: string]: string
24
16
  }
@@ -29,7 +21,7 @@ interface DocumentProjectionSubscriptions {
29
21
  }
30
22
  }
31
23
 
32
- interface DocumentStatus {
24
+ export interface DocumentStatus {
33
25
  lastEditedDraftAt?: string
34
26
  lastEditedPublishedAt?: string
35
27
  lastEditedVersionAt?: string
@@ -13,7 +13,7 @@ describe('projects', () => {
13
13
  let instance: SanityInstance
14
14
 
15
15
  beforeEach(() => {
16
- instance = createSanityInstance({projectId: 'p', dataset: 'd'})
16
+ instance = createSanityInstance()
17
17
  })
18
18
 
19
19
  afterEach(() => {
@@ -3,7 +3,7 @@ import {delay, filter, firstValueFrom, Observable, of, Subject} from 'rxjs'
3
3
  import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
 
5
5
  import {getClientState} from '../client/clientStore'
6
- import {isCanvasSource} from '../config/sanityConfig'
6
+ import {isCanvasResource} from '../config/sanityConfig'
7
7
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
8
8
  import {type StateSource} from '../store/createStateSourceAction'
9
9
  import {getQueryState, resolveQuery} from './queryStore'
@@ -48,7 +48,7 @@ describe('queryStore', () => {
48
48
  }
49
49
 
50
50
  beforeEach(() => {
51
- instance = createSanityInstance({projectId: 'test', dataset: 'test'})
51
+ instance = createSanityInstance()
52
52
 
53
53
  fetch = vi
54
54
  .fn()
@@ -80,7 +80,7 @@ describe('queryStore', () => {
80
80
 
81
81
  it('initializes query state and cleans up after unsubscribe', async () => {
82
82
  const query = '*[_type == "movie"]'
83
- const state = getQueryState(instance, {query})
83
+ const state = getQueryState(instance, {query, resource: {projectId: 'p', dataset: 'd'}})
84
84
 
85
85
  // Initially undefined before subscription
86
86
  expect(state.getCurrent()).toBeUndefined()
@@ -109,7 +109,7 @@ describe('queryStore', () => {
109
109
 
110
110
  it('maintains state when multiple subscribers exist', async () => {
111
111
  const query = '*[_type == "movie"]'
112
- const state = getQueryState(instance, {query})
112
+ const state = getQueryState(instance, {query, resource: {projectId: 'p', dataset: 'd'}})
113
113
 
114
114
  // Add two subscribers
115
115
  const unsubscribe1 = state.subscribe()
@@ -146,13 +146,13 @@ describe('queryStore', () => {
146
146
  it('resolveQuery works without affecting subscriber cleanup', async () => {
147
147
  const query = '*[_type == "movie"]'
148
148
 
149
- const state = getQueryState(instance, {query})
149
+ const state = getQueryState(instance, {query, resource: {projectId: 'p', dataset: 'd'}})
150
150
 
151
151
  // Check that getQueryState starts undefined
152
152
  expect(state.getCurrent()).toBeUndefined()
153
153
 
154
154
  // Use resolveQuery which should not add a subscriber
155
- const result = await resolveQuery(instance, {query})
155
+ const result = await resolveQuery(instance, {query, resource: {projectId: 'p', dataset: 'd'}})
156
156
  expect(result).toEqual([
157
157
  {_id: 'movie1', _type: 'movie', title: 'Movie 1'},
158
158
  {_id: 'movie2', _type: 'movie', title: 'Movie 2'},
@@ -179,7 +179,11 @@ describe('queryStore', () => {
179
179
  const abortController = new AbortController()
180
180
 
181
181
  // Create a promise that will reject when aborted
182
- const queryPromise = resolveQuery(instance, {query, signal: abortController.signal})
182
+ const queryPromise = resolveQuery(instance, {
183
+ query,
184
+ resource: {projectId: 'p', dataset: 'd'},
185
+ signal: abortController.signal,
186
+ })
183
187
 
184
188
  // Abort the request
185
189
  abortController.abort()
@@ -188,7 +192,9 @@ describe('queryStore', () => {
188
192
  await expect(queryPromise).rejects.toThrow('The operation was aborted.')
189
193
 
190
194
  // Verify state is cleared after abort
191
- expect(getQueryState(instance, {query}).getCurrent()).toBeUndefined()
195
+ expect(
196
+ getQueryState(instance, {query, resource: {projectId: 'p', dataset: 'd'}}).getCurrent(),
197
+ ).toBeUndefined()
192
198
  })
193
199
 
194
200
  it('refetches query when receiving live event with matching sync tag', async () => {
@@ -207,7 +213,10 @@ describe('queryStore', () => {
207
213
  )
208
214
 
209
215
  const query = '*[_type == "movie"]'
210
- const state = getQueryState<{_id: string; _type: string; title: string}[]>(instance, {query})
216
+ const state = getQueryState<{_id: string; _type: string; title: string}[]>(instance, {
217
+ query,
218
+ resource: {projectId: 'p', dataset: 'd'},
219
+ })
211
220
 
212
221
  const unsubscribe = state.subscribe()
213
222
  await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -238,7 +247,7 @@ describe('queryStore', () => {
238
247
  )
239
248
 
240
249
  const query = '*[_type == "movie"]'
241
- const state = getQueryState(instance, {query})
250
+ const state = getQueryState(instance, {query, resource: {projectId: 'p', dataset: 'd'}})
242
251
 
243
252
  const unsubscribe = state.subscribe()
244
253
  await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -271,7 +280,7 @@ describe('queryStore', () => {
271
280
  )
272
281
 
273
282
  const query = '*[_type == "movie"]'
274
- const state = getQueryState(instance, {query})
283
+ const state = getQueryState(instance, {query, resource: {projectId: 'p', dataset: 'd'}})
275
284
 
276
285
  const unsubscribe = state.subscribe()
277
286
  await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -308,7 +317,7 @@ describe('queryStore', () => {
308
317
  )
309
318
 
310
319
  const query = '*[_type == "movie"]'
311
- const state = getQueryState(instance, {query})
320
+ const state = getQueryState(instance, {query, resource: {projectId: 'p', dataset: 'd'}})
312
321
  const unsubscribe = state.subscribe()
313
322
 
314
323
  // Verify error is thrown when accessing state
@@ -319,7 +328,7 @@ describe('queryStore', () => {
319
328
 
320
329
  it('delays query state removal after unsubscribe', async () => {
321
330
  const query = '*[_type == "movie"]'
322
- const state = getQueryState(instance, {query})
331
+ const state = getQueryState(instance, {query, resource: {projectId: 'p', dataset: 'd'}})
323
332
  const unsubscribe = state.subscribe()
324
333
 
325
334
  await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -335,7 +344,7 @@ describe('queryStore', () => {
335
344
 
336
345
  it('preserves query state if a new subscriber subscribes before cleanup delay', async () => {
337
346
  const query = '*[_type == "movie"]'
338
- const state = getQueryState(instance, {query})
347
+ const state = getQueryState(instance, {query, resource: {projectId: 'p', dataset: 'd'}})
339
348
  const unsubscribe1 = state.subscribe()
340
349
 
341
350
  await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -373,20 +382,20 @@ describe('queryStore', () => {
373
382
  }) as SanityClient['observable']['fetch'])
374
383
 
375
384
  const draftsInstance = createSanityInstance({
376
- projectId: 'test',
377
- dataset: 'test',
378
385
  perspective: 'drafts',
379
386
  })
380
387
  const publishedInstance = createSanityInstance({
381
- projectId: 'test',
382
- dataset: 'test',
383
388
  perspective: 'published',
384
389
  })
385
390
 
386
391
  // Same query/options, different implicit perspectives via instance.config
387
- const sDrafts = getQueryState<{_id: string}[]>(draftsInstance, {query: '*[_type == "movie"]'})
392
+ const sDrafts = getQueryState<{_id: string}[]>(draftsInstance, {
393
+ query: '*[_type == "movie"]',
394
+ resource: {projectId: 'p', dataset: 'd'},
395
+ })
388
396
  const sPublished = getQueryState<{_id: string}[]>(publishedInstance, {
389
397
  query: '*[_type == "movie"]',
398
+ resource: {projectId: 'p', dataset: 'd'},
390
399
  })
391
400
 
392
401
  const unsubDrafts = sDrafts.subscribe()
@@ -418,15 +427,17 @@ describe('queryStore', () => {
418
427
  >
419
428
  }) as SanityClient['observable']['fetch'])
420
429
 
421
- const base = createSanityInstance({projectId: 'test', dataset: 'test'})
430
+ const base = createSanityInstance()
422
431
 
423
432
  const sDrafts = getQueryState<{_id: string}[]>(base, {
424
433
  query: '*[_type == "movie"]',
425
434
  perspective: 'drafts',
435
+ resource: {projectId: 'p', dataset: 'd'},
426
436
  })
427
437
  const sPublished = getQueryState<{_id: string}[]>(base, {
428
438
  query: '*[_type == "movie"]',
429
439
  perspective: 'published',
440
+ resource: {projectId: 'p', dataset: 'd'},
430
441
  })
431
442
 
432
443
  const unsubDrafts = sDrafts.subscribe()
@@ -448,11 +459,11 @@ describe('queryStore', () => {
448
459
  base.dispose()
449
460
  })
450
461
 
451
- it('uses source from params when passed in query options (listenForNewSubscribersAndFetch)', async () => {
462
+ it('uses resource from params when passed in query options (listenForNewSubscribersAndFetch)', async () => {
452
463
  const query = '*[_type == "movie"]'
453
- const mediaLibrarySource = {mediaLibraryId: 'ml123'}
464
+ const mediaLibraryResource = {mediaLibraryId: 'ml123'}
454
465
 
455
- const state = getQueryState(instance, {query, source: mediaLibrarySource})
466
+ const state = getQueryState(instance, {query, resource: mediaLibraryResource})
456
467
  const unsubscribe = state.subscribe()
457
468
 
458
469
  await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -462,7 +473,7 @@ describe('queryStore', () => {
462
473
  expect(getClientState).toHaveBeenCalledWith(
463
474
  instance,
464
475
  expect.objectContaining({
465
- source: expect.objectContaining({
476
+ resource: expect.objectContaining({
466
477
  mediaLibraryId: 'ml123',
467
478
  }),
468
479
  }),
@@ -471,11 +482,11 @@ describe('queryStore', () => {
471
482
  unsubscribe()
472
483
  })
473
484
 
474
- it('uses source from store context key when not a dataset source (listenToLiveClientAndSetLastLiveEventIds)', async () => {
485
+ it('uses resource from store context key when not a dataset resource (listenToLiveClientAndSetLastLiveEventIds)', async () => {
475
486
  const query = '*[_type == "movie"]'
476
- const canvasSource = {canvasId: 'canvas456'}
487
+ const canvasResource = {canvasId: 'canvas456'}
477
488
 
478
- const state = getQueryState(instance, {query, source: canvasSource})
489
+ const state = getQueryState(instance, {query, resource: canvasResource})
479
490
  const unsubscribe = state.subscribe()
480
491
 
481
492
  await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -486,10 +497,57 @@ describe('queryStore', () => {
486
497
  const calls = vi.mocked(getClientState).mock.calls
487
498
  const liveClientCall = calls.find(
488
499
  ([_instance, options]) =>
489
- isCanvasSource(options.source!) && options.source.canvasId === 'canvas456',
500
+ isCanvasResource(options.resource!) && options.resource.canvasId === 'canvas456',
490
501
  )
491
502
  expect(liveClientCall).toBeDefined()
492
503
 
493
504
  unsubscribe()
494
505
  })
506
+
507
+ it('uses bound resource when a shared store receives a query with the same explicit resource', async () => {
508
+ // Regression test: when a store for source B is first created by an
509
+ // instance (passing source B explicitly), subsequent queries added to
510
+ // that store by a different instance (also passing source B explicitly)
511
+ // must still use resource B for client creation.
512
+ const projectBSource = {projectId: 'project-b', dataset: 'production'}
513
+
514
+ const rootInstance = createSanityInstance()
515
+
516
+ // 1. Root instance queries with an explicit source for project B.
517
+ // This creates the QueryStore:project-b.production store, whose
518
+ // initialize() captures rootInstance in its closure.
519
+ const stateWithResource = getQueryState(rootInstance, {
520
+ query: '*[_type == "author"]',
521
+ resource: projectBSource,
522
+ })
523
+ const unsub1 = stateWithResource.subscribe()
524
+ await firstValueFrom(stateWithResource.observable.pipe(filter((i) => i !== undefined)))
525
+
526
+ vi.mocked(getClientState).mockClear()
527
+
528
+ // 2. A second instance queries the SAME store (same composite key)
529
+ // with the same explicit resource.
530
+ const secondInstance = createSanityInstance()
531
+ const stateWithSameResource = getQueryState(secondInstance, {
532
+ query: '*[_type == "movie"]',
533
+ resource: projectBSource,
534
+ })
535
+ const unsub2 = stateWithSameResource.subscribe()
536
+ await firstValueFrom(stateWithSameResource.observable.pipe(filter((i) => i !== undefined)))
537
+
538
+ // The listener should create a client using the bound source (project B).
539
+ const fetchCalls = vi.mocked(getClientState).mock.calls
540
+ const correctCall = fetchCalls.find(
541
+ ([, options]) =>
542
+ options.resource &&
543
+ 'projectId' in options.resource &&
544
+ options.resource.projectId === 'project-b',
545
+ )
546
+ expect(correctCall).toBeDefined()
547
+
548
+ unsub1()
549
+ unsub2()
550
+ secondInstance.dispose()
551
+ rootInstance.dispose()
552
+ })
495
553
  })
@@ -1,5 +1,4 @@
1
1
  import {CorsOriginError, type ResponseQueryOptions} from '@sanity/client'
2
- import {type SanityQueryResult} from 'groq'
3
2
  import {
4
3
  catchError,
5
4
  combineLatest,
@@ -24,7 +23,7 @@ import {
24
23
  } from 'rxjs'
25
24
 
26
25
  import {getClientState} from '../client/clientStore'
27
- import {type DatasetHandle, isDatasetSource} from '../config/sanityConfig'
26
+ import {type ResourceHandle} from '../config/sanityConfig'
28
27
  /*
29
28
  * Although this is an import dependency cycle, it is not a logical cycle:
30
29
  * 1. queryStore uses getPerspectiveState when resolving release perspectives
@@ -35,7 +34,7 @@ import {type DatasetHandle, isDatasetSource} from '../config/sanityConfig'
35
34
  // eslint-disable-next-line import/no-cycle
36
35
  import {getPerspectiveState} from '../releases/getPerspectiveState'
37
36
  import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
38
- import {bindActionBySource, type BoundSourceKey} from '../store/createActionBinder'
37
+ import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
39
38
  import {type SanityInstance} from '../store/createSanityInstance'
40
39
  import {
41
40
  createStateSourceAction,
@@ -71,7 +70,7 @@ export interface QueryOptions<
71
70
  >
72
71
  extends
73
72
  Pick<ResponseQueryOptions, 'useCdn' | 'cache' | 'next' | 'cacheMode' | 'tag'>,
74
- DatasetHandle<TDataset, TProjectId> {
73
+ ResourceHandle<TDataset, TProjectId> {
75
74
  query: TQuery
76
75
  params?: Record<string, unknown>
77
76
  }
@@ -116,7 +115,7 @@ function normalizeOptionsWithPerspective(
116
115
  }
117
116
  }
118
117
 
119
- const queryStore = defineStore<QueryStoreState, BoundSourceKey>({
118
+ const queryStore = defineStore<QueryStoreState, BoundResourceKey>({
120
119
  name: 'QueryStore',
121
120
  getInitialState: () => ({queries: {}}),
122
121
  initialize(context) {
@@ -137,7 +136,11 @@ const errorHandler = (state: StoreState<{error?: unknown}>) => {
137
136
  return (error: unknown): void => state.set('setError', {error})
138
137
  }
139
138
 
140
- const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QueryStoreState>) => {
139
+ const listenForNewSubscribersAndFetch = ({
140
+ state,
141
+ instance,
142
+ key: {resource: boundResource},
143
+ }: StoreContext<QueryStoreState, BoundResourceKey>) => {
141
144
  return state.observable
142
145
  .pipe(
143
146
  map((s) => new Set(Object.keys(s.queries))),
@@ -169,10 +172,8 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
169
172
  const {
170
173
  query,
171
174
  params,
172
- projectId,
173
- dataset,
174
175
  tag,
175
- source,
176
+ resource,
176
177
  perspective: perspectiveFromOptions,
177
178
  ...restOptions
178
179
  } = parseQueryKey(group$.key)
@@ -182,14 +183,19 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
182
183
  const perspective$ = isReleasePerspective(perspectiveFromOptions)
183
184
  ? getPerspectiveState(instance, {
184
185
  perspective: perspectiveFromOptions,
186
+ resource: resource ?? boundResource,
185
187
  }).observable.pipe(filter(Boolean))
186
188
  : of(perspectiveFromOptions ?? QUERY_STORE_DEFAULT_PERSPECTIVE)
187
189
 
190
+ // Use the store's bound resource as fallback when the query key
191
+ // doesn't include an explicit resource. The store is scoped to a
192
+ // specific resource via bindActionByResource, but the captured
193
+ // `instance` may have a different default resource (e.g. when the
194
+ // store was first created by a caller that passed an explicit
195
+ // resource while using the root app instance).
188
196
  const client$ = getClientState(instance, {
189
197
  apiVersion: QUERY_STORE_API_VERSION,
190
- projectId,
191
- dataset,
192
- source,
198
+ resource: resource ?? boundResource,
193
199
  }).observable
194
200
 
195
201
  return combineLatest({
@@ -225,12 +231,11 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
225
231
  const listenToLiveClientAndSetLastLiveEventIds = ({
226
232
  state,
227
233
  instance,
228
- key: {source},
229
- }: StoreContext<QueryStoreState, BoundSourceKey>) => {
234
+ key: {resource},
235
+ }: StoreContext<QueryStoreState, BoundResourceKey>) => {
230
236
  const liveMessages$ = getClientState(instance, {
231
237
  apiVersion: QUERY_STORE_API_VERSION,
232
- // temporary guard here until we're ready for everything to be queried via global api
233
- ...(source && !isDatasetSource(source) ? {source} : {}),
238
+ resource,
234
239
  }).observable.pipe(
235
240
  switchMap((client) =>
236
241
  defer(() =>
@@ -288,16 +293,6 @@ const listenToLiveClientAndSetLastLiveEventIds = ({
288
293
  *
289
294
  * @beta
290
295
  */
291
- export function getQueryState<
292
- TQuery extends string = string,
293
- TDataset extends string = string,
294
- TProjectId extends string = string,
295
- >(
296
- instance: SanityInstance,
297
- queryOptions: QueryOptions<TQuery, TDataset, TProjectId>,
298
- ): StateSource<SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`> | undefined>
299
-
300
- /** @beta */
301
296
  export function getQueryState<TData>(
302
297
  instance: SanityInstance,
303
298
  queryOptions: QueryOptions,
@@ -315,7 +310,7 @@ export function getQueryState(
315
310
  ): ReturnType<typeof _getQueryState> {
316
311
  return _getQueryState(...args)
317
312
  }
318
- const _getQueryState = bindActionBySource(
313
+ const _getQueryState = bindActionByResource(
319
314
  queryStore,
320
315
  createStateSourceAction({
321
316
  selector: ({state, instance}: SelectorContext<QueryStoreState>, options: QueryOptions) => {
@@ -356,16 +351,6 @@ const _getQueryState = bindActionBySource(
356
351
  *
357
352
  * @beta
358
353
  */
359
- export function resolveQuery<
360
- TQuery extends string = string,
361
- TDataset extends string = string,
362
- TProjectId extends string = string,
363
- >(
364
- instance: SanityInstance,
365
- queryOptions: ResolveQueryOptions<TQuery, TDataset, TProjectId>,
366
- ): Promise<SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>>
367
-
368
- /** @beta */
369
354
  export function resolveQuery<TData>(
370
355
  instance: SanityInstance,
371
356
  queryOptions: ResolveQueryOptions,
@@ -374,7 +359,7 @@ export function resolveQuery<TData>(
374
359
  export function resolveQuery(...args: Parameters<typeof _resolveQuery>): Promise<unknown> {
375
360
  return _resolveQuery(...args)
376
361
  }
377
- const _resolveQuery = bindActionBySource(
362
+ const _resolveQuery = bindActionByResource(
378
363
  queryStore,
379
364
  ({state, instance}, {signal, ...options}: ResolveQueryOptions) => {
380
365
  const normalized = normalizeOptionsWithPerspective(instance, options)