@sanity/sdk 2.8.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
@@ -1,120 +0,0 @@
1
- import {NEVER} from 'rxjs'
2
- import {describe, it} from 'vitest'
3
-
4
- import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
5
- import {type StoreState} from '../store/createStoreState'
6
- import {insecureRandomId} from '../utils/ids'
7
- import {getPreviewState} from './getPreviewState'
8
- import {type PreviewStoreState} from './previewStore'
9
- import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
10
- import {STABLE_EMPTY_PREVIEW} from './util'
11
-
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')
18
-
19
- describe('getPreviewState', () => {
20
- let instance: SanityInstance
21
- const docHandle = {documentId: 'exampleId', documentType: 'exampleType'}
22
- let state: StoreState<PreviewStoreState & {extra?: unknown}>
23
-
24
- 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'})
32
- })
33
-
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},
81
- })
82
-
83
- unsubscribe2()
84
- expect(state.get().subscriptions).toEqual({
85
- exampleId: {pseudoRandomId1: true},
86
- })
87
-
88
- unsubscribe1()
89
- expect(state.get().subscriptions).toEqual({})
90
- })
91
-
92
- it('resets to pending false on unsubscribe if the subscription is the last one', () => {
93
- const previewState = getPreviewState(instance, docHandle)
94
-
95
- state.set('presetValueToPending', (prev) => ({
96
- values: {...prev.values, [docHandle.documentId]: {data: {title: 'Foo'}, isPending: true}},
97
- }))
98
-
99
- const unsubscribe1 = previewState.subscribe(vi.fn())
100
- const unsubscribe2 = previewState.subscribe(vi.fn())
101
-
102
- expect(state.get().values[docHandle.documentId]).toEqual({
103
- data: {title: 'Foo'},
104
- isPending: true,
105
- })
106
-
107
- unsubscribe1()
108
- expect(state.get().values[docHandle.documentId]).toEqual({
109
- data: {title: 'Foo'},
110
- isPending: true,
111
- })
112
-
113
- unsubscribe2()
114
- expect(state.get().subscriptions).toEqual({})
115
- expect(state.get().values[docHandle.documentId]).toEqual({
116
- data: {title: 'Foo'},
117
- isPending: false,
118
- })
119
- })
120
- })
@@ -1,91 +0,0 @@
1
- import {omit} from 'lodash-es'
2
-
3
- import {type DocumentHandle} from '../config/sanityConfig'
4
- import {bindActionByDataset} from '../store/createActionBinder'
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'
19
-
20
- /**
21
- * @beta
22
- */
23
- export type GetPreviewStateOptions = DocumentHandle
24
-
25
- /**
26
- * @beta
27
- */
28
- export function getPreviewState<TResult extends object>(
29
- instance: SanityInstance,
30
- options: GetPreviewStateOptions,
31
- ): StateSource<ValuePending<TResult>>
32
- /**
33
- * @beta
34
- */
35
- export function getPreviewState(
36
- instance: SanityInstance,
37
- options: GetPreviewStateOptions,
38
- ): StateSource<ValuePending<PreviewValue>>
39
- /**
40
- * @beta
41
- */
42
- export function getPreviewState(
43
- ...args: Parameters<typeof _getPreviewState>
44
- ): StateSource<ValuePending<object>> {
45
- return _getPreviewState(...args)
46
- }
47
-
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)
61
-
62
- state.set('addSubscription', (prev) => ({
63
- subscriptions: {
64
- ...prev.subscriptions,
65
- [documentId]: {
66
- ...prev.subscriptions[documentId],
67
- [subscriptionId]: true,
68
- },
69
- },
70
- }))
71
-
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
78
-
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
- )
@@ -1,236 +0,0 @@
1
- import {describe, expect, it} from 'vitest'
2
-
3
- import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
4
- import {createPreviewQuery, normalizeMedia, processPreviewQuery} from './previewQuery'
5
- import {STABLE_EMPTY_PREVIEW} from './util'
6
-
7
- describe('createPreviewQuery', () => {
8
- it('creates a query and params for given ids and schema', () => {
9
- const ids = new Set(['book1', 'book2'])
10
- const {query, params} = createPreviewQuery(ids)
11
- expect(query).toMatch(/.*_id in \$__ids_.*/)
12
- expect(Object.keys(params)).toHaveLength(1)
13
- })
14
- })
15
-
16
- describe('processPreviewQuery', () => {
17
- it('returns STABLE_EMPTY_PREVIEW if documentType is missing', () => {
18
- const ids = new Set(['doc1'])
19
- const result = processPreviewQuery({
20
- projectId: 'p',
21
- dataset: 'd',
22
- results: [],
23
- ids,
24
- })
25
-
26
- expect(result['doc1']).toEqual(STABLE_EMPTY_PREVIEW)
27
- })
28
-
29
- it('returns STABLE_EMPTY_PREVIEW if no candidates found', () => {
30
- const ids = new Set(['doc1'])
31
- const result = processPreviewQuery({
32
- projectId: 'p',
33
- dataset: 'd',
34
- results: [], // no results, so no selectResult
35
- ids,
36
- })
37
-
38
- expect(result['doc1']).toEqual(STABLE_EMPTY_PREVIEW)
39
- })
40
-
41
- // it.only('returns STABLE_ERROR_PREVIEW if an error occurs', () => {
42
- // const ids = new Set(['doc1'])
43
- // const result = processPreviewQuery({
44
- // projectId: 'p',
45
- // dataset: 'd',
46
- // results: [
47
- // {
48
- // _id: 'doc1',
49
- // _type: 'someType',
50
- // _updatedAt: new Date().toISOString(),
51
- // titleCandidates: {title: null},
52
- // subtitleCandidates: {subtitle: null},
53
- // },
54
- // ], // no results, so no selectResult
55
- // ids,
56
- // })
57
-
58
- // expect(result['doc1']).toEqual(STABLE_ERROR_PREVIEW)
59
- // })
60
-
61
- it('processes query results into preview values', () => {
62
- const results = [
63
- {
64
- _id: 'person1',
65
- _type: 'person',
66
- _updatedAt: '2021-01-01',
67
- titleCandidates: {title: 'John'},
68
- subtitleCandidates: {subtitle: null},
69
- },
70
- ]
71
-
72
- const processed = processPreviewQuery({
73
- projectId: 'p',
74
- dataset: 'd',
75
- ids: new Set(['person1']),
76
- results,
77
- })
78
-
79
- const val = processed['person1']
80
- expect(val?.data).toEqual({
81
- title: 'John',
82
- media: null,
83
- _status: {lastEditedPublishedAt: '2021-01-01'},
84
- })
85
- expect(val?.isPending).toBe(false)
86
- })
87
-
88
- it('resolves status preferring draft over published when available', () => {
89
- const results = [
90
- {
91
- _id: 'drafts.article1',
92
- _type: 'article',
93
- _updatedAt: '2023-12-16T12:00:00Z',
94
- titleCandidates: {
95
- title: 'Draft Title',
96
- name: null,
97
- },
98
- subtitleCandidates: {
99
- title: null,
100
- name: null,
101
- },
102
- },
103
- {
104
- _id: 'article1',
105
- _type: 'article',
106
- _updatedAt: '2023-12-15T12:00:00Z',
107
- titleCandidates: {
108
- title: 'Published Title',
109
- name: null,
110
- },
111
- subtitleCandidates: {
112
- title: null,
113
- name: null,
114
- },
115
- },
116
- ]
117
-
118
- const processed = processPreviewQuery({
119
- projectId: 'p',
120
- dataset: 'd',
121
- ids: new Set(['article1']),
122
- results,
123
- })
124
-
125
- const val = processed['article1']
126
- expect(val?.data).toEqual({
127
- title: 'Draft Title',
128
- media: null,
129
- _status: {
130
- lastEditedDraftAt: '2023-12-16T12:00:00Z',
131
- lastEditedPublishedAt: '2023-12-15T12:00:00Z',
132
- },
133
- })
134
- expect(val?.isPending).toBe(false)
135
- })
136
-
137
- it('uses the first defined title or subtitle from the candidates', () => {
138
- const titleCandidates = {
139
- title: 'Draft Title',
140
- name: 'Draft Name',
141
- label: 'Draft Label',
142
- heading: 'Draft Heading',
143
- header: 'Draft Header',
144
- caption: 'Draft Caption',
145
- }
146
-
147
- const subtitleCandidates = {
148
- subtitle: 'Draft Subtitle',
149
- description: 'Draft Description',
150
- name: 'Draft Name',
151
- label: 'Draft Label',
152
- heading: 'Draft Heading',
153
- header: 'Draft Header',
154
- caption: 'Draft Caption',
155
- }
156
-
157
- const results = [
158
- {
159
- _id: 'article1',
160
- _type: 'article',
161
- _updatedAt: '2023-12-15T12:00:00Z',
162
- titleCandidates,
163
- subtitleCandidates,
164
- },
165
- ]
166
-
167
- const processed = processPreviewQuery({
168
- projectId: 'p',
169
- dataset: 'd',
170
- ids: new Set(['article1']),
171
- results,
172
- })
173
-
174
- const val = processed['article1']
175
- expect(val?.data).toEqual({
176
- title: titleCandidates[TITLE_CANDIDATES[0] as keyof typeof titleCandidates],
177
- subtitle: subtitleCandidates[SUBTITLE_CANDIDATES[0] as keyof typeof subtitleCandidates],
178
- media: null,
179
- _status: {lastEditedPublishedAt: '2023-12-15T12:00:00Z'},
180
- })
181
- expect(val?.isPending).toBe(false)
182
- })
183
- })
184
-
185
- describe('normalizeMedia', () => {
186
- it('returns null if media is null or undefined', () => {
187
- expect(normalizeMedia(null, 'projectId', 'dataset')).toBeNull()
188
- expect(normalizeMedia(undefined, 'projectId', 'dataset')).toBeNull()
189
- })
190
-
191
- it('returns null if media does not have a valid asset', () => {
192
- const invalidMedia1 = {media: {_ref: 'image-abc123-200x200-png'}} // Missing `asset` property
193
- const invalidMedia2 = {asset: {ref: 'image-abc123-200x200-png'}} // Incorrect property name `ref`
194
- expect(normalizeMedia(invalidMedia1, 'projectId', 'dataset')).toBeNull()
195
- expect(normalizeMedia(invalidMedia2, 'projectId', 'dataset')).toBeNull()
196
- })
197
-
198
- it('returns null if media is not an object', () => {
199
- expect(normalizeMedia(123, 'projectId', 'dataset')).toBeNull()
200
- expect(normalizeMedia('invalid', 'projectId', 'dataset')).toBeNull()
201
- })
202
-
203
- it('returns a normalized URL for valid image asset objects', () => {
204
- const validMedia = {type: 'image-asset', _ref: 'image-abc123-200x200-png'}
205
- const result = normalizeMedia(validMedia, 'projectId', 'dataset')
206
- expect(result).toEqual({
207
- type: 'image-asset',
208
- _ref: 'image-abc123-200x200-png',
209
- url: 'https://cdn.sanity.io/images/projectId/dataset/abc123-200x200.png',
210
- })
211
- })
212
-
213
- it('throws an error for invalid asset IDs in the media', () => {
214
- const invalidMedia = {type: 'image-asset', _ref: 'invalid-asset-id'}
215
- expect(() => normalizeMedia(invalidMedia, 'projectId', 'dataset')).toThrow(
216
- 'Invalid asset ID `invalid-asset-id`. Expected: image-{assetName}-{width}x{height}-{format}',
217
- )
218
- })
219
-
220
- it('handles image assets with expected URL format', () => {
221
- const media = {type: 'image-asset', _ref: 'image-xyz456-400x400-jpg'}
222
- const result = normalizeMedia(media, 'projectId', 'dataset')
223
- expect(result).toEqual({
224
- type: 'image-asset',
225
- _ref: 'image-xyz456-400x400-jpg',
226
- url: 'https://cdn.sanity.io/images/projectId/dataset/xyz456-400x400.jpg',
227
- })
228
- })
229
-
230
- it('ensures assetIdToUrl throws for asset IDs with missing groups', () => {
231
- const invalidMedia = {type: 'image-asset', _ref: 'image-missinggroups'}
232
- expect(() => normalizeMedia(invalidMedia, 'projectId', 'dataset')).toThrow(
233
- 'Invalid asset ID `image-missinggroups`. Expected: image-{assetName}-{width}x{height}-{format}',
234
- )
235
- })
236
- })
@@ -1,153 +0,0 @@
1
- import {isObject} from 'lodash-es'
2
-
3
- import {hashString} from '../utils/hashString'
4
- import {getDraftId, getPublishedId} from '../utils/ids'
5
- import {PREVIEW_PROJECTION, SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
6
- import {
7
- type PreviewQueryResult,
8
- type PreviewStoreState,
9
- type PreviewValue,
10
- type ValuePending,
11
- } from './previewStore'
12
- import {STABLE_EMPTY_PREVIEW, STABLE_ERROR_PREVIEW} from './util'
13
-
14
- interface ProcessPreviewQueryOptions {
15
- projectId: string
16
- dataset: string
17
- ids: Set<string>
18
- results: PreviewQueryResult[]
19
- }
20
-
21
- /**
22
- * Converts an asset ID to a URL.
23
- *
24
- * @internal
25
- */
26
- function assetIdToUrl(assetId: string, projectId: string, dataset: string) {
27
- const pattern = /^image-(?<assetName>[A-Za-z0-9]+)-(?<dimensions>\d+x\d+)-(?<format>[a-z]+)$/
28
- const match = assetId.match(pattern)
29
- if (!match?.groups) {
30
- throw new Error(
31
- `Invalid asset ID \`${assetId}\`. Expected: image-{assetName}-{width}x{height}-{format}`,
32
- )
33
- }
34
-
35
- const {assetName, dimensions, format} = match.groups
36
- return `https://cdn.sanity.io/images/${projectId}/${dataset}/${assetName}-${dimensions}.${format}`
37
- }
38
-
39
- /**
40
- * Checks if the provided value has `_ref` property that is a string and starts with `image-`
41
- */
42
- function hasImageRef<T>(value: unknown): value is T & {_ref: string} {
43
- return isObject(value) && '_ref' in value && typeof (value as {_ref: unknown})._ref === 'string'
44
- }
45
-
46
- /**
47
- * Normalizes a media asset to a preview value.
48
- * Adds a url to a media asset reference.
49
- *
50
- * @internal
51
- */
52
- export function normalizeMedia(
53
- media: unknown,
54
- projectId: string,
55
- dataset: string,
56
- ): PreviewValue['media'] {
57
- if (!media) return null
58
- if (!hasImageRef(media)) return null
59
- return {
60
- type: 'image-asset',
61
- _ref: media._ref,
62
- url: assetIdToUrl(media._ref, projectId, dataset),
63
- }
64
- }
65
-
66
- /**
67
- * Finds a single field value from a set of candidates based on a priority list of field names.
68
- * Returns the first non-empty string value found from the candidates matching the priority list order.
69
- *
70
- * @internal
71
- */
72
- function findFirstDefined(
73
- fieldsToSearch: string[],
74
- candidates: Record<string, unknown>,
75
- exclude?: unknown,
76
- ): string | undefined {
77
- if (!candidates) return undefined
78
-
79
- for (const field of fieldsToSearch) {
80
- const value = candidates[field]
81
- if (typeof value === 'string' && value.trim() !== '' && value !== exclude) {
82
- return value
83
- }
84
- }
85
-
86
- return undefined
87
- }
88
-
89
- export function processPreviewQuery({
90
- projectId,
91
- dataset,
92
- ids,
93
- results,
94
- }: ProcessPreviewQueryOptions): PreviewStoreState['values'] {
95
- const resultMap = results.reduce<{[TDocumentId in string]?: PreviewQueryResult}>((acc, next) => {
96
- acc[next._id] = next
97
- return acc
98
- }, {})
99
-
100
- return Object.fromEntries(
101
- Array.from(ids).map((id): [string, ValuePending<PreviewValue>] => {
102
- const publishedId = getPublishedId(id)
103
- const draftId = getDraftId(id)
104
-
105
- const draftResult = resultMap[draftId]
106
- const publishedResult = resultMap[publishedId]
107
-
108
- if (!draftResult && !publishedResult) return [id, STABLE_EMPTY_PREVIEW]
109
-
110
- try {
111
- const result = draftResult || publishedResult
112
- if (!result) return [id, STABLE_EMPTY_PREVIEW]
113
- const title = findFirstDefined(TITLE_CANDIDATES, result.titleCandidates)
114
- const subtitle = findFirstDefined(SUBTITLE_CANDIDATES, result.subtitleCandidates, title)
115
- const preview: Omit<PreviewValue, 'status'> = {
116
- title: String(title || `${result._type}: ${result._id}`),
117
- subtitle: subtitle || undefined,
118
- media: normalizeMedia(result.media, projectId, dataset),
119
- }
120
-
121
- const _status: PreviewValue['_status'] = {
122
- ...(draftResult?._updatedAt && {lastEditedDraftAt: draftResult._updatedAt}),
123
- ...(publishedResult?._updatedAt && {lastEditedPublishedAt: publishedResult._updatedAt}),
124
- }
125
-
126
- return [id, {data: {...preview, _status}, isPending: false}]
127
- } catch (e) {
128
- // TODO: replace this with bubbling the error
129
- // eslint-disable-next-line no-console
130
- console.warn(e)
131
- return [id, STABLE_ERROR_PREVIEW]
132
- }
133
- }),
134
- )
135
- }
136
-
137
- interface CreatePreviewQueryResult {
138
- query: string
139
- params: Record<string, string[]>
140
- }
141
-
142
- export function createPreviewQuery(documentIds: Set<string>): CreatePreviewQueryResult {
143
- // Create arrays of draft and published IDs
144
- const allIds = Array.from(documentIds).flatMap((id) => [getPublishedId(id), getDraftId(id)])
145
- const queryHash = hashString(PREVIEW_PROJECTION)
146
-
147
- return {
148
- query: `*[_id in $__ids_${queryHash}]${PREVIEW_PROJECTION}`,
149
- params: {
150
- [`__ids_${queryHash}`]: allIds,
151
- },
152
- }
153
- }
@@ -1,36 +0,0 @@
1
- import {Observable} from 'rxjs'
2
- import {describe, it, vi} from 'vitest'
3
-
4
- import {createSanityInstance} from '../store/createSanityInstance'
5
- import {createStoreInstance} from '../store/createStoreInstance'
6
- import {previewStore} from './previewStore'
7
- import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
8
-
9
- vi.mock('./subscribeToStateAndFetchBatches')
10
-
11
- describe('previewStore', () => {
12
- it('is a resource that initializes with state and subscriptions', async () => {
13
- const teardown = vi.fn()
14
- const subscriber = vi.fn().mockReturnValue(teardown)
15
- vi.mocked(subscribeToStateAndFetchBatches).mockReturnValue(
16
- new Observable(subscriber).subscribe(),
17
- )
18
-
19
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
20
-
21
- const {state, dispose} = createStoreInstance(
22
- instance,
23
- {name: 'p.d', projectId: 'p', dataset: 'd'},
24
- previewStore,
25
- )
26
-
27
- expect(subscribeToStateAndFetchBatches).toHaveBeenCalledWith({
28
- instance,
29
- state,
30
- key: {name: 'p.d', projectId: 'p', dataset: 'd'},
31
- })
32
-
33
- dispose()
34
- instance.dispose()
35
- })
36
- })
@@ -1,47 +0,0 @@
1
- import {of} from 'rxjs'
2
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
3
-
4
- import {createDocumentHandle} from '../config/handles'
5
- import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
6
- import {type StateSource} from '../store/createStateSourceAction'
7
- import {getPreviewState} from './getPreviewState'
8
- import {type PreviewValue, type ValuePending} from './previewStore'
9
- import {resolvePreview} from './resolvePreview'
10
-
11
- vi.mock('./getPreviewState')
12
-
13
- describe('resolvePreview', () => {
14
- let instance: SanityInstance
15
-
16
- beforeEach(() => {
17
- vi.resetAllMocks()
18
- // Create a mock that returns the correct ValuePending type
19
- vi.mocked(getPreviewState).mockReturnValue({
20
- observable: of({
21
- data: {title: 'test'},
22
- isPending: false,
23
- } as ValuePending<PreviewValue>),
24
- } as StateSource<ValuePending<PreviewValue>>)
25
-
26
- instance = createSanityInstance({projectId: 'p', dataset: 'd'})
27
- })
28
-
29
- afterEach(() => {
30
- instance.dispose()
31
- })
32
-
33
- it('resolves a preview and returns the first emitted value with results', async () => {
34
- const docHandle = createDocumentHandle({
35
- documentId: 'doc123',
36
- documentType: 'movie',
37
- })
38
-
39
- const result = await resolvePreview(instance, docHandle)
40
-
41
- expect(getPreviewState).toHaveBeenCalledWith(instance, docHandle)
42
- expect(result).toEqual({
43
- data: {title: 'test'},
44
- isPending: false,
45
- })
46
- })
47
- })