@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.1

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 (197) hide show
  1. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +1147 -0
  2. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
  3. package/dist/_chunks-es/_internal.js +134 -0
  4. package/dist/_chunks-es/_internal.js.map +1 -0
  5. package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
  6. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
  7. package/dist/_chunks-es/telemetryManager.js +93 -0
  8. package/dist/_chunks-es/telemetryManager.js.map +1 -0
  9. package/dist/_exports/_internal.d.ts +95 -0
  10. package/dist/_exports/_internal.d.ts.map +1 -0
  11. package/dist/_exports/_internal.js +24 -0
  12. package/dist/_exports/_internal.js.map +1 -0
  13. package/dist/index.d.ts +1079 -1076
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +1967 -2302
  16. package/dist/index.js.map +1 -1
  17. package/package.json +48 -47
  18. package/src/_exports/_internal.ts +26 -0
  19. package/src/_exports/index.ts +120 -11
  20. package/src/access/checkPermissions.test.ts +55 -0
  21. package/src/access/checkPermissions.ts +54 -0
  22. package/src/agent/agentActions.test.ts +16 -60
  23. package/src/agent/agentActions.ts +25 -38
  24. package/src/applications/applications.test-d.ts +83 -0
  25. package/src/applications/applications.test.ts +130 -0
  26. package/src/applications/applications.ts +364 -0
  27. package/src/applications/userApplications.test.ts +126 -0
  28. package/src/applications/userApplications.ts +261 -0
  29. package/src/auth/authLogger.ts +30 -0
  30. package/src/auth/authMode.test.ts +25 -0
  31. package/src/auth/authMode.ts +6 -3
  32. package/src/auth/authStore.test.ts +194 -13
  33. package/src/auth/authStore.ts +59 -26
  34. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  35. package/src/auth/getOrganizationVerificationState.ts +4 -4
  36. package/src/auth/handleAuthCallback.test.ts +35 -1
  37. package/src/auth/handleAuthCallback.ts +25 -6
  38. package/src/auth/logout.test.ts +75 -2
  39. package/src/auth/logout.ts +23 -4
  40. package/src/auth/refreshStampedToken.test.ts +219 -469
  41. package/src/auth/refreshStampedToken.ts +79 -195
  42. package/src/auth/studioAuth.ts +1 -1
  43. package/src/auth/studioModeAuth.ts +2 -1
  44. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  45. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  46. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  47. package/src/client/clientStore.test.ts +62 -37
  48. package/src/client/clientStore.ts +45 -75
  49. package/src/client/liveEvents.test.ts +151 -0
  50. package/src/client/liveEvents.ts +107 -0
  51. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  52. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  53. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  54. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  55. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  56. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  57. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  58. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  59. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  60. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  61. package/src/comlink/node/getNodeState.test.ts +1 -1
  62. package/src/comlink/node/getNodeState.ts +2 -1
  63. package/src/config/__tests__/handles.test.ts +18 -12
  64. package/src/config/handles.ts +25 -7
  65. package/src/config/sanityConfig.ts +109 -79
  66. package/src/datasets/datasets.test.ts +2 -2
  67. package/src/datasets/datasets.ts +10 -4
  68. package/src/document/actions.test.ts +115 -33
  69. package/src/document/actions.ts +212 -15
  70. package/src/document/applyDocumentActions.test.ts +38 -12
  71. package/src/document/applyDocumentActions.ts +30 -9
  72. package/src/document/documentConstants.ts +7 -0
  73. package/src/document/documentStore.concurrency.test.ts +1306 -0
  74. package/src/document/documentStore.test.ts +673 -278
  75. package/src/document/documentStore.ts +162 -71
  76. package/src/document/events.test.ts +57 -2
  77. package/src/document/events.ts +82 -26
  78. package/src/document/listen.test.ts +56 -0
  79. package/src/document/listen.ts +74 -21
  80. package/src/document/listenerEventOperators.test.ts +311 -0
  81. package/src/document/listenerEventOperators.ts +217 -0
  82. package/src/document/patchOperations.test.ts +49 -2
  83. package/src/document/patchOperations.ts +50 -0
  84. package/src/document/permissions.test.ts +105 -60
  85. package/src/document/permissions.ts +44 -19
  86. package/src/document/processActions/create.ts +138 -0
  87. package/src/document/processActions/delete.ts +100 -0
  88. package/src/document/processActions/discard.ts +63 -0
  89. package/src/document/processActions/edit.ts +154 -0
  90. package/src/document/processActions/processActions.ts +218 -0
  91. package/src/document/processActions/publish.ts +120 -0
  92. package/src/document/processActions/releaseArchive.ts +77 -0
  93. package/src/document/processActions/releaseCreate.ts +59 -0
  94. package/src/document/processActions/releaseDelete.ts +65 -0
  95. package/src/document/processActions/releaseEdit.ts +37 -0
  96. package/src/document/processActions/releasePublish.ts +45 -0
  97. package/src/document/processActions/releaseSchedule.ts +87 -0
  98. package/src/document/processActions/releaseUtil.ts +31 -0
  99. package/src/document/processActions/shared.ts +308 -0
  100. package/src/document/processActions/unpublish.ts +85 -0
  101. package/src/document/processActions.test.ts +1129 -121
  102. package/src/document/reducers.test.ts +639 -64
  103. package/src/document/reducers.ts +187 -48
  104. package/src/document/resourceRules.test.ts +178 -0
  105. package/src/document/resourceRules.ts +117 -0
  106. package/src/document/sharedListener.test.ts +16 -14
  107. package/src/document/sharedListener.ts +24 -8
  108. package/src/document/util.ts +14 -0
  109. package/src/favorites/favorites.test.ts +5 -6
  110. package/src/installations/installations.test-d.ts +68 -0
  111. package/src/installations/installations.test.ts +62 -0
  112. package/src/installations/installations.ts +202 -0
  113. package/src/organization/organization.test-d.ts +36 -0
  114. package/src/organization/organization.test.ts +138 -0
  115. package/src/organization/organization.ts +138 -0
  116. package/src/organizations/organizations.test-d.ts +56 -0
  117. package/src/organizations/organizations.test.ts +150 -0
  118. package/src/organizations/organizations.ts +107 -0
  119. package/src/presence/bifurTransport.test.ts +46 -6
  120. package/src/presence/bifurTransport.ts +19 -2
  121. package/src/presence/presenceStore.test.ts +94 -23
  122. package/src/presence/presenceStore.ts +85 -29
  123. package/src/preview/getPreviewState.test.ts +137 -0
  124. package/src/preview/getPreviewState.ts +69 -0
  125. package/src/preview/previewProjectionUtils.test.ts +9 -22
  126. package/src/preview/previewProjectionUtils.ts +7 -3
  127. package/src/preview/resolvePreview.test.ts +64 -0
  128. package/src/preview/resolvePreview.ts +43 -0
  129. package/src/preview/types.ts +10 -0
  130. package/src/project/project.test-d.ts +37 -0
  131. package/src/project/project.test.ts +157 -12
  132. package/src/project/project.ts +138 -25
  133. package/src/projection/getProjectionState.test.ts +18 -20
  134. package/src/projection/getProjectionState.ts +23 -3
  135. package/src/projection/projectionQuery.ts +5 -5
  136. package/src/projection/projectionStore.test.ts +1 -1
  137. package/src/projection/resolveProjection.test.ts +1 -2
  138. package/src/projection/resolveProjection.ts +16 -0
  139. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  140. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  141. package/src/projection/types.ts +8 -0
  142. package/src/projects/projects.test-d.ts +32 -0
  143. package/src/projects/projects.test.ts +108 -42
  144. package/src/projects/projects.ts +63 -27
  145. package/src/query/queryStore.test.ts +259 -123
  146. package/src/query/queryStore.ts +78 -63
  147. package/src/query/reducers.test.ts +0 -42
  148. package/src/query/reducers.ts +2 -18
  149. package/src/releases/getPerspectiveState.test.ts +17 -23
  150. package/src/releases/getPerspectiveState.ts +8 -20
  151. package/src/releases/observeReleases.test.ts +127 -0
  152. package/src/releases/observeReleases.ts +102 -0
  153. package/src/releases/releasesStore.test.ts +100 -53
  154. package/src/releases/releasesStore.ts +69 -44
  155. package/src/releases/utils/sortReleases.test.ts +38 -2
  156. package/src/releases/utils/sortReleases.ts +15 -10
  157. package/src/store/createActionBinder.test.ts +87 -90
  158. package/src/store/createActionBinder.ts +79 -25
  159. package/src/store/createSanityInstance.test.ts +83 -13
  160. package/src/store/createSanityInstance.ts +77 -2
  161. package/src/store/createStateSourceAction.test.ts +64 -2
  162. package/src/store/createStateSourceAction.ts +39 -44
  163. package/src/store/createStoreInstance.test.ts +4 -2
  164. package/src/store/fetcherStore.test-d.ts +121 -0
  165. package/src/store/fetcherStore.test.ts +299 -0
  166. package/src/store/fetcherStore.ts +801 -0
  167. package/src/telemetry/environment.test.ts +119 -0
  168. package/src/telemetry/environment.ts +92 -0
  169. package/src/telemetry/events.ts +42 -0
  170. package/src/telemetry/initTelemetry.test.ts +432 -0
  171. package/src/telemetry/initTelemetry.ts +218 -0
  172. package/src/telemetry/telemetryManager.test.ts +327 -0
  173. package/src/telemetry/telemetryManager.ts +199 -0
  174. package/src/users/reducers.test.ts +6 -1
  175. package/src/users/reducers.ts +5 -6
  176. package/src/users/types.ts +4 -4
  177. package/src/users/usersStore.test.ts +16 -12
  178. package/src/users/usersStore.ts +5 -1
  179. package/src/utils/buildQuery.test.ts +14 -0
  180. package/src/utils/buildQuery.ts +17 -0
  181. package/src/utils/createFetcherStore.test.ts +7 -5
  182. package/src/utils/createFetcherStore.ts +8 -5
  183. package/src/utils/getStagingApiHost.test.ts +21 -0
  184. package/src/utils/getStagingApiHost.ts +14 -0
  185. package/src/utils/ids.test.ts +1 -29
  186. package/src/utils/ids.ts +0 -10
  187. package/src/utils/includeShape.ts +36 -0
  188. package/src/utils/isImportError.test.ts +72 -0
  189. package/src/utils/isImportError.ts +34 -0
  190. package/src/utils/logger.test.ts +12 -0
  191. package/src/utils/logger.ts +8 -3
  192. package/src/utils/object.test.ts +95 -0
  193. package/src/utils/object.ts +142 -0
  194. package/src/utils/setCleanupTimeout.ts +37 -0
  195. package/src/document/processActions.ts +0 -707
  196. package/src/utils/defineIntent.test.ts +0 -477
  197. package/src/utils/defineIntent.ts +0 -244
