@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
@@ -69,7 +69,7 @@ describe('presenceStore', () => {
69
69
  mockGetUserState = vi.fn(() => of(mockUser))
70
70
  vi.mocked(getUserState).mockImplementation(mockGetUserState)
71
71
 
72
- instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
72
+ instance = createSanityInstance()
73
73
  })
74
74
 
75
75
  afterEach(() => {
@@ -77,8 +77,9 @@ describe('presenceStore', () => {
77
77
  })
78
78
 
79
79
  describe('getPresence', () => {
80
+ const key = {resource: {projectId: 'test-project', dataset: 'test-dataset'}}
80
81
  it('creates bifur transport with correct parameters', () => {
81
- getPresence(instance)
82
+ getPresence(instance, key)
82
83
 
83
84
  expect(createBifurTransport).toHaveBeenCalledWith({
84
85
  client: mockClient,
@@ -88,18 +89,18 @@ describe('presenceStore', () => {
88
89
  })
89
90
 
90
91
  it('sends rollCall message on initialization', () => {
91
- getPresence(instance)
92
+ getPresence(instance, key)
92
93
 
93
94
  expect(mockDispatchMessage).toHaveBeenCalledWith({type: 'rollCall'})
94
95
  })
95
96
 
96
97
  it('returns empty array when no users present', () => {
97
- const source = getPresence(instance)
98
+ const source = getPresence(instance, key)
98
99
  expect(source.getCurrent()).toEqual([])
99
100
  })
100
101
 
101
102
  it('handles state events from other users', async () => {
102
- const source = getPresence(instance)
103
+ const source = getPresence(instance, key)
103
104
 
104
105
  // Subscribe to initialize the store
105
106
  const unsubscribe = source.subscribe(() => {})
@@ -136,7 +137,7 @@ describe('presenceStore', () => {
136
137
  })
137
138
 
138
139
  it('ignores events from own session', async () => {
139
- const source = getPresence(instance)
140
+ const source = getPresence(instance, key)
140
141
  const unsubscribe = source.subscribe(() => {})
141
142
 
142
143
  await firstValueFrom(of(null).pipe(delay(10)))
@@ -158,7 +159,7 @@ describe('presenceStore', () => {
158
159
  })
159
160
 
160
161
  it('handles disconnect events', async () => {
161
- const source = getPresence(instance)
162
+ const source = getPresence(instance, key)
162
163
  const unsubscribe = source.subscribe(() => {})
163
164
 
164
165
  await firstValueFrom(of(null).pipe(delay(10)))
@@ -190,7 +191,7 @@ describe('presenceStore', () => {
190
191
  })
191
192
 
192
193
  it('fetches user data for present users', async () => {
193
- const source = getPresence(instance)
194
+ const source = getPresence(instance, key)
194
195
  const unsubscribe = source.subscribe(() => {})
195
196
 
196
197
  await firstValueFrom(of(null).pipe(delay(10)))
@@ -222,7 +223,7 @@ describe('presenceStore', () => {
222
223
  })
223
224
 
224
225
  it('handles presence events correctly', async () => {
225
- const source = getPresence(instance)
226
+ const source = getPresence(instance, key)
226
227
  const unsubscribe = source.subscribe(() => {})
227
228
 
228
229
  await firstValueFrom(of(null).pipe(delay(10)))
@@ -243,5 +244,29 @@ describe('presenceStore', () => {
243
244
 
244
245
  unsubscribe()
245
246
  })
247
+
248
+ it('should throw an error when initialized with a media library resource', () => {
249
+ const mediaLibraryResource = {mediaLibraryId: 'ml123'}
250
+
251
+ expect(() => {
252
+ getPresence(instance, {resource: mediaLibraryResource})
253
+ }).toThrow('Presence is not supported for media library resources')
254
+ })
255
+
256
+ it('should throw an error when initialized with a canvas resource', () => {
257
+ const canvasResource = {canvasId: 'canvas123'}
258
+
259
+ expect(() => {
260
+ getPresence(instance, {resource: canvasResource})
261
+ }).toThrow('Presence is not supported for canvas resources')
262
+ })
263
+
264
+ it('should work with a dataset resource', () => {
265
+ const datasetResource = {projectId: 'test-project', dataset: 'test-dataset'}
266
+
267
+ expect(() => {
268
+ getPresence(instance, {resource: datasetResource})
269
+ }).not.toThrow()
270
+ })
246
271
  })
247
272
  })
@@ -1,10 +1,10 @@
1
- import {type SanityClient} from '@sanity/client'
2
1
  import {createSelector} from 'reselect'
3
2
  import {combineLatest, distinctUntilChanged, filter, map, of, Subscription, switchMap} from 'rxjs'
4
3
 
5
4
  import {getTokenState} from '../auth/authStore'
6
5
  import {getClient} from '../client/clientStore'
7
- import {bindActionByDataset, type BoundDatasetKey} from '../store/createActionBinder'
6
+ import {isCanvasResource, isDatasetResource, isMediaLibraryResource} from '../config/sanityConfig'
7
+ import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
8
8
  import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
9
9
  import {defineStore, type StoreContext} from '../store/defineStore'
10
10
  import {type SanityUser} from '../users/types'
@@ -23,27 +23,40 @@ const getInitialState = (): PresenceStoreState => ({
23
23
  })
24
24
 
25
25
  /** @public */
26
- export const presenceStore = defineStore<PresenceStoreState, BoundDatasetKey>({
26
+ export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
27
27
  name: 'presence',
28
28
  getInitialState,
29
- initialize: (context: StoreContext<PresenceStoreState, BoundDatasetKey>) => {
29
+ initialize: (context: StoreContext<PresenceStoreState, BoundResourceKey>) => {
30
30
  const {
31
31
  instance,
32
32
  state,
33
- key: {projectId, dataset},
33
+ key: {resource},
34
34
  } = context
35
+
36
+ // Presence is only supported for dataset resources
37
+ if (isMediaLibraryResource(resource)) {
38
+ throw new Error(
39
+ 'Presence is not supported for media library resources. Presence tracking requires a dataset resource with a projectId.',
40
+ )
41
+ }
42
+
43
+ if (isCanvasResource(resource)) {
44
+ throw new Error(
45
+ 'Presence is not supported for canvas resources. Presence tracking requires a dataset resource with a projectId.',
46
+ )
47
+ }
48
+
35
49
  const sessionId = crypto.randomUUID()
36
50
 
37
51
  const client = getClient(instance, {
38
52
  apiVersion: '2022-06-30',
39
- projectId,
40
- dataset,
53
+ resource,
41
54
  })
42
55
 
43
56
  const token$ = getTokenState(instance).observable.pipe(distinctUntilChanged())
44
57
 
45
58
  const [incomingEvents$, dispatch] = createBifurTransport({
46
- client: client as SanityClient,
59
+ client,
47
60
  token$,
48
61
  sessionId,
49
62
  })
@@ -114,13 +127,13 @@ const selectPresence = createSelector(
114
127
  },
115
128
  )
116
129
 
117
- /** @public */
118
- export const getPresence = bindActionByDataset(
130
+ /** @beta */
131
+ export const getPresence = bindActionByResource(
119
132
  presenceStore,
120
133
  createStateSourceAction({
121
- selector: (context: SelectorContext<PresenceStoreState>, _?): UserPresence[] =>
134
+ selector: (context: SelectorContext<PresenceStoreState>): UserPresence[] =>
122
135
  selectPresence(context.state),
123
- onSubscribe: (context: StoreContext<PresenceStoreState, BoundDatasetKey>, _?) => {
136
+ onSubscribe: (context: StoreContext<PresenceStoreState, BoundResourceKey>) => {
124
137
  const userIds$ = context.state.observable.pipe(
125
138
  map((state) =>
126
139
  Array.from(state.locations.values())
@@ -140,7 +153,10 @@ export const getPresence = bindActionByDataset(
140
153
  getUserState(context.instance, {
141
154
  userId,
142
155
  resourceType: 'project',
143
- projectId: context.key.projectId,
156
+ projectId:
157
+ context.key.resource && isDatasetResource(context.key.resource)
158
+ ? context.key.resource.projectId
159
+ : undefined,
144
160
  }).pipe(filter((v): v is NonNullable<typeof v> => !!v)),
145
161
  )
146
162
  return combineLatest(userObservables)
@@ -0,0 +1,192 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {createSanityInstance} from '../store/createSanityInstance'
5
+ import {normalizeMedia, transformProjectionToPreview} from './previewProjectionUtils'
6
+ import {type PreviewQueryResult} from './types'
7
+
8
+ // Mock the getClient function
9
+ vi.mock('../client/clientStore', () => ({
10
+ getClient: vi.fn(),
11
+ }))
12
+
13
+ const mockClient = {
14
+ config: () => ({projectId: 'test-project', dataset: 'test-dataset'}),
15
+ } as unknown as SanityClient
16
+
17
+ describe('normalizeMedia', () => {
18
+ beforeEach(async () => {
19
+ vi.clearAllMocks()
20
+ })
21
+
22
+ it('returns null if media is null or undefined', () => {
23
+ expect(normalizeMedia(null, mockClient)).toBeNull()
24
+ expect(normalizeMedia(undefined, mockClient)).toBeNull()
25
+ })
26
+
27
+ it('returns null if media does not have a valid asset', () => {
28
+ const invalidMedia1 = {media: {_ref: 'image-abc123-200x200-png'}} // Missing `asset` property
29
+ const invalidMedia2 = {asset: {ref: 'image-abc123-200x200-png'}} // Incorrect property name `ref`
30
+ expect(normalizeMedia(invalidMedia1, mockClient)).toBeNull()
31
+ expect(normalizeMedia(invalidMedia2, mockClient)).toBeNull()
32
+ })
33
+
34
+ it('returns null if media is not an object', () => {
35
+ expect(normalizeMedia(123, mockClient)).toBeNull()
36
+ expect(normalizeMedia('invalid', mockClient)).toBeNull()
37
+ })
38
+
39
+ it('returns a normalized URL for valid image asset objects', () => {
40
+ const validMedia = {type: 'image-asset', _ref: 'image-abc123-200x200-png'}
41
+ const result = normalizeMedia(validMedia, mockClient)
42
+ expect(result).toEqual({
43
+ type: 'image-asset',
44
+ _ref: 'image-abc123-200x200-png',
45
+ url: 'https://cdn.sanity.io/images/test-project/test-dataset/abc123-200x200.png',
46
+ })
47
+ })
48
+
49
+ it('handles image assets with expected URL format', () => {
50
+ const media = {type: 'image-asset', _ref: 'image-xyz456-400x400-jpg'}
51
+ const result = normalizeMedia(media, mockClient)
52
+ expect(result).toEqual({
53
+ type: 'image-asset',
54
+ _ref: 'image-xyz456-400x400-jpg',
55
+ url: 'https://cdn.sanity.io/images/test-project/test-dataset/xyz456-400x400.jpg',
56
+ })
57
+ })
58
+ })
59
+
60
+ describe('transformProjectionToPreview', () => {
61
+ const instance = createSanityInstance()
62
+
63
+ beforeEach(async () => {
64
+ vi.clearAllMocks()
65
+
66
+ // Mock getClient to return our mock client
67
+ const {getClient} = await import('../client/clientStore')
68
+ vi.mocked(getClient).mockReturnValue(mockClient)
69
+ })
70
+
71
+ it('transforms projection result with title and subtitle', () => {
72
+ const projectionResult: PreviewQueryResult = {
73
+ _id: 'doc1',
74
+ _type: 'article',
75
+ _updatedAt: '2026-01-01',
76
+ titleCandidates: {title: 'My Title'},
77
+ subtitleCandidates: {description: 'My Description'},
78
+ media: null,
79
+ }
80
+
81
+ const result = transformProjectionToPreview(
82
+ instance,
83
+ {projectId: 'p', dataset: 'd'},
84
+ projectionResult,
85
+ )
86
+
87
+ expect(result).toEqual({
88
+ title: 'My Title',
89
+ subtitle: 'My Description',
90
+ media: null,
91
+ })
92
+ })
93
+
94
+ it('uses fallback title when no title candidates exist', () => {
95
+ const projectionResult: PreviewQueryResult = {
96
+ _id: 'doc1',
97
+ _type: 'article',
98
+ _updatedAt: '2026-01-01',
99
+ titleCandidates: {},
100
+ subtitleCandidates: {},
101
+ media: null,
102
+ }
103
+
104
+ const result = transformProjectionToPreview(
105
+ instance,
106
+ {projectId: 'p', dataset: 'd'},
107
+ projectionResult,
108
+ )
109
+
110
+ expect(result.title).toBe('article: doc1')
111
+ expect(result.subtitle).toBeUndefined()
112
+ })
113
+
114
+ it('transforms projection result with media', () => {
115
+ const projectionResult: PreviewQueryResult = {
116
+ _id: 'doc1',
117
+ _type: 'article',
118
+ _updatedAt: '2026-01-01',
119
+ titleCandidates: {title: 'My Title'},
120
+ subtitleCandidates: {},
121
+ media: {type: 'image-asset', _ref: 'image-abc123-200x200-png', url: ''},
122
+ }
123
+
124
+ const result = transformProjectionToPreview(
125
+ instance,
126
+ {projectId: 'p', dataset: 'd'},
127
+ projectionResult,
128
+ )
129
+
130
+ expect(result).toEqual({
131
+ title: 'My Title',
132
+ subtitle: undefined,
133
+ media: {
134
+ type: 'image-asset',
135
+ _ref: 'image-abc123-200x200-png',
136
+ url: 'https://cdn.sanity.io/images/test-project/test-dataset/abc123-200x200.png',
137
+ },
138
+ })
139
+ })
140
+
141
+ it('includes status when provided', () => {
142
+ const projectionResult: PreviewQueryResult = {
143
+ _id: 'doc1',
144
+ _type: 'article',
145
+ _updatedAt: '2026-01-01',
146
+ titleCandidates: {title: 'My Title'},
147
+ subtitleCandidates: {},
148
+ media: null,
149
+ _status: {
150
+ lastEditedPublishedAt: '2026-01-01',
151
+ lastEditedDraftAt: '2026-01-02',
152
+ },
153
+ }
154
+
155
+ const result = transformProjectionToPreview(
156
+ instance,
157
+ {projectId: 'p', dataset: 'd'},
158
+ projectionResult,
159
+ )
160
+
161
+ expect(result).toEqual({
162
+ title: 'My Title',
163
+ subtitle: undefined,
164
+ media: null,
165
+ _status: {
166
+ lastEditedPublishedAt: '2026-01-01',
167
+ lastEditedDraftAt: '2026-01-02',
168
+ },
169
+ })
170
+ })
171
+
172
+ it('calls getClient with the provided resource', async () => {
173
+ const projectionResult: PreviewQueryResult = {
174
+ _id: 'doc1',
175
+ _type: 'article',
176
+ _updatedAt: '2026-01-01',
177
+ titleCandidates: {title: 'My Title'},
178
+ subtitleCandidates: {},
179
+ media: null,
180
+ }
181
+
182
+ const resource = {mediaLibraryId: 'test-library'}
183
+
184
+ transformProjectionToPreview(instance, resource, projectionResult)
185
+
186
+ const {getClient} = await import('../client/clientStore')
187
+ expect(getClient).toHaveBeenCalledWith(instance, {
188
+ apiVersion: 'v2025-05-06',
189
+ resource,
190
+ })
191
+ })
192
+ })
@@ -0,0 +1,88 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {createImageUrlBuilder} from '@sanity/image-url'
3
+ import {isObject} from 'lodash-es'
4
+
5
+ import {getClient} from '../client/clientStore'
6
+ import {type DocumentResource} from '../config/sanityConfig'
7
+ import {type SanityInstance} from '../store/createSanityInstance'
8
+ import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
9
+ import {type PreviewQueryResult, type PreviewValue} from './types'
10
+
11
+ const API_VERSION = 'v2025-05-06'
12
+
13
+ /**
14
+ * Checks if the provided value has `_ref` property that is a string and starts with `image-`
15
+ */
16
+ function hasImageRef<T>(value: unknown): value is T & {_ref: string} {
17
+ return isObject(value) && '_ref' in value && typeof (value as {_ref: unknown})._ref === 'string'
18
+ }
19
+
20
+ /**
21
+ * Normalizes a media asset to a preview value.
22
+ * Adds a url to a media asset reference using `@sanity/image-url`.
23
+ *
24
+ * @internal
25
+ */
26
+ export function normalizeMedia(media: unknown, client: SanityClient): PreviewValue['media'] {
27
+ if (!media) return null
28
+ if (!hasImageRef(media)) return null
29
+
30
+ const builder = createImageUrlBuilder(client)
31
+ const url = builder.image({_ref: media._ref}).url()
32
+
33
+ return {
34
+ type: 'image-asset',
35
+ _ref: media._ref,
36
+ url,
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Finds a single field value from a set of candidates based on a priority list of field names.
42
+ * Returns the first non-empty string value found from the candidates matching the priority list order.
43
+ *
44
+ * @internal
45
+ */
46
+ function findFirstDefined(
47
+ fieldsToSearch: string[],
48
+ candidates: Record<string, unknown>,
49
+ exclude?: unknown,
50
+ ): string | undefined {
51
+ if (!candidates) return undefined
52
+
53
+ for (const field of fieldsToSearch) {
54
+ const value = candidates[field]
55
+ if (typeof value === 'string' && value.trim() !== '' && value !== exclude) {
56
+ return value
57
+ }
58
+ }
59
+
60
+ return undefined
61
+ }
62
+
63
+ /**
64
+ * Transforms a projection result (with titleCandidates, subtitleCandidates, media)
65
+ * into a PreviewValue (with title, subtitle, media).
66
+ *
67
+ * @param projectionResult - The raw projection result from GROQ
68
+ * @param instance - The Sanity instance to use for client configuration
69
+ * @param resource - Data resource for the preview
70
+ * @internal
71
+ */
72
+ export function transformProjectionToPreview(
73
+ instance: SanityInstance,
74
+ resource: DocumentResource,
75
+ projectionResult: PreviewQueryResult,
76
+ ): PreviewValue {
77
+ const title = findFirstDefined(TITLE_CANDIDATES, projectionResult.titleCandidates)
78
+ const subtitle = findFirstDefined(SUBTITLE_CANDIDATES, projectionResult.subtitleCandidates, title)
79
+
80
+ const client = getClient(instance, {apiVersion: API_VERSION, resource})
81
+
82
+ return {
83
+ title: String(title || `${projectionResult._type}: ${projectionResult._id}`),
84
+ subtitle: subtitle || undefined,
85
+ media: normalizeMedia(projectionResult.media, client),
86
+ ...(projectionResult._status && {_status: projectionResult._status}),
87
+ }
88
+ }
@@ -1,7 +1,9 @@
1
- import {type BoundDatasetKey} from '../store/createActionBinder'
2
- import {defineStore} from '../store/defineStore'
3
- import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
1
+ import {type DocumentStatus} from '../projection/types'
4
2
 
3
+ /**
4
+ *
5
+ * @internal
6
+ */
5
7
  export interface PreviewQueryResult {
6
8
  _id: string
7
9
  _type: string
@@ -9,6 +11,7 @@ export interface PreviewQueryResult {
9
11
  titleCandidates: Record<string, unknown>
10
12
  subtitleCandidates: Record<string, unknown>
11
13
  media?: PreviewMedia | null
14
+ _status?: DocumentStatus
12
15
  }
13
16
 
14
17
  /**
@@ -71,25 +74,3 @@ export type ValuePending<T> = {
71
74
  data: T | null
72
75
  isPending: boolean
73
76
  }
74
-
75
- /**
76
- * @public
77
- */
78
- export interface PreviewStoreState {
79
- values: {[TDocumentId in string]?: ValuePending<PreviewValue>}
80
- subscriptions: {[TDocumentId in string]?: {[TSubscriptionId in string]?: true}}
81
- }
82
-
83
- export const previewStore = defineStore<PreviewStoreState, BoundDatasetKey>({
84
- name: 'Preview',
85
- getInitialState() {
86
- return {
87
- subscriptions: {},
88
- values: {},
89
- }
90
- },
91
- initialize: (context) => {
92
- const subscription = subscribeToStateAndFetchBatches(context)
93
- return () => subscription.unsubscribe
94
- },
95
- })
@@ -11,7 +11,7 @@ vi.mock('../client/clientStore')
11
11
 
12
12
  describe('project', () => {
13
13
  it('calls the `client.observable.projects.getById` method on the client and returns the result', async () => {
14
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
14
+ const instance = createSanityInstance()
15
15
 
16
16
  const project = {id: 'a'}
17
17
  const getById = vi.fn().mockReturnValue(of(project))
@@ -1,38 +1,32 @@
1
1
  import {switchMap} from 'rxjs'
2
2
 
3
3
  import {getClientState} from '../client/clientStore'
4
- import {type ProjectHandle} from '../config/sanityConfig'
5
4
  import {createFetcherStore} from '../utils/createFetcherStore'
6
5
 
7
6
  const API_VERSION = 'v2025-02-19'
8
7
 
8
+ /** @public */
9
+ export type ProjectHandle = {
10
+ projectId: string
11
+ }
9
12
  const project = createFetcherStore({
10
13
  name: 'Project',
11
- getKey: (instance, options?: ProjectHandle) => {
12
- const projectId = options?.projectId ?? instance.config.projectId
14
+ getKey: (_instance, options: ProjectHandle) => {
15
+ const projectId = options?.projectId
13
16
  if (!projectId) {
14
17
  throw new Error('A projectId is required to use the project API.')
15
18
  }
16
19
  return projectId
17
20
  },
18
- fetcher:
19
- (instance) =>
20
- (options: ProjectHandle = {}) => {
21
- const projectId = options.projectId ?? instance.config.projectId
21
+ fetcher: (instance) => (options: ProjectHandle) => {
22
+ const projectId = options.projectId
22
23
 
23
- return getClientState(instance, {
24
- apiVersion: API_VERSION,
25
- scope: 'global',
26
- projectId,
27
- }).observable.pipe(
28
- switchMap((client) =>
29
- client.observable.projects.getById(
30
- // non-null assertion is fine with the above throwing
31
- (projectId ?? instance.config.projectId)!,
32
- ),
33
- ),
34
- )
35
- },
24
+ return getClientState(instance, {
25
+ apiVersion: API_VERSION,
26
+ scope: 'global',
27
+ projectId,
28
+ }).observable.pipe(switchMap((client) => client.observable.projects.getById(projectId!)))
29
+ },
36
30
  })
37
31
 
38
32
  /** @public */
@@ -1,6 +1,7 @@
1
1
  import {NEVER} from 'rxjs'
2
2
  import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
3
3
 
4
+ import {type DocumentResource} from '../config/sanityConfig'
4
5
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
5
6
  import {type StoreState} from '../store/createStoreState'
6
7
  import {hashString} from '../utils/hashString'
@@ -30,7 +31,8 @@ vi.mock('./subscribeToStateAndFetchBatches.ts')
30
31
 
31
32
  describe('getProjectionState', () => {
32
33
  let instance: SanityInstance
33
- const docHandle = {documentId: 'exampleId', documentType: 'exampleType'}
34
+ const resource: DocumentResource = {projectId: 'p', dataset: 'd'}
35
+ const docHandle = {documentId: 'exampleId', documentType: 'exampleType', resource}
34
36
  const projection1 = '{exampleProjection1}'
35
37
  const hash1 = hashString(projection1)
36
38
  const projection2 = '{exampleProjection2}'
@@ -48,7 +50,7 @@ describe('getProjectionState', () => {
48
50
  return NEVER.subscribe()
49
51
  })
50
52
 
51
- instance = createSanityInstance({projectId: 'exampleProject', dataset: 'exampleDataset'})
53
+ instance = createSanityInstance()
52
54
  vi.useFakeTimers() // Enable fake timers for each test
53
55
  })
54
56
 
@@ -1,9 +1,8 @@
1
1
  import {DocumentId, getPublishedId} from '@sanity/id-utils'
2
- import {type SanityProjectionResult} from 'groq'
3
2
  import {omit} from 'lodash-es'
4
3
 
5
4
  import {type DocumentHandle} from '../config/sanityConfig'
6
- import {bindActionBySourceAndPerspective} from '../store/createActionBinder'
5
+ import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
7
6
  import {type SanityInstance} from '../store/createSanityInstance'
8
7
  import {
9
8
  createStateSourceAction,
@@ -25,24 +24,6 @@ export interface ProjectionOptions<
25
24
  projection: TProjection
26
25
  }
27
26
 
28
- /**
29
- * @beta
30
- */
31
- export function getProjectionState<
32
- TProjection extends string = string,
33
- TDocumentType extends string = string,
34
- TDataset extends string = string,
35
- TProjectId extends string = string,
36
- >(
37
- instance: SanityInstance,
38
- options: ProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
39
- ): StateSource<
40
- | ProjectionValuePending<
41
- SanityProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
42
- >
43
- | undefined
44
- >
45
-
46
27
  /**
47
28
  * @beta
48
29
  */
@@ -71,7 +52,7 @@ export function getProjectionState(
71
52
  /**
72
53
  * @beta
73
54
  */
74
- export const _getProjectionState = bindActionBySourceAndPerspective(
55
+ export const _getProjectionState = bindActionByResourceAndPerspective(
75
56
  projectionStore,
76
57
  createStateSourceAction({
77
58
  selector: (
@@ -1,7 +1,6 @@
1
- import {type ClientPerspective} from '@sanity/client'
2
1
  import {DocumentId} from '@sanity/id-utils'
3
2
 
4
- import {type ReleasePerspective} from '../config/sanityConfig'
3
+ import {type PerspectiveHandle} from '../config/sanityConfig'
5
4
  import {getPublishedId} from '../utils/ids'
6
5
  import {
7
6
  type DocumentProjections,
@@ -68,7 +67,7 @@ interface ProcessProjectionQueryOptions {
68
67
  ids: Set<string>
69
68
  results: ProjectionQueryResult[]
70
69
  documentStatuses?: ProjectionStoreState['documentStatuses']
71
- perspective: ClientPerspective | ReleasePerspective
70
+ perspective: NonNullable<PerspectiveHandle['perspective']>
72
71
  }
73
72
 
74
73
  export function processProjectionQuery({