@sanity/sdk 2.8.0 → 2.9.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 (92) hide show
  1. package/dist/_chunks-dts/utils.d.ts +2396 -0
  2. package/dist/_chunks-es/_internal.js +129 -0
  3. package/dist/_chunks-es/_internal.js.map +1 -0
  4. package/dist/_chunks-es/createGroqSearchFilter.js +1460 -0
  5. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
  6. package/dist/_chunks-es/telemetryManager.js +87 -0
  7. package/dist/_chunks-es/telemetryManager.js.map +1 -0
  8. package/dist/_chunks-es/version.js +7 -0
  9. package/dist/_chunks-es/version.js.map +1 -0
  10. package/dist/_exports/_internal.d.ts +64 -0
  11. package/dist/_exports/_internal.js +20 -0
  12. package/dist/_exports/_internal.js.map +1 -0
  13. package/dist/index.d.ts +2 -2343
  14. package/dist/index.js +383 -1777
  15. package/dist/index.js.map +1 -1
  16. package/package.json +11 -4
  17. package/src/_exports/_internal.ts +14 -0
  18. package/src/_exports/index.ts +10 -1
  19. package/src/auth/authStore.test.ts +150 -1
  20. package/src/auth/authStore.ts +11 -11
  21. package/src/auth/dashboardAuth.ts +2 -2
  22. package/src/auth/handleAuthCallback.ts +9 -3
  23. package/src/auth/logout.test.ts +1 -1
  24. package/src/auth/logout.ts +1 -1
  25. package/src/auth/refreshStampedToken.test.ts +118 -1
  26. package/src/auth/refreshStampedToken.ts +3 -2
  27. package/src/auth/standaloneAuth.ts +9 -3
  28. package/src/auth/studioAuth.ts +34 -7
  29. package/src/auth/studioModeAuth.ts +2 -1
  30. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +10 -2
  31. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +5 -1
  32. package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
  33. package/src/auth/utils.ts +33 -0
  34. package/src/client/clientStore.test.ts +14 -0
  35. package/src/client/clientStore.ts +2 -1
  36. package/src/comlink/node/getNodeState.ts +2 -1
  37. package/src/config/sanityConfig.ts +6 -0
  38. package/src/document/actions.ts +18 -11
  39. package/src/document/applyDocumentActions.test.ts +7 -6
  40. package/src/document/applyDocumentActions.ts +10 -4
  41. package/src/document/documentStore.test.ts +536 -188
  42. package/src/document/documentStore.ts +142 -76
  43. package/src/document/events.ts +7 -2
  44. package/src/document/permissions.test.ts +18 -16
  45. package/src/document/permissions.ts +35 -11
  46. package/src/document/processActions.test.ts +359 -32
  47. package/src/document/processActions.ts +104 -76
  48. package/src/document/reducers.test.ts +117 -29
  49. package/src/document/reducers.ts +43 -36
  50. package/src/document/sharedListener.ts +16 -6
  51. package/src/document/util.ts +14 -0
  52. package/src/favorites/favorites.test.ts +9 -2
  53. package/src/presence/bifurTransport.ts +6 -1
  54. package/src/preview/getPreviewState.test.ts +115 -98
  55. package/src/preview/getPreviewState.ts +38 -60
  56. package/src/preview/previewProjectionUtils.test.ts +179 -0
  57. package/src/preview/previewProjectionUtils.ts +93 -0
  58. package/src/preview/resolvePreview.test.ts +42 -25
  59. package/src/preview/resolvePreview.ts +29 -10
  60. package/src/preview/{previewStore.ts → types.ts} +8 -17
  61. package/src/projection/getProjectionState.test.ts +16 -16
  62. package/src/projection/getProjectionState.ts +2 -1
  63. package/src/projection/projectionQuery.ts +2 -3
  64. package/src/projection/types.ts +1 -1
  65. package/src/query/queryStore.ts +2 -1
  66. package/src/releases/getPerspectiveState.ts +7 -6
  67. package/src/releases/releasesStore.test.ts +20 -5
  68. package/src/releases/releasesStore.ts +20 -8
  69. package/src/store/createStateSourceAction.test.ts +62 -0
  70. package/src/store/createStateSourceAction.ts +34 -39
  71. package/src/telemetry/__telemetry__/sdk.telemetry.ts +42 -0
  72. package/src/telemetry/devMode.test.ts +52 -0
  73. package/src/telemetry/devMode.ts +40 -0
  74. package/src/telemetry/initTelemetry.test.ts +225 -0
  75. package/src/telemetry/initTelemetry.ts +205 -0
  76. package/src/telemetry/telemetryManager.test.ts +263 -0
  77. package/src/telemetry/telemetryManager.ts +187 -0
  78. package/src/users/usersStore.test.ts +1 -0
  79. package/src/users/usersStore.ts +5 -1
  80. package/src/utils/createFetcherStore.test.ts +6 -4
  81. package/src/utils/createFetcherStore.ts +2 -1
  82. package/src/utils/getStagingApiHost.test.ts +21 -0
  83. package/src/utils/getStagingApiHost.ts +14 -0
  84. package/src/utils/ids.test.ts +1 -29
  85. package/src/utils/ids.ts +0 -10
  86. package/src/utils/setCleanupTimeout.ts +24 -0
  87. package/src/preview/previewQuery.test.ts +0 -236
  88. package/src/preview/previewQuery.ts +0 -153
  89. package/src/preview/previewStore.test.ts +0 -36
  90. package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
  91. package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
  92. package/src/preview/util.ts +0 -13