@@ -1,20 +1,45 @@
1
1
  import {createSelector} from 'reselect'
2
- import {combineLatest, distinctUntilChanged, filter, map, of, Subscription, switchMap} from 'rxjs'
2
+ import {
3
+ catchError,
4
+ combineLatest,
5
+ distinctUntilChanged,
6
+ EMPTY,
7
+ filter,
8
+ first,
9
+ map,
10
+ type Observable,
11
+ of,
12
+ Subscription,
13
+ switchMap,
14
+ } from 'rxjs'
3
15
 
4
16
  import {getTokenState} from '../auth/authStore'
5
17
  import {getClient} from '../client/clientStore'
6
- import {isCanvasResource, isDatasetResource, isMediaLibraryResource} from '../config/sanityConfig'
18
+ import {
19
+ type DocumentResource,
20
+ isCanvasResource,
21
+ isDatasetResource,
22
+ isMediaLibraryResource,
23
+ } from '../config/sanityConfig'
7
24
  import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
8
- import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
25
+ import {type SanityInstance} from '../store/createSanityInstance'
26
+ import {
27
+ createStateSourceAction,
28
+ type SelectorContext,
29
+ type StateSource,
30
+ } from '../store/createStateSourceAction'
9
31
  import {defineStore, type StoreContext} from '../store/defineStore'
