@sanity/sdk 2.6.0 → 2.7.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 (39) hide show
  1. package/dist/index.d.ts +124 -13
  2. package/dist/index.js +468 -243
  3. package/dist/index.js.map +1 -1
  4. package/package.json +5 -4
  5. package/src/_exports/index.ts +3 -0
  6. package/src/auth/authMode.test.ts +56 -0
  7. package/src/auth/authMode.ts +71 -0
  8. package/src/auth/authStore.test.ts +85 -4
  9. package/src/auth/authStore.ts +63 -125
  10. package/src/auth/authStrategy.ts +39 -0
  11. package/src/auth/dashboardAuth.ts +132 -0
  12. package/src/auth/standaloneAuth.ts +109 -0
  13. package/src/auth/studioAuth.ts +217 -0
  14. package/src/auth/studioModeAuth.test.ts +43 -1
  15. package/src/auth/studioModeAuth.ts +10 -1
  16. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +21 -6
  17. package/src/config/sanityConfig.ts +48 -7
  18. package/src/projection/getProjectionState.ts +6 -5
  19. package/src/projection/projectionQuery.test.ts +38 -55
  20. package/src/projection/projectionQuery.ts +27 -31
  21. package/src/projection/projectionStore.test.ts +4 -4
  22. package/src/projection/projectionStore.ts +3 -2
  23. package/src/projection/resolveProjection.ts +2 -2
  24. package/src/projection/statusQuery.test.ts +35 -0
  25. package/src/projection/statusQuery.ts +71 -0
  26. package/src/projection/subscribeToStateAndFetchBatches.test.ts +63 -50
  27. package/src/projection/subscribeToStateAndFetchBatches.ts +106 -27
  28. package/src/projection/types.ts +12 -0
  29. package/src/projection/util.ts +0 -1
  30. package/src/query/queryStore.test.ts +64 -0
  31. package/src/query/queryStore.ts +30 -10
  32. package/src/releases/getPerspectiveState.test.ts +17 -14
  33. package/src/releases/getPerspectiveState.ts +58 -38
  34. package/src/releases/releasesStore.test.ts +59 -61
  35. package/src/releases/releasesStore.ts +21 -35
  36. package/src/releases/utils/isReleasePerspective.ts +7 -0
  37. package/src/store/createActionBinder.test.ts +211 -1
  38. package/src/store/createActionBinder.ts +95 -17
  39. package/src/store/createSanityInstance.ts +3 -1
@@ -1,6 +1,13 @@
1
1
  import {beforeEach, describe, expect, it, vi} from 'vitest'
2
2
 
3
- import {bindActionByDataset, bindActionGlobally, createActionBinder} from './createActionBinder'
3
+ import {type DocumentSource} from '../config/sanityConfig'
4
+ import {
5
+ bindActionByDataset,
6
+ bindActionBySource,
7
+ bindActionBySourceAndPerspective,
8
+ bindActionGlobally,
9
+ createActionBinder,
10
+ } from './createActionBinder'
4
11
  import {createSanityInstance} from './createSanityInstance'
5
12
  import {createStoreInstance} from './createStoreInstance'
6
13
 
@@ -153,3 +160,206 @@ describe('bindActionGlobally', () => {
153
160
  expect(storeInstance.dispose).toHaveBeenCalledTimes(1)
154
161
  })
155
162
  })