@@ -1,9 +1,12 @@
1
- import {getPublishedId} from '@sanity/client/csm'
1
+ import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
2
2
  import {type Mutation, type PatchOperations, type SanityDocumentLike} from '@sanity/types'
3
3
  import {omit} from 'lodash-es'
4
4
 
5
+ import {type DocumentHandle} from '../config/sanityConfig'
6
+ import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
5
7
  import {type StoreContext} from '../store/defineStore'
6
- import {getDraftId, insecureRandomId} from '../utils/ids'
8
+ import {insecureRandomId} from '../utils/ids'
9
+ import {setCleanupTimeout} from '../utils/setCleanupTimeout'
7
10
  import {type DocumentAction} from './actions'
8
11
  import {DOCUMENT_STATE_CLEAR_DELAY} from './documentConstants'
9
12
  import {type DocumentState, type DocumentStoreState} from './documentStore'
@@ -18,9 +21,13 @@ export type SyncTransactionState = Pick<
18
21
  'queued' | 'applied' | 'documentStates' | 'outgoing' | 'grants'
19
22
  >
20
23
 
24
+ type DocumentHandleLike = Pick<DocumentHandle, 'perspective'> & {
25
+ documentId?: string
26
+ liveEdit?: boolean
27
+ }
28
+
21
29
  type ActionMap = {
22
30
  create: 'sanity.action.document.version.create'
23
- createLiveEdit: 'sanity.action.document.create'
24
31
  discard: 'sanity.action.document.version.discard'
25
32
  unpublish: 'sanity.action.document.unpublish'
26
33
  delete: 'sanity.action.document.delete'
@@ -35,7 +42,6 @@ type OptimisticLock = {
35
42
 
36
43
  export type HttpAction =
37
44
  | {actionType: ActionMap['create']; publishedId: string; attributes: SanityDocumentLike}
38
- | {actionType: ActionMap['createLiveEdit']; publishedId: string; attributes: SanityDocumentLike}
39
45
  | {actionType: ActionMap['discard']; versionId: string; purge?: boolean}
40
46
  | {actionType: ActionMap['unpublish']; draftId: string; publishedId: string}
41
47
  | {actionType: ActionMap['delete']; publishedId: string; includeDrafts?: string[]}
@@ -115,9 +121,9 @@ export interface AppliedTransaction extends QueuedTransaction {
115
121
  outgoingActions: HttpAction[]
116
122
 
117
123
  /**
118
- * similar to `outgoingActions` but comprised of mutations instead of action.
119
- * this left here for debugging purposes but could be used to send mutations
120
- * to Content Lake instead of actions.
124
+ * similar to `outgoingActions` but comprised of mutations instead of actions.
125
+ * Useful for debugging, and is also used by liveEdit documents to send mutations,
126
+ * since they can't use the Actions API which is pretty dependent on the draft model.
121
127
  */
122
128
  outgoingMutations: Mutation[]
123
129
  }
@@ -145,7 +151,7 @@ export function queueTransaction(
145
151
  transaction: QueuedTransaction,
146
152
  ): SyncTransactionState {
147
153
  const {transactionId, actions} = transaction
148
- const prevWithSubscriptionIds = getDocumentIdsFromActions(actions).reduce(
154
+ const prevWithSubscriptionIds = getDocumentIdsFromHandleLikes(actions).reduce(
149
155
  (acc, id) => addSubscriptionIdToDocument(acc, id, transactionId),
150
156
  prev,
151
157
  )
@@ -163,7 +169,7 @@ export function removeQueuedTransaction(
163
169
  const transaction = prev.queued.find((t) => t.transactionId === transactionId)
164
170
  if (!transaction) return prev
165
171
 
166
- const prevWithSubscriptionIds = getDocumentIdsFromActions(transaction.actions).reduce(
172
+ const prevWithSubscriptionIds = getDocumentIdsFromHandleLikes(transaction.actions).reduce(
167
173
  (acc, id) => removeSubscriptionIdFromDocument(acc, id, transactionId),
168
174
  prev,
169
175
  )
@@ -179,7 +185,7 @@ export function applyFirstQueuedTransaction(prev: SyncTransactionState): SyncTra
179
185
  if (!queued) return prev
180
186
  if (!prev.grants) return prev
181
187
 
182
- const ids = getDocumentIdsFromActions(queued.actions)
188
+ const ids = getDocumentIdsFromHandleLikes(queued.actions)
183
189
  // the local value is only ever `undefined` if it has not been loaded yet
184
190
  // we can't get the next applied state unless all relevant documents are ready
185
191
  if (ids.some((id) => prev.documentStates[id]?.local === undefined)) return prev
@@ -265,6 +271,9 @@ export function batchAppliedTransactions([curr, ...rest]: AppliedTransaction[]):
265
271
  if (!next) return undefined
266
272
  if (next.disableBatching) return editAction
267
273
 
274
+ // Don't batch a liveEdit edit with a non-liveEdit edit — they route to different APIs
275
+ if (!!action.liveEdit !== !!next.actions[0]?.liveEdit) return editAction
276
+
268
277
  return {
269
278
  disableBatching: false,
270
279
  // Use the transactionId from the later (next) transaction.
@@ -337,7 +346,7 @@ export function cleanupOutgoingTransaction(prev: SyncTransactionState): SyncTran
337
346
  if (!outgoing) return prev
338
347
 
339
348
  let next = prev
340
- const ids = getDocumentIdsFromActions(outgoing.actions)
349
+ const ids = getDocumentIdsFromHandleLikes(outgoing.actions)
341
350
  for (const transactionId of outgoing.batchedTransactionIds) {
342
351
  for (const documentId of ids) {
343
352
  next = removeSubscriptionIdFromDocument(next, documentId, transactionId)
@@ -551,22 +560,10 @@ export function removeSubscriptionIdFromDocument(
551
560
 
552
561
  export function manageSubscriberIds(
553
562
  {state}: StoreContext<SyncTransactionState>,
554
- documentId: string | string[],
555
- options?: {expandDraftPublished?: boolean},
563
+ handles: DocumentHandleLike[],
556
564
  ): () => void {
557
- const expandDraftPublished = options?.expandDraftPublished ?? true
558
- const documentIds = Array.from(
559
- new Set(
560
- expandDraftPublished
561
- ? (Array.isArray(documentId) ? documentId : [documentId]).flatMap((id) => [
562
- getPublishedId(id),
563
- getDraftId(id),
564
- ])
565
- : Array.isArray(documentId)
566
- ? documentId
567
- : [documentId],
568
- ),
569
- )
565
+ const documentIds = getDocumentIdsFromHandleLikes(handles)
566
+
570
567
  const subscriptionId = insecureRandomId()
571
568
  state.set('addSubscribers', (prev) =>
572
569
  documentIds.reduce(
@@ -576,7 +573,7 @@ export function manageSubscriberIds(
576
573
  )
577
574
 
578
575
  return () => {
579
- setTimeout(() => {
576
+ setCleanupTimeout(() => {
580
577
  state.set('removeSubscribers', (prev) =>
581
578
  documentIds.reduce(
582
579
  (acc, id) => removeSubscriptionIdFromDocument(acc, id, subscriptionId),
@@ -587,13 +584,23 @@ export function manageSubscriberIds(
587
584
  }
588
585
  }
589
586
 
590
- export function getDocumentIdsFromActions(actions: DocumentAction[]): string[] {
591
- return Array.from(
592
- new Set(
593
- actions
594
- .map((i) => i.documentId)
595
- .filter((i) => typeof i === 'string')
596
- .flatMap((documentId) => [getPublishedId(documentId), getDraftId(documentId)]),
597
- ),
598
- )
587
+ // document handles are passed in via the public facing API, but we also need to
588
+ // pull the correct document ids from action bodies, which have similar but not
589
+ // identical shapes to the document handles.
590
+ function getDocumentIdsFromHandleLikes(handles: DocumentHandleLike[]): string[] {
591
+ return handles.flatMap((handle) => {
592
+ const idsForDocument = []
593
+ if (!handle.documentId) return []
594
+ if (handle.liveEdit) {
595
+ return [handle.documentId]
596
+ }
597
+ if (isReleasePerspective(handle.perspective)) {
598
+ idsForDocument.push(
599
+ getVersionId(DocumentId(handle.documentId), handle.perspective.releaseName),
600
+ )
601
+ }
602
+ idsForDocument.push(getPublishedId(DocumentId(handle.documentId)))
603
+ idsForDocument.push(getDraftId(DocumentId(handle.documentId)))
604
+ return idsForDocument
605
+ })
599
606
  }
@@ -14,6 +14,7 @@ import {
14
14
  } from 'rxjs'
15
15
 
16
16
  import {getClientState} from '../client/clientStore'
17
+ import {type DocumentSource, isDatasetSource} from '../config/sanityConfig'
17
18
  import {type SanityInstance} from '../store/createSanityInstance'
18
19
 
19
20
  const API_VERSION = 'v2025-05-06'
@@ -23,10 +24,15 @@ export interface SharedListener {
23
24
  dispose: () => void
24
25
  }
25
26
 
26
- export function createSharedListener(instance: SanityInstance): SharedListener {
27
+ export function createSharedListener(
28
+ instance: SanityInstance,
29
+ source?: DocumentSource,
30
+ ): SharedListener {
27
31
  const dispose$ = new Subject<void>()
28
32
  const events$ = getClientState(instance, {
29
33
  apiVersion: API_VERSION,
34
+ // TODO: remove in v3 when we're ready for everything to be queried via source
35
+ source: source && !isDatasetSource(source) ? source : undefined,
30
36
  }).observable.pipe(
31
37
  switchMap((client) =>
32
38
  // TODO: it seems like the client.listen method is not emitting disconnected
@@ -62,13 +68,17 @@ export function createSharedListener(instance: SanityInstance): SharedListener {
62
68
  }
63
69
  }
64
70
 
65
- export function createFetchDocument(instance: SanityInstance) {
71
+ export function createFetchDocument(instance: SanityInstance, source?: DocumentSource) {
66
72
  return function (documentId: string): Observable<SanityDocument | null> {
67
- return getClientState(instance, {apiVersion: API_VERSION}).observable.pipe(
73
+ return getClientState(instance, {
74
+ apiVersion: API_VERSION,
75
+ // TODO: remove in v3 when we're ready for everything to be queried via source
76
+ source: source && !isDatasetSource(source) ? source : undefined,
77
+ }).observable.pipe(
68
78
  switchMap((client) => {
69
- // TODO: remove this once the client is updated to v7 the new type is available in @sanity/mutate/_unstable_store
70
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
- const loadDocument = createDocumentLoaderFromClient(client as any)
79
+ // creates a observable request to the /doc/{documentId} endpoint for a given document id
80
+ // should work across all kinds of document IDs (drafts.**, version.**., etc.)
81
+ const loadDocument = createDocumentLoaderFromClient(client)
72
82
  return loadDocument(documentId)
73
83
  }),
74
84
  map((result) => {
@@ -0,0 +1,14 @@
1
+ import {DocumentId, getPublishedId, getVersionId} from '@sanity/id-utils'
2
+
3
+ import {type DocumentHandle} from '../config/sanityConfig'
4
+ import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
5
+
6
+ export function getEffectiveDocumentId(doc: DocumentHandle): string {
7
+ if (doc.liveEdit) {
8
+ return doc.documentId
9
+ } else if (isReleasePerspective(doc.perspective)) {
10
+ return getVersionId(DocumentId(doc.documentId), doc.perspective.releaseName)
11
+ } else {
12
+ return getPublishedId(DocumentId(doc.documentId))
13
+ }
14
+ }
@@ -126,9 +126,15 @@ describe('favoritesStore', () => {
126
126
  })
127
127
 
128
128
  it('handles error and returns default response', async () => {
129
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
129
130
  setupMockStateSource({fetchImpl: vi.fn().mockRejectedValue(new Error('Failed to fetch'))})
130
131
  const result = await resolveFavoritesState(instance!, mockContext)
131
132
  expect(result).toEqual({isFavorited: false})
133
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
134
+ 'Favorites service connection error',
135
+ expect.any(Error),
136
+ )
137
+ consoleErrorSpy.mockRestore()
132
138
  })
133
139
 
134
140
  it('shares observable between multiple subscribers and cleans up', async () => {
@@ -140,8 +146,9 @@ describe('favoritesStore', () => {
140
146
  await firstValueFrom(state.observable)
141
147
  expect(mockFetch).toHaveBeenCalledTimes(1)
142
148
  // Second subscriber should use cached response
143
- const sub2 = state.subscribe()
144
- await firstValueFrom(state.observable)
149
+ const state2 = getFavoritesState(instance!, mockContext)
150
+ const sub2 = state2.subscribe()
151
+ await firstValueFrom(state2.observable)
145
152
  expect(mockFetch).toHaveBeenCalledTimes(1)
146
153
  // Cleanup
147
154
  sub1()
@@ -36,8 +36,13 @@ type IncomingBifurEvent = RollCallEvent | BifurStateMessage | BifurDisconnectMes
36
36
 
37
37
  function getBifurClient(client: SanityClient, token$: Observable<string | null>): BifurClient {
38
38
  const bifurVersionedClient = client.withConfig({apiVersion: '2022-06-30'})
39
- const {dataset, url: baseUrl, requestTagPrefix = 'sanity.studio'} = bifurVersionedClient.config()
39
+ const {
40
+ dataset,
41
+ url: baseUrl,
42
+ requestTagPrefix = 'sanity.sdk.presence',
43
+ } = bifurVersionedClient.config()
40
44
  const url = `${baseUrl.replace(/\/+$/, '')}/socket/${dataset}`.replace(/^http/, 'ws')
45
+
41
46
  const urlWithTag = `${url}?tag=${requestTagPrefix}`
42
47
 
43
48
  return fromUrl(urlWithTag, {token$})
@@ -1,120 +1,137 @@
1
- import {NEVER} from 'rxjs'
2
- import {describe, it} from 'vitest'
1
+ import {of} from 'rxjs'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
3
 
4
- import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
5
- import {type StoreState} from '../store/createStoreState'
6
- import {insecureRandomId} from '../utils/ids'
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'
7
8
  import {getPreviewState} from './getPreviewState'
8
- import {type PreviewStoreState} from './previewStore'
9
- import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
10
- import {STABLE_EMPTY_PREVIEW} from './util'
9
+ import {type PreviewQueryResult} from './types'
11
10
 
12
- vi.mock('../utils/ids', async (importOriginal) => {
13
- const util = await importOriginal<typeof import('../utils/ids')>()
14
- return {...util, insecureRandomId: vi.fn(util.insecureRandomId)}
15
- })
16
-
17
- vi.mock('./subscribeToStateAndFetchBatches.ts')
11
+ vi.mock('../projection/getProjectionState')
18
12
 
19
13
  describe('getPreviewState', () => {
20
- let instance: SanityInstance
21
- const docHandle = {documentId: 'exampleId', documentType: 'exampleType'}
22
- let state: StoreState<PreviewStoreState & {extra?: unknown}>
23
-
24
14
  beforeEach(() => {
25
- // capture state
26
- vi.mocked(subscribeToStateAndFetchBatches).mockImplementation((context) => {
27
- state = context.state
28
- return NEVER.subscribe()
29
- })
30
-
31
- instance = createSanityInstance({projectId: 'exampleProject', dataset: 'exampleDataset'})
15
+ vi.clearAllMocks()
32
16
  })
33
17
 
34
- afterEach(() => {
35
- instance.dispose()
36
- })
37
-
38
- it('returns a state source that emits when the preview value changes', () => {
39
- const previewState = getPreviewState(instance, docHandle)
40
- expect(previewState.getCurrent()).toBe(STABLE_EMPTY_PREVIEW)
41
-
42
- const subscriber = vi.fn()
43
- previewState.subscribe(subscriber)
44
-
45
- // emit unrelated state changes
46
- state.set('updateLastLiveEventId', {extra: 'unrelated change'})
47
- expect(subscriber).toHaveBeenCalledTimes(0)
48
-
49
- state.set('relatedChange', (prev) => ({
50
- values: {...prev.values, exampleId: {data: {title: 'Changed!'}, isPending: false}},
51
- }))
52
- expect(subscriber).toHaveBeenCalledTimes(1)
53
-
54
- state.set('unrelatedChange', (prev) => ({
55
- values: {
56
- ...prev.values,
57
- unrelatedId: {data: {title: 'Unrelated Document'}, isPending: false},
58
- },
59
- }))
60
- expect(subscriber).toHaveBeenCalledTimes(1)
61
-
62
- state.set('relatedChange', (prev) => ({
63
- values: {...prev.values, exampleId: {data: {title: 'Changed again!'}, isPending: false}},
64
- }))
65
- expect(subscriber).toHaveBeenCalledTimes(2)
66
- })
67
-
68
- it('adds a subscription ID and document type to the state on subscription', () => {
69
- const previewState = getPreviewState(instance, docHandle)
70
-
71
- expect(state.get().subscriptions).toEqual({})
72
- vi.mocked(insecureRandomId)
73
- .mockImplementationOnce(() => 'pseudoRandomId1')
74
- .mockImplementationOnce(() => 'pseudoRandomId2')
75
-
76
- const unsubscribe1 = previewState.subscribe(vi.fn())
77
- const unsubscribe2 = previewState.subscribe(vi.fn())
78
-
79
- expect(state.get().subscriptions).toEqual({
80
- exampleId: {pseudoRandomId1: true, pseudoRandomId2: true},
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',
81
49
  })
82
-
83
- unsubscribe2()
84
- expect(state.get().subscriptions).toEqual({
85
- exampleId: {pseudoRandomId1: true},
50
+ const previewState = getPreviewState(instance, {
51
+ documentId: 'doc1',
52
+ documentType: 'article',
86
53
  })
87
54
 
88
- unsubscribe1()
89
- expect(state.get().subscriptions).toEqual({})
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)
90
63
  })
91
64
 
92
- it('resets to pending false on unsubscribe if the subscription is the last one', () => {
93
- const previewState = getPreviewState(instance, docHandle)
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
+ })
94
85
 
95
- state.set('presetValueToPending', (prev) => ({
96
- values: {...prev.values, [docHandle.documentId]: {data: {title: 'Foo'}, isPending: true}},
97
- }))
86
+ const result = previewState.getCurrent()
98
87
 
99
- const unsubscribe1 = previewState.subscribe(vi.fn())
100
- const unsubscribe2 = previewState.subscribe(vi.fn())
88
+ expect(result.data).toBeNull()
89
+ expect(result.isPending).toBe(true)
90
+ })
101
91
 
102
- expect(state.get().values[docHandle.documentId]).toEqual({
103
- data: {title: 'Foo'},
104
- isPending: true,
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',
105
123
  })
106
-
107
- unsubscribe1()
108
- expect(state.get().values[docHandle.documentId]).toEqual({
109
- data: {title: 'Foo'},
110
- isPending: true,
124
+ const previewState = getPreviewState(instance, {
125
+ documentId: 'doc1',
126
+ documentType: 'article',
111
127
  })
112
128
 
113
- unsubscribe2()
114
- expect(state.get().subscriptions).toEqual({})
115
- expect(state.get().values[docHandle.documentId]).toEqual({
116
- data: {title: 'Foo'},
117
- isPending: false,
129
+ const result = previewState.getCurrent()
130
+
131
+ expect(result.data).toEqual({
132
+ title: 'article: doc1',
133
+ subtitle: undefined,
134
+ media: null,
118
135
  })
119
136
  })
120
137
  })
@@ -1,29 +1,22 @@
1
- import {omit} from 'lodash-es'
1
+ import {map} from 'rxjs'
2
2
 
3
3
  import {type DocumentHandle} from '../config/sanityConfig'
4
- import {bindActionByDataset} from '../store/createActionBinder'
4
+ import {getProjectionState} from '../projection/getProjectionState'
5
5
  import {type SanityInstance} from '../store/createSanityInstance'
6
- import {
7
- createStateSourceAction,
8
- type SelectorContext,
9
- type StateSource,
10
- } from '../store/createStateSourceAction'
11
- import {getPublishedId, insecureRandomId} from '../utils/ids'
12
- import {
13
- previewStore,
14
- type PreviewStoreState,
15
- type PreviewValue,
16
- type ValuePending,
17
- } from './previewStore'
18
- import {STABLE_EMPTY_PREVIEW} from './util'
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'
19
10
 
20
11
  /**
21
12
  * @beta
13
+ * @deprecated This type is deprecated and will be removed in a future release.
22
14
  */
23
15
  export type GetPreviewStateOptions = DocumentHandle
24
16
 
25
17
  /**
26
18
  * @beta
19
+ * @deprecated This function is deprecated and will be removed in a future release.
27
20
  */
28
21
  export function getPreviewState<TResult extends object>(
29
22
  instance: SanityInstance,
@@ -31,6 +24,7 @@ export function getPreviewState<TResult extends object>(
31
24
  ): StateSource<ValuePending<TResult>>
32
25
  /**
33
26
  * @beta
27
+ * @deprecated This function is deprecated and will be removed in a future release.
34
28
  */
35
29
  export function getPreviewState(
36
30
  instance: SanityInstance,
@@ -38,54 +32,38 @@ export function getPreviewState(
38
32
  ): StateSource<ValuePending<PreviewValue>>
39
33
  /**
40
34
  * @beta
35
+ * @deprecated This function is deprecated and will be removed in a future release.
41
36
  */
42
37
  export function getPreviewState(
43
- ...args: Parameters<typeof _getPreviewState>
44
- ): StateSource<ValuePending<object>> {
45
- return _getPreviewState(...args)
46
- }
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
+ })
47
46
 
48
- /**
49
- * @beta
50
- */
51
- export const _getPreviewState = bindActionByDataset(
52
- previewStore,
53
- createStateSourceAction({
54
- selector: (
55
- {state}: SelectorContext<PreviewStoreState>,
56
- docHandle: GetPreviewStateOptions,
57
- ): ValuePending<object> => state.values[docHandle.documentId] ?? STABLE_EMPTY_PREVIEW,
58
- onSubscribe: ({state}, docHandle: GetPreviewStateOptions) => {
59
- const subscriptionId = insecureRandomId()
60
- const documentId = getPublishedId(docHandle.documentId)
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
+ }
61
54
 
62
- state.set('addSubscription', (prev) => ({
63
- subscriptions: {
64
- ...prev.subscriptions,
65
- [documentId]: {
66
- ...prev.subscriptions[documentId],
67
- [subscriptionId]: true,
68
- },
69
- },
70
- }))
55
+ const previewValue = transformProjectionToPreview(instance, current.data, options.source)
71
56
 
72
- return () => {
73
- state.set('removeSubscription', (prev): Partial<PreviewStoreState> => {
74
- const documentSubscriptions = omit(prev.subscriptions[documentId], subscriptionId)
75
- const hasSubscribers = !!Object.keys(documentSubscriptions).length
76
- const prevValue = prev.values[documentId]
77
- const previewValue = prevValue?.data ? prevValue.data : null
57
+ return {
58
+ data: previewValue,
59
+ isPending: current.isPending,
60
+ }
61
+ }
78
62
 
79
- return {
80
- subscriptions: hasSubscribers
81
- ? {...prev.subscriptions, [documentId]: documentSubscriptions}
82
- : omit(prev.subscriptions, documentId),
83
- values: hasSubscribers
84
- ? prev.values
85
- : {...prev.values, [documentId]: {data: previewValue, isPending: false}},
86
- }
87
- })
88
- }
89
- },
90
- }),
91
- )
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
+ }