10
32
  import {type SanityUser} from '../users/types'
11
33
  import {getUserState} from '../users/usersStore'
12
34
  import {createBifurTransport} from './bifurTransport'
13
35
  import {type PresenceLocation, type TransportEvent, type UserPresence} from './types'
14
36
 
37
+ const PRESENCE_API_VERSION = '2026-03-30'
38
+
15
39
  type PresenceStoreState = {
16
40
  locations: Map<string, {userId: string; locations: PresenceLocation[]}>
17
41
  users: Record<string, SanityUser | undefined>
42
+ organizationId?: string
18
43
  }
19
44
 
20
45
  const getInitialState = (): PresenceStoreState => ({
@@ -33,25 +58,25 @@ export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
33
58
  key: {resource},
34
59
  } = context
35
60
 
36
- // Presence is only supported for dataset resources
37
61
  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
- )
62
+ throw new Error('Presence is not supported for media library resources.')
47
63
  }
48
64
 
49
65
  const sessionId = crypto.randomUUID()
50
66
 
51
- const client = getClient(instance, {
52
- apiVersion: '2022-06-30',
53
- resource,
54
- })
67
+ // Dataset resources must use the project hostname so the socket URL is project-specific.
68
+ // Canvas resources use the global API endpoint via the resource config.
69
+ const client = isDatasetResource(resource)
70
+ ? getClient(instance, {
71
+ apiVersion: PRESENCE_API_VERSION,
72
+ projectId: resource.projectId,
73
+ dataset: resource.dataset,
74
+ useProjectHostname: true,
75
+ })
76
+ : getClient(instance, {
77
+ apiVersion: PRESENCE_API_VERSION,
78
+ resource,
79
+ })
55
80
 