163
+
164
+ describe('bindActionBySource', () => {
165
+ it('should throw an error when provided an invalid source', () => {
166
+ const storeDefinition = {
167
+ name: 'SourceStore',
168
+ getInitialState: () => ({counter: 0}),
169
+ }
170
+ const action = vi.fn((_context) => 'success')
171
+ const boundAction = bindActionBySource(storeDefinition, action)
172
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
173
+
174
+ expect(() =>
175
+ boundAction(instance, {source: {invalid: 'source'} as unknown as DocumentSource}),
176
+ ).toThrow('Received invalid source:')
177
+ })
178
+
179
+ it('should throw an error when no source provided and projectId/dataset are missing', () => {
180
+ const storeDefinition = {
181
+ name: 'SourceStore',
182
+ getInitialState: () => ({counter: 0}),
183
+ }
184
+ const action = vi.fn((_context) => 'success')
185
+ const boundAction = bindActionBySource(storeDefinition, action)
186
+ const instance = createSanityInstance({projectId: '', dataset: ''})
187
+
188
+ expect(() => boundAction(instance, {})).toThrow(
189
+ 'This API requires a project ID and dataset configured.',
190
+ )
191
+ })
192
+
193
+ it('should work correctly with a valid dataset source', () => {
194
+ const storeDefinition = {
195
+ name: 'SourceStore',
196
+ getInitialState: () => ({counter: 0}),
197
+ }
198
+ const action = vi.fn((_context) => 'success')
199
+ const boundAction = bindActionBySource(storeDefinition, action)
200
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
201
+
202
+ const result = boundAction(instance, {
203
+ source: {projectId: 'proj2', dataset: 'ds2'},
204
+ })
205
+ expect(result).toBe('success')
206
+ })
207
+ })
208
+
209
+ describe('bindActionBySourceAndPerspective', () => {
210
+ it('should throw an error when provided an invalid source', () => {
211
+ const storeDefinition = {
212
+ name: 'PerspectiveStore',
213
+ getInitialState: () => ({counter: 0}),
214
+ }
215
+ const action = vi.fn((_context) => 'success')
216
+ const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
217
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
218
+
219
+ expect(() =>
220
+ boundAction(instance, {
221
+ source: {invalid: 'source'} as unknown as DocumentSource,
222
+ perspective: 'drafts',
223
+ }),
224
+ ).toThrow('Received invalid source:')
225
+ })
226
+
227
+ it('should throw an error when no source provided and projectId/dataset are missing', () => {
228
+ const storeDefinition = {
229
+ name: 'PerspectiveStore',
230
+ getInitialState: () => ({counter: 0}),
231
+ }
232
+ const action = vi.fn((_context) => 'success')
233
+ const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
234
+ const instance = createSanityInstance({projectId: '', dataset: ''})
235
+
236
+ expect(() => boundAction(instance, {perspective: 'drafts'})).toThrow(
237
+ 'This API requires a project ID and dataset configured.',
238
+ )
239
+ })
240
+
241
+ it('should work correctly with a valid dataset source and explicit perspective', () => {
242
+ const storeDefinition = {
243
+ name: 'PerspectiveStore',
244
+ getInitialState: () => ({counter: 0}),
245
+ }
246
+ const action = vi.fn((_context) => 'success')
247
+ const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
248
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
249
+
250
+ const result = boundAction(instance, {
251
+ source: {projectId: 'proj2', dataset: 'ds2'},
252
+ perspective: 'drafts',
253
+ })
254
+ expect(result).toBe('success')
255
+ })
256
+
257
+ it('should work correctly with valid dataset source and no perspective (falls back to drafts)', () => {
258
+ const storeDefinition = {
259
+ name: 'PerspectiveStore',
260
+ getInitialState: () => ({counter: 0}),
261
+ }
262
+ const action = vi.fn((_context) => 'success')
263
+ const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
264
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
265
+
266
+ const result = boundAction(instance, {
267
+ source: {projectId: 'proj1', dataset: 'ds1'},
268
+ })
269
+ expect(result).toBe('success')
270
+ })
271
+
272
+ it('should use instance.config.perspective when options.perspective is not provided', () => {
273
+ const storeDefinition = {
274
+ name: 'PerspectiveStore',
275
+ getInitialState: () => ({counter: 0}),
276
+ }
277
+ const action = vi.fn((context) => context.key)
278
+ const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
279
+ const instance = createSanityInstance({
280
+ projectId: 'proj1',
281
+ dataset: 'ds1',
282
+ perspective: 'published',
283
+ })
284
+
285
+ const result = boundAction(instance, {})
286
+ expect(result).toEqual(
287
+ expect.objectContaining({
288
+ name: 'proj1.ds1:published',
289
+ perspective: 'published',
290
+ }),
291
+ )
292
+ })
293
+
294
+ it('should create separate store instances for different perspectives', () => {
295
+ const storeDefinition = {
296
+ name: 'PerspectiveStore',
297
+ getInitialState: () => ({counter: 0}),
298
+ }
299
+ const action = vi.fn((context, _options, increment: number) => {
300
+ context.state.counter += increment
301
+ return context.state.counter
302
+ })
303
+ const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
304
+ // Use unique project/dataset so we don't reuse stores from other tests
305
+ const instance = createSanityInstance({
306
+ projectId: 'perspective-isolation',
307
+ dataset: 'ds1',
308
+ })
309
+
310
+ const resultDrafts = boundAction(instance, {perspective: 'drafts'}, 3)
311
+ const resultPublished = boundAction(instance, {perspective: 'published'}, 4)
312
+
313
+ expect(resultDrafts).toBe(3)
314
+ expect(resultPublished).toBe(4)
315
+ // Two stores: one for drafts, one for published
316
+ expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(2)
317
+ })
318
+
319
+ it('should create separate store instance for release perspective', () => {
320
+ const storeDefinition = {
321
+ name: 'PerspectiveStore',
322
+ getInitialState: () => ({counter: 0}),
323
+ }
324
+ const action = vi.fn((_context) => 'success')
325
+ const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
326
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
327
+
328
+ const result = boundAction(instance, {
329
+ perspective: {releaseName: 'release1'},
330
+ })
331
+ expect(result).toBe('success')
332
+ expect(vi.mocked(createStoreInstance)).toHaveBeenCalledWith(
333
+ instance,
334
+ expect.objectContaining({
335
+ name: 'proj1.ds1:release1',
336
+ perspective: {releaseName: 'release1'},
337
+ }),
338
+ storeDefinition,
339
+ )
340
+ })
341
+
342
+ it('should reuse same store when same source and perspective are used', () => {
343
+ const storeDefinition = {
344
+ name: 'PerspectiveStore',
345
+ getInitialState: () => ({counter: 0}),
346
+ }
347
+ const action = vi.fn((context, _options, increment: number) => {
348
+ context.state.counter += increment
349
+ return context.state.counter
350
+ })
351
+ const boundAction = bindActionBySourceAndPerspective(storeDefinition, action)
352
+ // Use unique project/dataset so we don't reuse stores from other tests
353
+ const instance = createSanityInstance({
354
+ projectId: 'perspective-reuse',
355
+ dataset: 'ds1',
356
+ })
357
+
358
+ const result1 = boundAction(instance, {perspective: 'drafts'}, 2)
359
+ const result2 = boundAction(instance, {perspective: 'drafts'}, 3)
360
+
361
+ expect(result1).toBe(2)
362
+ expect(result2).toBe(5)
363
+ expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(1)
364
+ })
365
+ })
@@ -1,15 +1,27 @@
1
+ import {type ClientPerspective} from '@sanity/client'
2
+
1
3
  import {
4
+ type DatasetHandle,
2
5
  type DocumentSource,
3
6
  isCanvasSource,
4
7
  isDatasetSource,
5
8
  isMediaLibrarySource,
9
+ type ReleasePerspective,
6
10
  } from '../config/sanityConfig'
