@sanity/sdk 2.8.0 → 2.10.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 (111) hide show
  1. package/dist/_chunks-dts/utils.d.ts +2450 -0
  2. package/dist/_chunks-es/_internal.js +129 -0
  3. package/dist/_chunks-es/_internal.js.map +1 -0
  4. package/dist/_chunks-es/createGroqSearchFilter.js +1537 -0
  5. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
  6. package/dist/_chunks-es/telemetryManager.js +87 -0
  7. package/dist/_chunks-es/telemetryManager.js.map +1 -0
  8. package/dist/_chunks-es/version.js +7 -0
  9. package/dist/_chunks-es/version.js.map +1 -0
  10. package/dist/_exports/_internal.d.ts +64 -0
  11. package/dist/_exports/_internal.js +20 -0
  12. package/dist/_exports/_internal.js.map +1 -0
  13. package/dist/index.d.ts +2 -2343
  14. package/dist/index.js +465 -1813
  15. package/dist/index.js.map +1 -1
  16. package/package.json +17 -12
  17. package/src/_exports/_internal.ts +14 -0
  18. package/src/_exports/index.ts +18 -1
  19. package/src/auth/authStore.test.ts +150 -1
  20. package/src/auth/authStore.ts +11 -11
  21. package/src/auth/dashboardAuth.ts +2 -2
  22. package/src/auth/handleAuthCallback.ts +9 -3
  23. package/src/auth/logout.test.ts +1 -1
  24. package/src/auth/logout.ts +1 -1
  25. package/src/auth/refreshStampedToken.test.ts +118 -1
  26. package/src/auth/refreshStampedToken.ts +3 -2
  27. package/src/auth/standaloneAuth.ts +9 -3
  28. package/src/auth/studioAuth.ts +34 -7
  29. package/src/auth/studioModeAuth.ts +2 -1
  30. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +10 -2
  31. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +5 -1
  32. package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
  33. package/src/auth/utils.ts +33 -0
  34. package/src/client/clientStore.test.ts +44 -30
  35. package/src/client/clientStore.ts +49 -48
  36. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  37. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  38. package/src/comlink/node/getNodeState.ts +2 -1
  39. package/src/config/sanityConfig.ts +78 -12
  40. package/src/document/actions.ts +18 -11
  41. package/src/document/applyDocumentActions.test.ts +7 -6
  42. package/src/document/applyDocumentActions.ts +10 -4
  43. package/src/document/documentStore.test.ts +542 -188
  44. package/src/document/documentStore.ts +142 -76
  45. package/src/document/events.ts +7 -2
  46. package/src/document/permissions.test.ts +18 -16
  47. package/src/document/permissions.ts +35 -11
  48. package/src/document/processActions.test.ts +359 -32
  49. package/src/document/processActions.ts +106 -78
  50. package/src/document/reducers.test.ts +117 -29
  51. package/src/document/reducers.ts +47 -40
  52. package/src/document/sharedListener.ts +16 -6
  53. package/src/document/util.ts +14 -0
  54. package/src/favorites/favorites.test.ts +9 -2
  55. package/src/presence/bifurTransport.test.ts +46 -6
  56. package/src/presence/bifurTransport.ts +19 -2
  57. package/src/presence/presenceStore.test.ts +96 -0
  58. package/src/presence/presenceStore.ts +96 -24
  59. package/src/preview/getPreviewState.test.ts +115 -98
  60. package/src/preview/getPreviewState.ts +38 -60
  61. package/src/preview/previewProjectionUtils.test.ts +179 -0
  62. package/src/preview/previewProjectionUtils.ts +93 -0
  63. package/src/preview/resolvePreview.test.ts +42 -25
  64. package/src/preview/resolvePreview.ts +33 -10
  65. package/src/preview/{previewStore.ts → types.ts} +8 -17
  66. package/src/projection/getProjectionState.test.ts +16 -16
  67. package/src/projection/getProjectionState.ts +6 -5
  68. package/src/projection/projectionQuery.ts +2 -3
  69. package/src/projection/projectionStore.test.ts +2 -2
  70. package/src/projection/resolveProjection.ts +2 -2
  71. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  72. package/src/projection/subscribeToStateAndFetchBatches.ts +12 -11
  73. package/src/projection/types.ts +1 -1
  74. package/src/query/queryStore.test.ts +12 -12
  75. package/src/query/queryStore.ts +12 -11
  76. package/src/query/reducers.ts +3 -3
  77. package/src/releases/getPerspectiveState.ts +7 -6
  78. package/src/releases/releasesStore.test.ts +20 -5
  79. package/src/releases/releasesStore.ts +20 -8
  80. package/src/store/createActionBinder.test.ts +31 -31
  81. package/src/store/createActionBinder.ts +43 -38
  82. package/src/store/createSanityInstance.ts +2 -3
  83. package/src/store/createStateSourceAction.test.ts +62 -0
  84. package/src/store/createStateSourceAction.ts +34 -39
  85. package/src/telemetry/__telemetry__/sdk.telemetry.ts +42 -0
  86. package/src/telemetry/devMode.test.ts +52 -0
  87. package/src/telemetry/devMode.ts +40 -0
  88. package/src/telemetry/initTelemetry.test.ts +225 -0
  89. package/src/telemetry/initTelemetry.ts +205 -0
  90. package/src/telemetry/telemetryManager.test.ts +263 -0
  91. package/src/telemetry/telemetryManager.ts +187 -0
  92. package/src/users/reducers.ts +3 -4
  93. package/src/users/usersStore.test.ts +1 -0
  94. package/src/users/usersStore.ts +5 -1
  95. package/src/utils/createFetcherStore.test.ts +6 -4
  96. package/src/utils/createFetcherStore.ts +8 -5
  97. package/src/utils/getStagingApiHost.test.ts +21 -0
  98. package/src/utils/getStagingApiHost.ts +14 -0
  99. package/src/utils/ids.test.ts +1 -29
  100. package/src/utils/ids.ts +0 -10
  101. package/src/utils/isImportError.test.ts +72 -0
  102. package/src/utils/isImportError.ts +34 -0
  103. package/src/utils/object.test.ts +95 -0
  104. package/src/utils/object.ts +142 -0
  105. package/src/utils/setCleanupTimeout.ts +24 -0
  106. package/src/preview/previewQuery.test.ts +0 -236
  107. package/src/preview/previewQuery.ts +0 -153
  108. package/src/preview/previewStore.test.ts +0 -36
  109. package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
  110. package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
  111. package/src/preview/util.ts +0 -13