56
81
  const token$ = getTokenState(instance).observable.pipe(distinctUntilChanged())
57
82
 
@@ -94,6 +119,22 @@ export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
94
119
 
95
120
  dispatch({type: 'rollCall'}).subscribe()
96
121
 
122
+ // Canvas resources need the organizationId to resolve users — fetch it once from the canvas endpoint
123
+ if (isCanvasResource(resource)) {
124
+ const globalClient = getClient(instance, {apiVersion: PRESENCE_API_VERSION})
125
+ subscription.add(
126
+ globalClient.observable
127
+ .request<{organizationId: string}>({
128
+ uri: `/canvases/${resource.canvasId}`,
129
+ tag: 'canvases.get',
130
+ })
131
+ .pipe(catchError(() => EMPTY))
132
+ .subscribe(({organizationId}) => {
133
+ state.set('presence/organizationId', (prev) => ({...prev, organizationId}))
134
+ }),
135
+ )
136
+ }
137
+
97
138
  return () => {
98
139
  dispatch({type: 'disconnect'}).subscribe()
99
140
  subscription.unsubscribe()
@@ -127,13 +168,13 @@ const selectPresence = createSelector(
127
168
  },
128
169
  )
129
170
 
130
- /** @beta */
131
- export const getPresence = bindActionByResource(
171
+ const _getPresence = bindActionByResource(
132
172
  presenceStore,
133
173
  createStateSourceAction({
134
174
  selector: (context: SelectorContext<PresenceStoreState>): UserPresence[] =>
135
175
  selectPresence(context.state),
136
176
  onSubscribe: (context: StoreContext<PresenceStoreState, BoundResourceKey>) => {
177
+ const resource = context.key.resource
137
178
  const userIds$ = context.state.observable.pipe(
138
179
  map((state) =>
139
180
  Array.from(state.locations.values())
@@ -143,29 +184,34 @@ export const getPresence = bindActionByResource(
143
184
  distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i])),
144
185
  )
145
186
 
146
- const subscription = userIds$
187
+ // For canvas resources, wait for organizationId to be fetched and stored in state.
188
+ // For dataset resources, emit undefined immediately so the stream isn't blocked.
189
+ const organizationId$: Observable<string | undefined> = isCanvasResource(resource)
190
+ ? context.state.observable.pipe(
191
+ map((s) => s.organizationId),
192
+ filter((id): id is string => id !== undefined),
193
+ first(),
194
+ )
195
+ : of(undefined)
196
+
197
+ const subscription = combineLatest([userIds$, organizationId$])
147
198
  .pipe(
148
- switchMap((userIds) => {
199
+ switchMap(([userIds, organizationId]) => {
149
200
  if (userIds.length === 0) {
150
201
  return of([])
151
202
  }
152
203
  const userObservables = userIds.map((userId) =>
153
204
  getUserState(context.instance, {
154
205
  userId,
155
- resourceType: 'project',
156
- projectId:
157
- context.key.resource && isDatasetResource(context.key.resource)
158
- ? context.key.resource.projectId
159
- : undefined,
206
+ ...(isDatasetResource(resource)
207
+ ? {resourceType: 'project', projectId: resource.projectId}
208
+ : {resourceType: 'organization', organizationId}),
160
209
  }).pipe(filter((v): v is NonNullable<typeof v> => !!v)),
161
210
  )
162
211
  return combineLatest(userObservables)
163
212
  }),
164
213
  )
165
214
  .subscribe((users) => {
166
- if (!users) {
167
- return
168
- }
169
215
  context.state.set('presence/users', (prevState) => ({
170
216
  ...prevState,
171
217
  users: {
@@ -183,3 +229,13 @@ export const getPresence = bindActionByResource(
183
229
  },
184
230
  }),
185
231
  )
232
+
233
+ /** @beta */
234
+ export function getPresence(
235
+ instance: SanityInstance,
236
+ params?: {resource?: DocumentResource},
237
+ ): StateSource<UserPresence[]> {
238
+ // bit of a hack to support the old bound action by dataset
239
+ // in reality, this will always be passed a resource
240
+ return _getPresence(instance, params ?? {})
241
+ }
@@ -0,0 +1,137 @@
1
+ import {of} from 'rxjs'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {getProjectionState} from '../projection/getProjectionState'
5
+ import {type ProjectionValuePending} from '../projection/types'
6
+ import {createSanityInstance} from '../store/createSanityInstance'
7
+ import {type StateSource} from '../store/createStateSourceAction'
8
+ import {getPreviewState} from './getPreviewState'
9
+ import {type PreviewQueryResult} from './types'
10
+
11
+ vi.mock('../projection/getProjectionState')
12
+
13
+ describe('getPreviewState', () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks()
16
+ })
17
+
18
+ it('transforms projection result to preview format', () => {
19
+ const mockProjectionResult: PreviewQueryResult = {
20
+ _id: 'doc1',
21
+ _type: 'article',
22
+ _updatedAt: '2024-01-01',
23
+ titleCandidates: {title: 'Test Title'},
24
+ subtitleCandidates: {description: 'Test Description'},
25
+ media: null,
26
+ }
27
+
28
+ const mockProjectionState = {
29
+ getCurrent: vi.fn().mockReturnValue({
30
+ data: mockProjectionResult,
31
+ isPending: false,
32
+ } as ProjectionValuePending<PreviewQueryResult>),
33
+ subscribe: vi.fn(),
34
+ observable: of({
35
+ data: mockProjectionResult,
36
+ isPending: false,
37
+ } as ProjectionValuePending<PreviewQueryResult>),
38
+ }
39
+
40
+ vi.mocked(getProjectionState).mockReturnValue(
41
+ mockProjectionState as unknown as StateSource<
42
+ ProjectionValuePending<Record<string, unknown>> | undefined
43
+ >,
44
+ )
45
+
46
+ const instance = createSanityInstance({
47
+ projectId: 'test-project',
48
+ dataset: 'test-dataset',
49
+ })
50
+ const previewState = getPreviewState(instance, {
51
+ documentId: 'doc1',
52
+ documentType: 'article',
53
+ })
54
+
55
+ const result = previewState.getCurrent()
56
+
57
+ expect(result.data).toEqual({
58
+ title: 'Test Title',
59
+ subtitle: 'Test Description',
60
+ media: null,
61
+ })
62
+ expect(result.isPending).toBe(false)
63
+ })
64
+
65
+ it('returns null data when projection result is null', () => {
66
+ const mockProjectionState = {
67
+ getCurrent: vi.fn().mockReturnValue({
68
+ data: null,
69
+ isPending: true,
70
+ }),
71
+ subscribe: vi.fn(),
72
+ observable: of({data: null, isPending: true}),
73
+ }
74
+
75
+ vi.mocked(getProjectionState).mockReturnValue(mockProjectionState)
76
+
77
+ const instance = createSanityInstance({
78
+ projectId: 'test-project',
79
+ dataset: 'test-dataset',
80
+ })
81
+ const previewState = getPreviewState(instance, {
82
+ documentId: 'doc1',
83
+ documentType: 'article',
84
+ })
85
+
86
+ const result = previewState.getCurrent()
87
+
88
+ expect(result.data).toBeNull()
89
+ expect(result.isPending).toBe(true)
90
+ })
91
+
92
+ it('uses fallback title when no title candidates exist', () => {
93
+ const mockProjectionResult: PreviewQueryResult = {
94
+ _id: 'doc1',
95
+ _type: 'article',
96
+ _updatedAt: '2024-01-01',
97
+ titleCandidates: {},
98
+ subtitleCandidates: {},
99
+ media: null,
100
+ }
101
+
102
+ const mockProjectionState = {
103
+ getCurrent: vi.fn().mockReturnValue({
104
+ data: mockProjectionResult,
105
+ isPending: false,
106
+ } as ProjectionValuePending<PreviewQueryResult>),
107
+ subscribe: vi.fn(),
108
+ observable: of({
109
+ data: mockProjectionResult,
110
+ isPending: false,
111
+ } as ProjectionValuePending<PreviewQueryResult>),
112
+ }
113
+
114
+ vi.mocked(getProjectionState).mockReturnValue(
115
+ mockProjectionState as unknown as StateSource<
116
+ ProjectionValuePending<Record<string, unknown>> | undefined
117
+ >,
118
+ )
119
+
120
+ const instance = createSanityInstance({
121
+ projectId: 'test-project',
122
+ dataset: 'test-dataset',
123
+ })
124
+ const previewState = getPreviewState(instance, {
125
+ documentId: 'doc1',
126
+ documentType: 'article',
127
+ })
128
+
129
+ const result = previewState.getCurrent()
130
+
131
+ expect(result.data).toEqual({
132
+ title: 'article: doc1',
133
+ subtitle: undefined,
134
+ media: null,
135
+ })
136
+ })
137
+ })
@@ -0,0 +1,69 @@
1
+ import {map} from 'rxjs'
2
+
3
+ import {type DocumentHandle} from '../config/sanityConfig'
4
+ import {getProjectionState} from '../projection/getProjectionState'
5
+ import {type SanityInstance} from '../store/createSanityInstance'
6
+ import {type StateSource} from '../store/createStateSourceAction'
7
+ import {PREVIEW_PROJECTION} from './previewConstants'
8
+ import {transformProjectionToPreview} from './previewProjectionUtils'
9
+ import {type PreviewQueryResult, type PreviewValue, type ValuePending} from './types'
10
+
11
+ /**
12
+ * @beta
13
+ * @deprecated This type is deprecated and will be removed in a future release.
14
+ */
15
+ export type GetPreviewStateOptions = DocumentHandle
16
+
17
+ /**
18
+ * @beta
19
+ * @deprecated This function is deprecated and will be removed in a future release.
20
+ */
21
+ export function getPreviewState<TResult extends object>(
22
+ instance: SanityInstance,
23
+ options: GetPreviewStateOptions,
24
+ ): StateSource<ValuePending<TResult>>
25
+ /**
26
+ * @beta
27
+ * @deprecated This function is deprecated and will be removed in a future release.
28
+ */
29
+ export function getPreviewState(
30
+ instance: SanityInstance,
31
+ options: GetPreviewStateOptions,
32
+ ): StateSource<ValuePending<PreviewValue>>
33
+ /**
34
+ * @beta
35
+ * @deprecated This function is deprecated and will be removed in a future release.
36
+ */
37
+ export function getPreviewState(
38
+ instance: SanityInstance,
39
+ options: GetPreviewStateOptions,
40
+ ): StateSource<ValuePending<PreviewValue>> {
41
+ // Get the projection state
42
+ const projectionState = getProjectionState<PreviewQueryResult>(instance, {
43
+ ...options,
44
+ projection: PREVIEW_PROJECTION,
45
+ })
46
+
47
+ // Transform helper to convert projection result to preview format
48
+ const transformResult = (
49
+ current: ReturnType<typeof projectionState.getCurrent>,
50
+ ): ValuePending<PreviewValue> => {
51
+ if (!current || current.data === null) {
52
+ return {data: null, isPending: current?.isPending ?? false}
53
+ }
54
+
55
+ const previewValue = transformProjectionToPreview(instance, current.data, options.resource)
56
+
57
+ return {
58
+ data: previewValue,
59
+ isPending: current.isPending,
60
+ }
61
+ }
62
+
63
+ // Wrap the state source to transform projection results to preview format
64
+ return {
65
+ getCurrent: () => transformResult(projectionState.getCurrent()),
66
+ subscribe: (callback) => projectionState.subscribe(callback),
67
+ observable: projectionState.observable.pipe(map(transformResult)),
68
+ }
69
+ }
@@ -58,7 +58,10 @@ describe('normalizeMedia', () => {
58
58
  })
59
59
 
60
60
  describe('transformProjectionToPreview', () => {
61
- const instance = createSanityInstance()
61
+ const instance = createSanityInstance({
62
+ projectId: 'test-project',
63
+ dataset: 'test-dataset',
64
+ })
62
65
 
63
66
  beforeEach(async () => {
64
67
  vi.clearAllMocks()
@@ -78,11 +81,7 @@ describe('transformProjectionToPreview', () => {
78
81
  media: null,
79
82
  }
80
83
 
81
- const result = transformProjectionToPreview(
82
- instance,
83
- {projectId: 'p', dataset: 'd'},
84
- projectionResult,
85
- )
84
+ const result = transformProjectionToPreview(instance, projectionResult)
86
85
 
87
86
  expect(result).toEqual({
88
87
  title: 'My Title',
@@ -101,11 +100,7 @@ describe('transformProjectionToPreview', () => {
101
100
  media: null,
102
101
  }
103
102
 
104
- const result = transformProjectionToPreview(
105
- instance,
106
- {projectId: 'p', dataset: 'd'},
107
- projectionResult,
108
- )
103
+ const result = transformProjectionToPreview(instance, projectionResult)
109
104
 
110
105
  expect(result.title).toBe('article: doc1')
111
106
  expect(result.subtitle).toBeUndefined()
@@ -121,11 +116,7 @@ describe('transformProjectionToPreview', () => {
121
116
  media: {type: 'image-asset', _ref: 'image-abc123-200x200-png', url: ''},
122
117
  }
123
118
 
124
- const result = transformProjectionToPreview(
125
- instance,
126
- {projectId: 'p', dataset: 'd'},
127
- projectionResult,
128
- )
119
+ const result = transformProjectionToPreview(instance, projectionResult)
129
120
 
130
121
  expect(result).toEqual({
131
122
  title: 'My Title',
@@ -152,11 +143,7 @@ describe('transformProjectionToPreview', () => {
152
143
  },
153
144
  }
154
145
 
155
- const result = transformProjectionToPreview(
156
- instance,
157
- {projectId: 'p', dataset: 'd'},
158
- projectionResult,
159
- )
146
+ const result = transformProjectionToPreview(instance, projectionResult)
160
147
 
161
148
  expect(result).toEqual({
162
149
  title: 'My Title',
@@ -181,7 +168,7 @@ describe('transformProjectionToPreview', () => {
181
168
 
182
169
  const resource = {mediaLibraryId: 'test-library'}
183
170
 
184
- transformProjectionToPreview(instance, resource, projectionResult)
171
+ transformProjectionToPreview(instance, projectionResult, resource)
185
172
 
186
173
  const {getClient} = await import('../client/clientStore')
187
174
  expect(getClient).toHaveBeenCalledWith(instance, {
@@ -1,10 +1,10 @@
1
1
  import {type SanityClient} from '@sanity/client'
2
2
  import {createImageUrlBuilder} from '@sanity/image-url'
3
- import {isObject} from 'lodash-es'
4
3
 
5
4
  import {getClient} from '../client/clientStore'
6
5
  import {type DocumentResource} from '../config/sanityConfig'
7
6
  import {type SanityInstance} from '../store/createSanityInstance'
7
+ import {isObject} from '../utils/object'
8
8
  import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
9
9
  import {type PreviewQueryResult, type PreviewValue} from './types'
10
10
 
@@ -71,13 +71,17 @@ function findFirstDefined(
71
71
  */
72
72
  export function transformProjectionToPreview(
73
73
  instance: SanityInstance,
74
- resource: DocumentResource,
75
74
  projectionResult: PreviewQueryResult,
75
+ resource?: DocumentResource,
76
76
  ): PreviewValue {
77
77
  const title = findFirstDefined(TITLE_CANDIDATES, projectionResult.titleCandidates)
78
78
  const subtitle = findFirstDefined(SUBTITLE_CANDIDATES, projectionResult.subtitleCandidates, title)
79
79
 
80
- const client = getClient(instance, {apiVersion: API_VERSION, resource})
80
+ // Get a client for the resource (if provided) or use the instance config
81
+ const client = getClient(instance, {
82
+ apiVersion: API_VERSION,
83
+ resource,
84
+ })
81
85
 
82
86
  return {
83
87
  title: String(title || `${projectionResult._type}: ${projectionResult._id}`),
@@ -0,0 +1,64 @@
1
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
2
+
3
+ import {resolveProjection} from '../projection/resolveProjection'
4
+ import {type ProjectionValuePending} from '../projection/types'
5
+ import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
6
+ import {resolvePreview} from './resolvePreview'
7
+ import {type PreviewQueryResult} from './types'
8
+
9
+ vi.mock('../projection/resolveProjection')
10
+
11
+ describe('resolvePreview', () => {
12
+ let instance: SanityInstance
13
+ beforeEach(() => {
14
+ vi.clearAllMocks()
15
+ instance = createSanityInstance({projectId: 'p', dataset: 'd'})
16
+ })
17
+
18
+ afterEach(() => {
19
+ instance.dispose()
20
+ })
21
+
22
+ it('resolves and transforms projection result to preview format', async () => {
23
+ const mockProjectionResult: PreviewQueryResult = {
24
+ _id: 'doc1',
25
+ _type: 'article',
26
+ _updatedAt: '2024-01-01',
27
+ titleCandidates: {title: 'Resolved Title'},
28
+ subtitleCandidates: {description: 'Resolved Description'},
29
+ media: null,
30
+ }
31
+
32
+ vi.mocked(resolveProjection).mockResolvedValue({
33
+ data: mockProjectionResult,
34
+ isPending: false,
35
+ } as ProjectionValuePending<PreviewQueryResult>)
36
+
37
+ const result = await resolvePreview(instance, {
38
+ documentId: 'doc1',
39
+ documentType: 'article',
40
+ })
41
+
42
+ expect(result.data).toEqual({
43
+ title: 'Resolved Title',
44
+ subtitle: 'Resolved Description',
45
+ media: null,
46
+ })
47
+ expect(result.isPending).toBe(false)
48
+ })
49
+
50
+ it('returns null data when projection resolves with null', async () => {
51
+ vi.mocked(resolveProjection).mockResolvedValue({
52
+ data: null,
53
+ isPending: false,
54
+ })
55
+
56
+ const result = await resolvePreview(instance, {
57
+ documentId: 'doc1',
58
+ documentType: 'article',
59
+ })
60
+
61
+ expect(result.data).toBeNull()
62
+ expect(result.isPending).toBe(false)
63
+ })
64
+ })
@@ -0,0 +1,43 @@
1
+ import {type DocumentHandle} from '../config/sanityConfig'
2
+ import {resolveProjection} from '../projection/resolveProjection'
3
+ import {type SanityInstance} from '../store/createSanityInstance'
4
+ import {PREVIEW_PROJECTION} from './previewConstants'
5
+ import {transformProjectionToPreview} from './previewProjectionUtils'
6
+ import {type PreviewQueryResult, type PreviewValue, type ValuePending} from './types'
7
+
8
+ /**
9
+ * @beta
10
+ * @deprecated This type is deprecated and will be removed in a future release.
11
+ */
12
+ export type ResolvePreviewOptions = DocumentHandle
13
+
14
+ /**
15
+ * @beta
16
+ * @deprecated This function is deprecated and will be removed in a future release.
17
+ */
18
+ export async function resolvePreview(
19
+ instance: SanityInstance,
20
+ options: ResolvePreviewOptions,
21
+ ): Promise<ValuePending<PreviewValue>> {
22
+ // Resolve the projection
23
+ const projectionResult = await resolveProjection<PreviewQueryResult>(instance, {
24
+ ...options,
25
+ projection: PREVIEW_PROJECTION,
26
+ })
27
+
28
+ if (!projectionResult.data) {
29
+ return {data: null, isPending: projectionResult.isPending}
30
+ }
31
+
32
+ // Transform to preview format
33
+ const previewValue = transformProjectionToPreview(
34
+ instance,
35
+ projectionResult.data,
36
+ options.resource,
37
+ )
38
+
39
+ return {
40
+ data: previewValue,
41
+ isPending: projectionResult.isPending,
42
+ }
43
+ }
@@ -74,3 +74,13 @@ export type ValuePending<T> = {
74
74
  data: T | null
75
75
  isPending: boolean
76
76
  }
77
+
78
+ /**
79
+ * @public
80
+ * @deprecated This interface is kept for backwards compatibility but is no longer used internally.
81
+ * Preview state is now stored in the projection store.
82
+ */
83
+ export interface PreviewStoreState {
84
+ values: {[TDocumentId in string]?: ValuePending<PreviewValue>}
85
+ subscriptions: {[TDocumentId in string]?: {[TSubscriptionId in string]?: true}}
86
+ }
@@ -0,0 +1,37 @@
1
+ import {expectTypeOf, test} from 'vitest'
2
+
3
+ import {type SanityInstance} from '../store/createSanityInstance'
4
+ import {type StateSource} from '../store/createStateSourceAction'
5
+ import {type FetcherSnapshot} from '../store/fetcherStore'
6
+ import {type Project, project, type ProjectMember} from './project'
7
+
8
+ const instance = {} as SanityInstance
9
+
10
+ test('project.resolveState — resolves the wide-boolean detail shape', () => {
11
+ expectTypeOf(project.resolveState(instance, {projectId: 'p'})).resolves.toEqualTypeOf<
12
+ Project<boolean, boolean>
13
+ >()
14
+ })
15
+
16
+ test('Project — literal flags narrow members and features', () => {
17
+ expectTypeOf<Project<true, true>['members']>().toEqualTypeOf<ProjectMember[]>()
18
+ expectTypeOf<Project<true, true>['features']>().toEqualTypeOf<string[]>()
19
+ expectTypeOf<keyof Project<false, false> & ('members' | 'features')>().toEqualTypeOf<never>()
20
+ })
21
+
22
+ test('Project<boolean, boolean> — members and features are optional', () => {
23
+ type Item = Project<boolean, boolean>
24
+ expectTypeOf<Item['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
25
+ expectTypeOf<Item['features']>().toEqualTypeOf<string[] | undefined>()
26
+ })
27
+
28
+ test('project.getState — exposes the snapshot envelope', () => {
29
+ expectTypeOf(project.getState(instance, {projectId: 'p'})).toEqualTypeOf<
30
+ StateSource<FetcherSnapshot<Project<boolean, boolean>>>
31
+ >()
32
+ })
33
+
34
+ test('project — rejects non-boolean flag values', () => {
35
+ // @ts-expect-error includeMembers must be a boolean
36
+ void project.resolveState(instance, {includeMembers: 'yes'})
37
+ })