11
+ import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
7
12
  import {type SanityInstance} from './createSanityInstance'
8
13
  import {createStoreInstance, type StoreInstance} from './createStoreInstance'
9
14
  import {type StoreState} from './createStoreState'
10
15
  import {type StoreContext, type StoreDefinition} from './defineStore'
11
16
 
12
- export type BoundDatasetKey = {
17
+ export interface BoundSourceKey {
18
+ name: string
19
+ source: DocumentSource
20
+ }
21
+ export interface BoundPerspectiveKey extends BoundSourceKey {
22
+ perspective: ClientPerspective | ReleasePerspective
23
+ }
24
+ export interface BoundDatasetKey {
13
25
  name: string
14
26
  projectId: string
15
27
  dataset: string
@@ -154,32 +166,98 @@ export const bindActionByDataset = createActionBinder<
154
166
  return {name: `${projectId}.${dataset}`, projectId, dataset}
155
167
  })
156
168
 
157
- /**
158
- * Binds an action to a store that's scoped to a specific document source.
159
- **/
160
- export const bindActionBySource = createActionBinder<
161
- {name: string},
162
- [{source?: DocumentSource}, ...unknown[]]
163
- >((instance, {source}) => {
169
+ const createSourceKey = (instance: SanityInstance, source?: DocumentSource): BoundSourceKey => {
170
+ let name: string | undefined
171
+ let sourceForKey: DocumentSource | undefined
164
172
  if (source) {
165
- let id: string | undefined
173
+ sourceForKey = source
166
174
  if (isDatasetSource(source)) {
167
- id = `${source.projectId}.${source.dataset}`
175
+ name = `${source.projectId}.${source.dataset}`
168
176
  } else if (isMediaLibrarySource(source)) {
169
- id = `media-library:${source.mediaLibraryId}`
177
+ name = `media-library:${source.mediaLibraryId}`
170
178
  } else if (isCanvasSource(source)) {
171
- id = `canvas:${source.canvasId}`
179
+ name = `canvas:${source.canvasId}`
180
+ } else {
181
+ throw new Error(`Received invalid source: ${JSON.stringify(source)}`)
172
182
  }
173
-
174
- if (!id) throw new Error(`Received invalid source: ${JSON.stringify(source)}`)
175
- return {name: id}
183
+ return {name, source: sourceForKey}
176
184
  }
177
- const {projectId, dataset} = instance.config
178
185
 
186
+ // TODO: remove reference to instance.config when we get to v3
187
+ const {projectId, dataset} = instance.config
179
188
  if (!projectId || !dataset) {
180
189
  throw new Error('This API requires a project ID and dataset configured.')
181
190
  }
182
- return {name: `${projectId}.${dataset}`}
191
+ return {name: `${projectId}.${dataset}`, source: {projectId, dataset}}
192
+ }
193
+
194
+ /**
195
+ * Binds an action to a store that's scoped to a specific document source.
196
+ **/
197
+ export const bindActionBySource = createActionBinder<
198
+ BoundSourceKey,
199
+ [{source?: DocumentSource}, ...unknown[]]
200
+ >((instance, {source}) => {
201
+ return createSourceKey(instance, source)
202
+ })
203
+
204
+ /**
205
+ * Binds an action to a store that's scoped to a specific document source and perspective.
206
+ *
207
+ * @remarks
208
+ * This creates actions that operate on state isolated to a specific document source and perspective.
209
+ * Different document sources and perspectives will have separate states.
210
+ *
211
+ * This is mostly useful for stores that do batch fetching operations, since the query store
212
+ * can isolate single queries by perspective.
213
+ *
214
+ * @throws Error if source or perspective is missing from the Sanity instance config
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * // Define a store
219
+ * const documentStore = defineStore<DocumentState>({
220
+ * name: 'Document',
221
+ * getInitialState: () => ({ documents: {} }),
222
+ * // ...
223
+ * })
224
+ *
225
+ * // Create source-and-perspective-specific actions
226
+ * export const fetchDocuments = bindActionBySourceAndPerspective(
227
+ * documentStore,
228
+ * ({instance, state}, documentId) => {
229
+ * // This state is isolated to the specific document source and perspective
230
+ * // ...fetch logic...
231
+ * }
232
+ * )
233
+ *
234
+ * // Usage
235
+ * fetchDocument(sanityInstance, 'doc123')
236
+ * ```
237
+ */
238
+ export const bindActionBySourceAndPerspective = createActionBinder<
239
+ BoundPerspectiveKey,
240
+ [DatasetHandle, ...unknown[]]
241
+ >((instance, options): BoundPerspectiveKey => {
242
+ const {source, perspective} = options
243
+ // TODO: remove reference to instance.config.perspective when we get to v3
244
+ const utilizedPerspective = perspective ?? instance.config.perspective ?? 'drafts'
245
+ let perspectiveKey: string
246
+ if (isReleasePerspective(utilizedPerspective)) {
247
+ perspectiveKey = utilizedPerspective.releaseName
248
+ } else if (typeof utilizedPerspective === 'string') {
249
+ perspectiveKey = utilizedPerspective
250
+ } else {
251
+ // "StackablePerspective", shouldn't be a common case, but just in case
252
+ perspectiveKey = JSON.stringify(utilizedPerspective)
253
+ }
254
+ const sourceKey = createSourceKey(instance, source)
255
+
256
+ return {
257
+ name: `${sourceKey.name}:${perspectiveKey}`,
258
+ source: sourceKey.source,
259
+ perspective: utilizedPerspective,
260
+ }
183
261
  })
184
262
 
185
263
  /**
@@ -100,7 +100,9 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
100
100
  projectId: config.projectId,
101
101
  dataset: config.dataset,
102
102
  perspective: config.perspective,
103
- studioMode: config.studioMode?.enabled,
103
+ hasStudioConfig: !!config.studio,
104
+ hasStudioTokenSource: !!config.studio?.auth?.token,
105
+ legacyStudioMode: config.studioMode?.enabled,
104
106
  hasAuthProviders: !!config.auth?.providers,
105
107
  hasAuthToken: !!config.auth?.token,
106
108
  })