@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
@@ -127,8 +127,9 @@ describe('createFetcherStore', () => {
127
127
  expect(fetchSpy).toHaveBeenCalledTimes(1)
128
128
 
129
129
  // Second subscription within throttle interval
130
- const sub2 = stateSource.subscribe()
131
- await firstValueFrom(stateSource.observable)
130
+ const stateSource2 = store.getState(instance, 1)
131
+ const sub2 = stateSource2.subscribe()
132
+ await firstValueFrom(stateSource2.observable)
132
133
  expect(fetchSpy).toHaveBeenCalledTimes(1)
133
134
 
134
135
  // Advance past throttle interval
@@ -136,8 +137,9 @@ describe('createFetcherStore', () => {
136
137
  await vi.advanceTimersByTimeAsync(1000)
137
138
 
138
139
  // Third subscription after throttle interval
139
- const sub3 = stateSource.subscribe()
140
- await firstValueFrom(stateSource.observable)
140
+ const stateSource3 = store.getState(instance, 1)
141
+ const sub3 = stateSource3.subscribe()
142
+ await firstValueFrom(stateSource3.observable)
141
143
  expect(fetchSpy).toHaveBeenCalledTimes(2)
142
144
 
143
145
  sub1()
@@ -23,6 +23,7 @@ import {
23
23
  } from '../store/createStateSourceAction'
24
24
  import {defineStore, type StoreContext} from '../store/defineStore'
25
25
  import {insecureRandomId} from '../utils/ids'
26
+ import {setCleanupTimeout} from './setCleanupTimeout'
26
27
 
27
28
  interface CreateFetcherStoreOptions<TParams extends unknown[], TData> {
28
29
  /**
@@ -247,7 +248,7 @@ export function createFetcherStore<TParams extends unknown[], TData>({
247
248
  }))
248
249
 
249
250
  return () => {
250
- setTimeout(() => {
251
+ setCleanupTimeout(() => {
251
252
  state.set('removeSubscription', (prev: FetcherStoreState<TParams, TData>) => {
252
253
  const entry = prev.stateByParams[key]
253
254
  if (!entry) return prev
@@ -0,0 +1,21 @@
1
+ import {describe, expect, it, vi} from 'vitest'
2
+
3
+ import {getStagingApiHost} from './getStagingApiHost'
4
+
5
+ describe('getStagingApiHost', () => {
6
+ it('returns staging host when __SANITY_STAGING__ is true', () => {
7
+ vi.stubGlobal('__SANITY_STAGING__', true)
8
+ expect(getStagingApiHost()).toBe('https://api.sanity.work')
9
+ vi.unstubAllGlobals()
10
+ })
11
+
12
+ it('returns undefined when __SANITY_STAGING__ is false', () => {
13
+ vi.stubGlobal('__SANITY_STAGING__', false)
14
+ expect(getStagingApiHost()).toBeUndefined()
15
+ vi.unstubAllGlobals()
16
+ })
17
+
18
+ it('returns undefined when __SANITY_STAGING__ is not defined', () => {
19
+ expect(getStagingApiHost()).toBeUndefined()
20
+ })
21
+ })
@@ -0,0 +1,14 @@
1
+ declare const __SANITY_STAGING__: boolean | undefined
2
+
3
+ /**
4
+ * Returns the staging API host if the `__SANITY_STAGING__` build-time flag is
5
+ * set to `true` (mirroring how Sanity Studio detects staging builds).
6
+ *
7
+ * @internal
8
+ */
9
+ export function getStagingApiHost(): string | undefined {
10
+ if (typeof __SANITY_STAGING__ !== 'undefined' && __SANITY_STAGING__ === true) {
11
+ return 'https://api.sanity.work'
12
+ }
13
+ return undefined
14
+ }
@@ -1,34 +1,6 @@
1
1
  import {describe, expect, it} from 'vitest'
2
2
 
3
- import {getDraftId, getPublishedId, insecureRandomId} from './ids'
4
-
5
- describe('getDraftId', () => {
6
- it('should add drafts prefix to non-draft ids', () => {
7
- expect(getDraftId('abc123')).toBe('drafts.abc123')
8
- })
9
-
10
- it('should not modify ids that already have drafts prefix', () => {
11
- expect(getDraftId('drafts.abc123')).toBe('drafts.abc123')
12
- })
13
-
14
- it('should handle empty string', () => {
15
- expect(getDraftId('')).toBe('drafts.')
16
- })
17
- })
18
-
19
- describe('getPublishedId', () => {
20
- it('should remove drafts prefix from draft ids', () => {
21
- expect(getPublishedId('drafts.abc123')).toBe('abc123')
22
- })
23
-
24
- it('should not modify ids that dont have drafts prefix', () => {
25
- expect(getPublishedId('abc123')).toBe('abc123')
26
- })
27
-
28
- it('should handle empty string', () => {
29
- expect(getPublishedId('')).toBe('')
30
- })
31
- })
3
+ import {insecureRandomId} from './ids'
32
4
 
33
5
  describe('insecureRandomId', () => {
34
6
  it('should generate 16-character string', () => {
package/src/utils/ids.ts CHANGED
@@ -1,13 +1,3 @@
1
- export function getPublishedId(id: string): string {
2
- const draftsPrefix = 'drafts.'
3
- return id.startsWith(draftsPrefix) ? id.slice(draftsPrefix.length) : id
4
- }
5
-
6
- export function getDraftId(id: string): string {
7
- const draftsPrefix = 'drafts.'
8
- return id.startsWith(draftsPrefix) ? id : `${draftsPrefix}${id}`
9
- }
10
-
11
1
  export function insecureRandomId(): string {
12
2
  return Array.from({length: 16}, () => Math.floor(Math.random() * 16).toString(16)).join('')
13
3
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Like `setTimeout`, but calls `.unref()` on the timer when running in Node.js.
3
+ *
4
+ * In Node.js, active timers prevent the process from exiting. Cleanup timers
5
+ * (e.g. deferred subscription removal) should not keep the process alive -
6
+ * `.unref()` lets the process exit naturally while still firing the timer if
7
+ * the process happens to still be running.
8
+ *
9
+ * In browsers, `setTimeout` returns a number and has no `.unref()` method,
10
+ * so this is a no-op there.
11
+ */
12
+ export function setCleanupTimeout(fn: () => void, delay: number): ReturnType<typeof setTimeout> {
13
+ const timer = setTimeout(fn, delay)
14
+
15
+ // In Node.js, setTimeout returns an object with an `unref()` method.
16
+ // In browsers, it returns a number. We assign to `unknown` and narrow
17
+ // at runtime so this works in both environments without type assertions.
18
+ const t: unknown = timer
19
+ if (typeof t === 'object' && t !== null && 'unref' in t && typeof t.unref === 'function') {
20
+ t.unref()
21
+ }
22
+
23
+ return timer
24
+ }
@@ -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
- })