@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.
- package/dist/index.d.ts +124 -13
- package/dist/index.js +468 -243
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/_exports/index.ts +3 -0
- package/src/auth/authMode.test.ts +56 -0
- package/src/auth/authMode.ts +71 -0
- package/src/auth/authStore.test.ts +85 -4
- package/src/auth/authStore.ts +63 -125
- package/src/auth/authStrategy.ts +39 -0
- package/src/auth/dashboardAuth.ts +132 -0
- package/src/auth/standaloneAuth.ts +109 -0
- package/src/auth/studioAuth.ts +217 -0
- package/src/auth/studioModeAuth.test.ts +43 -1
- package/src/auth/studioModeAuth.ts +10 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +21 -6
- package/src/config/sanityConfig.ts +48 -7
- package/src/projection/getProjectionState.ts +6 -5
- package/src/projection/projectionQuery.test.ts +38 -55
- package/src/projection/projectionQuery.ts +27 -31
- package/src/projection/projectionStore.test.ts +4 -4
- package/src/projection/projectionStore.ts +3 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/statusQuery.test.ts +35 -0
- package/src/projection/statusQuery.ts +71 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +63 -50
- package/src/projection/subscribeToStateAndFetchBatches.ts +106 -27
- package/src/projection/types.ts +12 -0
- package/src/projection/util.ts +0 -1
- package/src/query/queryStore.test.ts +64 -0
- package/src/query/queryStore.ts +30 -10
- package/src/releases/getPerspectiveState.test.ts +17 -14
- package/src/releases/getPerspectiveState.ts +58 -38
- package/src/releases/releasesStore.test.ts +59 -61
- package/src/releases/releasesStore.ts +21 -35
- package/src/releases/utils/isReleasePerspective.ts +7 -0
- package/src/store/createActionBinder.test.ts +211 -1
- package/src/store/createActionBinder.ts +95 -17
- 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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
173
|
+
sourceForKey = source
|
|
166
174
|
if (isDatasetSource(source)) {
|
|
167
|
-
|
|
175
|
+
name = `${source.projectId}.${source.dataset}`
|
|
168
176
|
} else if (isMediaLibrarySource(source)) {
|
|
169
|
-
|
|
177
|
+
name = `media-library:${source.mediaLibraryId}`
|
|
170
178
|
} else if (isCanvasSource(source)) {
|
|
171
|
-
|
|
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
|
-
|
|
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
|
})
|