@@ -1,4 +1,4 @@
1
- import {isEqual} from 'lodash-es'
1
+ import {DocumentId} from '@sanity/id-utils'
2
2
  import {
3
3
  combineLatest,
4
4
  debounceTime,
@@ -16,10 +16,11 @@ import {
16
16
  tap,
17
17
  } from 'rxjs'
18
18
 
19
- import {isDatasetSource} from '../config/sanityConfig'
19
+ import {isDatasetResource} from '../config/sanityConfig'
20
20
  import {getQueryState, resolveQuery} from '../query/queryStore'
21
21
  import {type BoundPerspectiveKey} from '../store/createActionBinder'
22
22
  import {type StoreContext} from '../store/defineStore'
23
+ import {isDeepEqual} from '../utils/object'
23
24
  import {
24
25
  createProjectionQuery,
25
26
  processProjectionQuery,
@@ -42,22 +43,22 @@ interface StatusQueryResult {
42
43
  export const subscribeToStateAndFetchBatches = ({
43
44
  state,
44
45
  instance,
45
- key: {source, perspective},
46
+ key: {resource, perspective},
46
47
  }: StoreContext<ProjectionStoreState, BoundPerspectiveKey>): Subscription => {
47
48
  const documentProjections$ = state.observable.pipe(
48
49
  map((s) => s.documentProjections),
49
- distinctUntilChanged(isEqual),
50
+ distinctUntilChanged(isDeepEqual),
50
51
  )
51
52
 
52
53
  const activeDocumentIds$ = state.observable.pipe(
53
- map(({subscriptions}) => new Set(Object.keys(subscriptions))),
54
+ map(({subscriptions}) => new Set(Object.keys(subscriptions).map((id) => DocumentId(id)))),
54
55
  distinctUntilChanged(isSetEqual),
55
56
  )
56
57
 
57
58
  const pendingUpdateSubscription = activeDocumentIds$
58
59
  .pipe(
59
60
  debounceTime(BATCH_DEBOUNCE_TIME),
60
- startWith(new Set<string>()),
61
+ startWith(new Set<DocumentId>()),
61
62
  pairwise(),
62
63
  tap(([prevIds, currIds]) => {
63
64
  const newIds = [...currIds].filter((id) => !prevIds.has(id))
@@ -89,7 +90,7 @@ export const subscribeToStateAndFetchBatches = ({
89
90
 
90
91
  const queryTrigger$ = combineLatest([activeDocumentIds$, documentProjections$]).pipe(
91
92
  debounceTime(BATCH_DEBOUNCE_TIME),
92
- distinctUntilChanged(isEqual),
93
+ distinctUntilChanged(isDeepEqual),
93
94
  )
94
95
 
95
96
  const queryExecutionSubscription = queryTrigger$
@@ -113,7 +114,7 @@ export const subscribeToStateAndFetchBatches = ({
113
114
  perspective,
114
115
  },
115
116
  // temporary guard here until we're ready for everything to be queried via global API
116
- ...(source && !isDatasetSource(source) ? {source} : {}),
117
+ ...(resource && !isDatasetResource(resource) ? {resource} : {}),
117
118
  })
118
119
 
119
120
  const querySource$ = defer(() => {
@@ -128,7 +129,7 @@ export const subscribeToStateAndFetchBatches = ({
128
129
  perspective,
129
130
  },
130
131
  // temporary guard here until we're ready for everything to be queried via global API in v3
131
- ...(source && !isDatasetSource(source) ? {source} : {}),
132
+ ...(resource && !isDatasetResource(resource) ? {resource} : {}),
132
133
  }),
133
134
  ).pipe(switchMap(() => observable))
134
135
  }
@@ -154,7 +155,7 @@ export const subscribeToStateAndFetchBatches = ({
154
155
  perspective: 'raw',
155
156
  },
156
157
  // temporary guard here until we're ready for everything to be queried via global API
157
- ...(source && !isDatasetSource(source) ? {source} : {}),
158
+ ...(resource && !isDatasetResource(resource) ? {resource} : {}),
158
159
  })
159
160
 
160
161
  const statusQuerySource$ = defer(() => {
@@ -169,7 +170,7 @@ export const subscribeToStateAndFetchBatches = ({
169
170
  perspective: 'raw',
170
171
  },
171
172
  // temporary guard here until we're ready for everything to be queried via global API
172
- ...(source && !isDatasetSource(source) ? {source} : {}),
173
+ ...(resource && !isDatasetResource(resource) ? {resource} : {}),
173
174
  }),
174
175
  ).pipe(switchMap(() => observable))
175
176
  }
@@ -29,7 +29,7 @@ interface DocumentProjectionSubscriptions {
29
29
  }
30
30
  }
31
31
 
32
- interface DocumentStatus {
32
+ export interface DocumentStatus {
33
33
  lastEditedDraftAt?: string
34
34
  lastEditedPublishedAt?: string
35
35
  lastEditedVersionAt?: string
@@ -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'
@@ -448,21 +448,21 @@ describe('queryStore', () => {
448
448
  base.dispose()
449
449
  })
450
450
 
451
- it('uses source from params when passed in query options (listenForNewSubscribersAndFetch)', async () => {
451
+ it('uses resource from params when passed in query options (listenForNewSubscribersAndFetch)', async () => {
452
452
  const query = '*[_type == "movie"]'
453
453
  const mediaLibrarySource = {mediaLibraryId: 'ml123'}
454
454
 
455
- const state = getQueryState(instance, {query, source: mediaLibrarySource})
455
+ const state = getQueryState(instance, {query, resource: mediaLibrarySource})
456
456
  const unsubscribe = state.subscribe()
457
457
 
458
458
  await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
459
459
 
460
- // Verify getClientState was called with the source from params in listenForNewSubscribersAndFetch
461
- // This call includes projectId, dataset, and source
460
+ // Verify getClientState was called with the resource from params in listenForNewSubscribersAndFetch
461
+ // This call includes projectId, dataset, and resource
462
462
  expect(getClientState).toHaveBeenCalledWith(
463
463
  instance,
464
464
  expect.objectContaining({
465
- source: expect.objectContaining({
465
+ resource: expect.objectContaining({
466
466
  mediaLibraryId: 'ml123',
467
467
  }),
468
468
  }),
@@ -471,22 +471,22 @@ describe('queryStore', () => {
471
471
  unsubscribe()
472
472
  })
473
473
 
474
- it('uses source from store context key when not a dataset source (listenToLiveClientAndSetLastLiveEventIds)', async () => {
474
+ it('uses resource from store context key when not a dataset resource (listenToLiveClientAndSetLastLiveEventIds)', async () => {
475
475
  const query = '*[_type == "movie"]'
476
476
  const canvasSource = {canvasId: 'canvas456'}
477
477
 
478
- const state = getQueryState(instance, {query, source: canvasSource})
478
+ const state = getQueryState(instance, {query, resource: canvasSource})
479
479
  const unsubscribe = state.subscribe()
480
480
 
481
481
  await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
482
482
 
483
- // Verify getClientState was called with the canvas source for live events
484
- // The source is extracted from the store key and passed when it's not a dataset source
485
- // This call only has apiVersion and source (no projectId/dataset)
483
+ // Verify getClientState was called with the canvas resource for live events
484
+ // The resource is extracted from the store key and passed when it's not a dataset resource
485
+ // This call only has apiVersion and resource (no projectId/dataset)
486
486
  const calls = vi.mocked(getClientState).mock.calls
487
487
  const liveClientCall = calls.find(
488
488
  ([_instance, options]) =>
489
- isCanvasSource(options.source!) && options.source.canvasId === 'canvas456',
489
+ isCanvasResource(options.resource!) && options.resource.canvasId === 'canvas456',
490
490
  )
491
491
  expect(liveClientCall).toBeDefined()
492
492
 
@@ -24,7 +24,7 @@ import {
24
24
  } from 'rxjs'
25
25
 
26
26
  import {getClientState} from '../client/clientStore'
27
- import {type DatasetHandle, isDatasetSource} from '../config/sanityConfig'
27
+ import {type DatasetHandle, isDatasetResource} from '../config/sanityConfig'
28
28
  /*
29
29
  * Although this is an import dependency cycle, it is not a logical cycle:
30
30
  * 1. queryStore uses getPerspectiveState when resolving release perspectives
@@ -35,7 +35,7 @@ import {type DatasetHandle, isDatasetSource} from '../config/sanityConfig'
35
35
  // eslint-disable-next-line import/no-cycle
36
36
  import {getPerspectiveState} from '../releases/getPerspectiveState'
37
37
  import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
38
- import {bindActionBySource, type BoundSourceKey} from '../store/createActionBinder'
38
+ import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
39
39
  import {type SanityInstance} from '../store/createSanityInstance'
40
40
  import {
41
41
  createStateSourceAction,
@@ -45,6 +45,7 @@ import {
45
45
  import {type StoreState} from '../store/createStoreState'
46
46
  import {defineStore, type StoreContext} from '../store/defineStore'
47
47
  import {insecureRandomId} from '../utils/ids'
48
+ import {setCleanupTimeout} from '../utils/setCleanupTimeout'
48
49
  import {
49
50
  QUERY_STATE_CLEAR_DELAY,
50
51
  QUERY_STORE_API_VERSION,
@@ -116,7 +117,7 @@ function normalizeOptionsWithPerspective(
116
117
  }
117
118
  }
118
119
 
119
- const queryStore = defineStore<QueryStoreState, BoundSourceKey>({
120
+ const queryStore = defineStore<QueryStoreState, BoundResourceKey>({
120
121
  name: 'QueryStore',
121
122
  getInitialState: () => ({queries: {}}),
122
123
  initialize(context) {
@@ -172,7 +173,7 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
172
173
  projectId,
173
174
  dataset,
174
175
  tag,
175
- source,
176
+ resource,
176
177
  perspective: perspectiveFromOptions,
177
178
  ...restOptions
178
179
  } = parseQueryKey(group$.key)
@@ -189,7 +190,7 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
189
190
  apiVersion: QUERY_STORE_API_VERSION,
190
191
  projectId,
191
192
  dataset,
192
- source,
193
+ resource,
193
194
  }).observable
194
195
 
195
196
  return combineLatest({
@@ -225,12 +226,12 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
225
226
  const listenToLiveClientAndSetLastLiveEventIds = ({
226
227
  state,
227
228
  instance,
228
- key: {source},
229
- }: StoreContext<QueryStoreState, BoundSourceKey>) => {
229
+ key: {resource},
230
+ }: StoreContext<QueryStoreState, BoundResourceKey>) => {
230
231
  const liveMessages$ = getClientState(instance, {
231
232
  apiVersion: QUERY_STORE_API_VERSION,
232
233
  // temporary guard here until we're ready for everything to be queried via global api
233
- ...(source && !isDatasetSource(source) ? {source} : {}),
234
+ ...(resource && !isDatasetResource(resource) ? {resource} : {}),
234
235
  }).observable.pipe(
235
236
  switchMap((client) =>
236
237
  defer(() =>
@@ -315,7 +316,7 @@ export function getQueryState(
315
316
  ): ReturnType<typeof _getQueryState> {
316
317
  return _getQueryState(...args)
317
318
  }
318
- const _getQueryState = bindActionBySource(
319
+ const _getQueryState = bindActionByResource(
319
320
  queryStore,
320
321
  createStateSourceAction({
321
322
  selector: ({state, instance}: SelectorContext<QueryStoreState>, options: QueryOptions) => {
@@ -333,7 +334,7 @@ const _getQueryState = bindActionBySource(
333
334
 
334
335
  return () => {
335
336
  // this runs on unsubscribe
336
- setTimeout(
337
+ setCleanupTimeout(
337
338
  () => state.set('removeSubscriber', removeSubscriber(key, subscriptionId)),
338
339
  QUERY_STATE_CLEAR_DELAY,
339
340
  )
@@ -374,7 +375,7 @@ export function resolveQuery<TData>(
374
375
  export function resolveQuery(...args: Parameters<typeof _resolveQuery>): Promise<unknown> {
375
376
  return _resolveQuery(...args)
376
377
  }
377
- const _resolveQuery = bindActionBySource(
378
+ const _resolveQuery = bindActionByResource(
378
379
  queryStore,
379
380
  ({state, instance}, {signal, ...options}: ResolveQueryOptions) => {
380
381
  const normalized = normalizeOptionsWithPerspective(instance, options)
@@ -1,4 +1,4 @@
1
- import {omit} from 'lodash-es'
1
+ import {omitProperty} from '../utils/object'
2
2
 
3
3
  interface QueryState {
4
4
  syncTags?: string[]
@@ -54,7 +54,7 @@ export const removeSubscriber =
54
54
  const prevQuery = prev.queries[key]
55
55
  if (!prevQuery) return prev
56
56
  const subscribers = prevQuery.subscribers.filter((id) => id !== subscriptionId)
57
- if (!subscribers.length) return {...prev, queries: omit(prev.queries, key)}
57
+ if (!subscribers.length) return {...prev, queries: omitProperty(prev.queries, key)}
58
58
  return {...prev, queries: {...prev.queries, [key]: {...prevQuery, subscribers}}}
59
59
  }
60
60
 
@@ -64,7 +64,7 @@ export const cancelQuery =
64
64
  const prevQuery = prev.queries[key]
65
65
  if (!prevQuery) return prev
66
66
  if (prevQuery.subscribers.length) return prev
67
- return {...prev, queries: omit(prev.queries, key)}
67
+ return {...prev, queries: omitProperty(prev.queries, key)}
68
68
  }
69
69
 
70
70
  export const initializeQuery =
@@ -1,7 +1,7 @@
1
1
  import {createSelector} from 'reselect'
2
2
 
3
- import {type PerspectiveHandle} from '../config/sanityConfig'
4
- import {bindActionByDataset, type BoundStoreAction} from '../store/createActionBinder'
3
+ import {type DocumentResource, type PerspectiveHandle} from '../config/sanityConfig'
4
+ import {bindActionByResource, type BoundStoreAction} from '../store/createActionBinder'
5
5
  import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
6
6
  /*
7
7
  * Although this is an import dependency cycle, it is not a logical cycle:
@@ -26,7 +26,7 @@ const selectActiveReleases = (context: SelectorContext<ReleasesStoreState>) =>
26
26
  context.state.activeReleases
27
27
  const selectOptions = (
28
28
  _context: SelectorContext<ReleasesStoreState>,
29
- options: PerspectiveHandle & {projectId?: string; dataset?: string},
29
+ options: PerspectiveHandle & {projectId?: string; dataset?: string; resource?: DocumentResource},
30
30
  ) => options
31
31
 
32
32
  const memoizedOptionsSelector = createSelector(
@@ -103,12 +103,13 @@ let _boundGetPerspectiveState: BoundGetPerspectiveState | undefined
103
103
  *
104
104
  * @public
105
105
  */
106
- export const getPerspectiveState: BoundGetPerspectiveState = (...args) => {
106
+ export const getPerspectiveState: BoundGetPerspectiveState = (instance, ...rest) => {
107
107
  if (!_boundGetPerspectiveState) {
108
- _boundGetPerspectiveState = bindActionByDataset(
108
+ _boundGetPerspectiveState = bindActionByResource(
109
109
  releasesStore,
110
110
  _getPerspectiveStateSelector,
111
111
  ) as BoundGetPerspectiveState
112
112
  }
113
- return _boundGetPerspectiveState(...args)
113
+ // bindActionByResource keyFn destructures { resource } from the first param, so pass {} when no options
114
+ return _boundGetPerspectiveState(instance, ...(rest.length ? rest : [{}]))
114
115
  }
@@ -30,6 +30,21 @@ describe('releasesStore', () => {
30
30
  instance.dispose()
31
31
  })
32
32
 
33
+ it('supports calls without options', () => {
34
+ const state = getActiveReleasesState(instance)
35
+
36
+ expect(state.getCurrent()).toBeUndefined()
37
+ expect(getQueryState).toHaveBeenCalledWith(
38
+ instance,
39
+ expect.objectContaining({
40
+ query: 'releases::all()',
41
+ perspective: 'raw',
42
+ resource: undefined,
43
+ tag: 'releases',
44
+ }),
45
+ )
46
+ })
47
+
33
48
  it('should set active releases state when the releases query emits', async () => {
34
49
  const teardown = vi.fn()
35
50
  const subscriber = vi
@@ -58,7 +73,7 @@ describe('releasesStore', () => {
58
73
  } as ReleaseDocument,
59
74
  ]
60
75
 
61
- const state = getActiveReleasesState(instance)
76
+ const state = getActiveReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
62
77
 
63
78
  const [observer] = subscriber.mock.lastCall!
64
79
 
@@ -77,7 +92,7 @@ describe('releasesStore', () => {
77
92
  observable: releasesSubject.asObservable(),
78
93
  } as StateSource<ReleaseDocument[] | undefined>)
79
94
 
80
- const state = getActiveReleasesState(instance)
95
+ const state = getActiveReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
81
96
 
82
97
  // Initial state should be default
83
98
  expect(state.getCurrent()).toBeUndefined() // Default initial state
@@ -124,7 +139,7 @@ describe('releasesStore', () => {
124
139
  observable: of([]),
125
140
  } as StateSource<ReleaseDocument[] | undefined>)
126
141
 
127
- const state = getActiveReleasesState(instance)
142
+ const state = getActiveReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
128
143
 
129
144
  await new Promise((resolve) => setTimeout(resolve, 0))
130
145
 
@@ -138,7 +153,7 @@ describe('releasesStore', () => {
138
153
  getCurrent: () => null as unknown as ReleaseDocument[] | undefined,
139
154
  observable: of(null as unknown as ReleaseDocument[] | undefined),
140
155
  } as StateSource<ReleaseDocument[] | undefined>)
141
- const state = getActiveReleasesState(instance)
156
+ const state = getActiveReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
142
157
  await new Promise((resolve) => setTimeout(resolve, 0))
143
158
  expect(state.getCurrent()).toEqual([])
144
159
 
@@ -160,7 +175,7 @@ describe('releasesStore', () => {
160
175
  observable: subject.asObservable(),
161
176
  } as StateSource<ReleaseDocument[] | undefined>)
162
177
 
163
- const state = getActiveReleasesState(instance)
178
+ const state = getActiveReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
164
179
 
165
180
  subject.error(new Error('Query failed'))
166
181
 
@@ -1,6 +1,7 @@
1
1
  import {type SanityDocument} from '@sanity/types'
2
2
  import {map} from 'rxjs'
3
3
 
4
+ import {type DocumentResource, isDatasetResource} from '../config/sanityConfig'
4
5
  /*
5
6
  * Although this is an import dependency cycle, it is not a logical cycle:
6
7
  * 1. releasesStore uses queryStore as a data source
@@ -10,8 +11,9 @@ import {map} from 'rxjs'
10
11
  */
11
12
  // eslint-disable-next-line import/no-cycle
12
13
  import {getQueryState} from '../query/queryStore'
13
- import {bindActionByDataset, type BoundDatasetKey} from '../store/createActionBinder'
14
- import {createStateSourceAction} from '../store/createStateSourceAction'
14
+ import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
15
+ import {type SanityInstance} from '../store/createSanityInstance'
16
+ import {createStateSourceAction, type StateSource} from '../store/createStateSourceAction'
15
17
  import {defineStore, type StoreContext} from '../store/defineStore'
16
18
  import {sortReleases} from './utils/sortReleases'
17
19
 
@@ -39,7 +41,7 @@ export interface ReleasesStoreState {
39
41
  error?: unknown
40
42
  }
41
43
 
42
- export const releasesStore = defineStore<ReleasesStoreState, BoundDatasetKey>({
44
+ export const releasesStore = defineStore<ReleasesStoreState, BoundResourceKey>({
43
45
  name: 'Releases',
44
46
  getInitialState: (): ReleasesStoreState => ({
45
47
  activeReleases: undefined,
@@ -54,25 +56,35 @@ export const releasesStore = defineStore<ReleasesStoreState, BoundDatasetKey>({
54
56
  * Get the active releases from the store.
55
57
  * @internal
56
58
  */
57
- export const getActiveReleasesState = bindActionByDataset(
59
+ const _getActiveReleasesState = bindActionByResource(
58
60
  releasesStore,
59
61
  createStateSourceAction({
60
62
  selector: ({state}, _?) => state.activeReleases,
61
63
  }),
62
64
  )
63
65
 
66
+ /**
67
+ * Get the active releases from the store.
68
+ * @internal
69
+ */
70
+ export const getActiveReleasesState = (
71
+ instance: SanityInstance,
72
+ options?: {resource?: DocumentResource},
73
+ ): StateSource<ReleaseDocument[] | undefined> =>
74
+ // bindActionByResource keyFn destructures { resource } from the first param, so pass {} when no options
75
+ _getActiveReleasesState(instance, options ?? {})
76
+
64
77
  const RELEASES_QUERY = 'releases::all()'
65
78
 
66
79
  const subscribeToReleases = ({
67
80
  instance,
68
81
  state,
69
- key: {projectId, dataset},
70
- }: StoreContext<ReleasesStoreState, BoundDatasetKey>) => {
82
+ key: {resource},
83
+ }: StoreContext<ReleasesStoreState, BoundResourceKey>) => {
71
84
  const {observable: releases$} = getQueryState<ReleaseDocument[]>(instance, {
72
85
  query: RELEASES_QUERY,
73
86
  perspective: 'raw',
74
- projectId,
75
- dataset,
87
+ resource: resource && !isDatasetResource(resource) ? resource : undefined,
76
88
  tag: 'releases',
77
89
  })
78
90
  return releases$
@@ -1,10 +1,10 @@
1
1
  import {beforeEach, describe, expect, it, vi} from 'vitest'
2
2
 
3
- import {type DocumentSource} from '../config/sanityConfig'
3
+ import {type DocumentResource} from '../config/sanityConfig'
4
4
  import {
5
5
  bindActionByDataset,
6
- bindActionBySource,
7
- bindActionBySourceAndPerspective,
6
+ bindActionByResource,
7
+ bindActionByResourceAndPerspective,
8
8
  bindActionGlobally,
9
9
  createActionBinder,
10
10
  } from './createActionBinder'
@@ -161,28 +161,28 @@ describe('bindActionGlobally', () => {
161
161
  })
162
162
  })
163
163
 
164
- describe('bindActionBySource', () => {
165
- it('should throw an error when provided an invalid source', () => {
164
+ describe('bindActionByResource', () => {
165
+ it('should throw an error when provided an invalid resource', () => {
166
166
  const storeDefinition = {
167
167
  name: 'SourceStore',
168
168
  getInitialState: () => ({counter: 0}),
169
169
  }
170
170
  const action = vi.fn((_context) => 'success')
171
- const boundAction = bindActionBySource(storeDefinition, action)
171
+ const boundAction = bindActionByResource(storeDefinition, action)
172
172
  const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
173
173
 
174
174
  expect(() =>
175
- boundAction(instance, {source: {invalid: 'source'} as unknown as DocumentSource}),
176
- ).toThrow('Received invalid source:')
175
+ boundAction(instance, {resource: {invalid: 'resource'} as unknown as DocumentResource}),
176
+ ).toThrow('Received invalid resource:')
177
177
  })
178
178
 
179
- it('should throw an error when no source provided and projectId/dataset are missing', () => {
179
+ it('should throw an error when no resource provided and projectId/dataset are missing', () => {
180
180
  const storeDefinition = {
181
181
  name: 'SourceStore',
182
182
  getInitialState: () => ({counter: 0}),
183
183
  }
184
184
  const action = vi.fn((_context) => 'success')
185
- const boundAction = bindActionBySource(storeDefinition, action)
185
+ const boundAction = bindActionByResource(storeDefinition, action)
186
186
  const instance = createSanityInstance({projectId: '', dataset: ''})
187
187
 
188
188
  expect(() => boundAction(instance, {})).toThrow(
@@ -190,47 +190,47 @@ describe('bindActionBySource', () => {
190
190
  )
191
191
  })
192
192
 
193
- it('should work correctly with a valid dataset source', () => {
193
+ it('should work correctly with a valid dataset resource', () => {
194
194
  const storeDefinition = {
195
195
  name: 'SourceStore',
196
196
  getInitialState: () => ({counter: 0}),
197
197
  }
198
198
  const action = vi.fn((_context) => 'success')
199
- const boundAction = bindActionBySource(storeDefinition, action)
199
+ const boundAction = bindActionByResource(storeDefinition, action)
200
200
  const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
201
201
 
202
202
  const result = boundAction(instance, {
203
- source: {projectId: 'proj2', dataset: 'ds2'},
203
+ resource: {projectId: 'proj2', dataset: 'ds2'},
204
204
  })
205
205
  expect(result).toBe('success')
206
206
  })
207
207
  })
208
208
 
209
- describe('bindActionBySourceAndPerspective', () => {
210
- it('should throw an error when provided an invalid source', () => {
209
+ describe('bindActionByResourceAndPerspective', () => {
210
+ it('should throw an error when provided an invalid resource', () => {
211
211
  const storeDefinition = {
212
212
  name: 'PerspectiveStore',
213
213
  getInitialState: () => ({counter: 0}),
214
214
  }
215
215
  const action = vi.fn((_context) => 'success')
216
- const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
216
+ const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
217
217
  const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
218
218
 
219
219
  expect(() =>
220
220
  boundAction(instance, {
221
- source: {invalid: 'source'} as unknown as DocumentSource,
221
+ resource: {invalid: 'resource'} as unknown as DocumentResource,
222
222
  perspective: 'drafts',
223
223
  }),
224
- ).toThrow('Received invalid source:')
224
+ ).toThrow('Received invalid resource:')
225
225
  })
226
226
 
227
- it('should throw an error when no source provided and projectId/dataset are missing', () => {
227
+ it('should throw an error when no resource provided and projectId/dataset are missing', () => {
228
228
  const storeDefinition = {
229
229
  name: 'PerspectiveStore',
230
230
  getInitialState: () => ({counter: 0}),
231
231
  }
232
232
  const action = vi.fn((_context) => 'success')
233
- const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
233
+ const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
234
234
  const instance = createSanityInstance({projectId: '', dataset: ''})
235
235
 
236
236
  expect(() => boundAction(instance, {perspective: 'drafts'})).toThrow(
@@ -238,33 +238,33 @@ describe('bindActionBySourceAndPerspective', () => {
238
238
  )
239
239
  })
240
240
 
241
- it('should work correctly with a valid dataset source and explicit perspective', () => {
241
+ it('should work correctly with a valid dataset resource and explicit perspective', () => {
242
242
  const storeDefinition = {
243
243
  name: 'PerspectiveStore',
244
244
  getInitialState: () => ({counter: 0}),
245
245
  }
246
246
  const action = vi.fn((_context) => 'success')
247
- const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
247
+ const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
248
248
  const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
249
249
 
250
250
  const result = boundAction(instance, {
251
- source: {projectId: 'proj2', dataset: 'ds2'},
251
+ resource: {projectId: 'proj2', dataset: 'ds2'},
252
252
  perspective: 'drafts',
253
253
  })
254
254
  expect(result).toBe('success')
255
255
  })
256
256
 
257
- it('should work correctly with valid dataset source and no perspective (falls back to drafts)', () => {
257
+ it('should work correctly with valid dataset resource and no perspective (falls back to drafts)', () => {
258
258
  const storeDefinition = {
259
259
  name: 'PerspectiveStore',
260
260
  getInitialState: () => ({counter: 0}),
261
261
  }
262
262
  const action = vi.fn((_context) => 'success')
263
- const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
263
+ const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
264
264
  const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
265
265
 
266
266
  const result = boundAction(instance, {
267
- source: {projectId: 'proj1', dataset: 'ds1'},
267
+ resource: {projectId: 'proj1', dataset: 'ds1'},
268
268
  })
269
269
  expect(result).toBe('success')
270
270
  })
@@ -275,7 +275,7 @@ describe('bindActionBySourceAndPerspective', () => {
275
275
  getInitialState: () => ({counter: 0}),
276
276
  }
277
277
  const action = vi.fn((context) => context.key)
278
- const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
278
+ const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
279
279
  const instance = createSanityInstance({
280
280
  projectId: 'proj1',
281
281
  dataset: 'ds1',
@@ -300,7 +300,7 @@ describe('bindActionBySourceAndPerspective', () => {
300
300
  context.state.counter += increment
301
301
  return context.state.counter
302
302
  })
303
- const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
303
+ const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
304
304
  // Use unique project/dataset so we don't reuse stores from other tests
305
305
  const instance = createSanityInstance({
306
306
  projectId: 'perspective-isolation',
@@ -322,7 +322,7 @@ describe('bindActionBySourceAndPerspective', () => {
322
322
  getInitialState: () => ({counter: 0}),
323
323
  }
324
324
  const action = vi.fn((_context) => 'success')
325
- const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
325
+ const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
326
326
  const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
327
327
 
328
328
  const result = boundAction(instance, {
@@ -339,7 +339,7 @@ describe('bindActionBySourceAndPerspective', () => {
339
339
  )
340
340
  })
341
341
 
342
- it('should reuse same store when same source and perspective are used', () => {
342
+ it('should reuse same store when same resource and perspective are used', () => {
343
343
  const storeDefinition = {
344
344
  name: 'PerspectiveStore',
345
345
  getInitialState: () => ({counter: 0}),
@@ -348,7 +348,7 @@ describe('bindActionBySourceAndPerspective', () => {
348
348
  context.state.counter += increment
349
349
  return context.state.counter
350
350
  })
351
- const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
351
+ const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
352
352
  // Use unique project/dataset so we don't reuse stores from other tests
353
353
  const instance = createSanityInstance({
354
354
  projectId: 'perspective-reuse',