@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.
- package/dist/_chunks-dts/utils.d.ts +2396 -0
- package/dist/_chunks-es/_internal.js +129 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1460 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +87 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_chunks-es/version.js +7 -0
- package/dist/_chunks-es/version.js.map +1 -0
- package/dist/_exports/_internal.d.ts +64 -0
- package/dist/_exports/_internal.js +20 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +2 -2343
- package/dist/index.js +383 -1777
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
- package/src/_exports/_internal.ts +14 -0
- package/src/_exports/index.ts +10 -1
- package/src/auth/authStore.test.ts +150 -1
- package/src/auth/authStore.ts +11 -11
- package/src/auth/dashboardAuth.ts +2 -2
- package/src/auth/handleAuthCallback.ts +9 -3
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -1
- package/src/auth/refreshStampedToken.test.ts +118 -1
- package/src/auth/refreshStampedToken.ts +3 -2
- package/src/auth/standaloneAuth.ts +9 -3
- package/src/auth/studioAuth.ts +34 -7
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +10 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +5 -1
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
- package/src/auth/utils.ts +33 -0
- package/src/client/clientStore.test.ts +14 -0
- package/src/client/clientStore.ts +2 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/sanityConfig.ts +6 -0
- package/src/document/actions.ts +18 -11
- package/src/document/applyDocumentActions.test.ts +7 -6
- package/src/document/applyDocumentActions.ts +10 -4
- package/src/document/documentStore.test.ts +536 -188
- package/src/document/documentStore.ts +142 -76
- package/src/document/events.ts +7 -2
- package/src/document/permissions.test.ts +18 -16
- package/src/document/permissions.ts +35 -11
- package/src/document/processActions.test.ts +359 -32
- package/src/document/processActions.ts +104 -76
- package/src/document/reducers.test.ts +117 -29
- package/src/document/reducers.ts +43 -36
- package/src/document/sharedListener.ts +16 -6
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +9 -2
- package/src/presence/bifurTransport.ts +6 -1
- package/src/preview/getPreviewState.test.ts +115 -98
- package/src/preview/getPreviewState.ts +38 -60
- package/src/preview/previewProjectionUtils.test.ts +179 -0
- package/src/preview/previewProjectionUtils.ts +93 -0
- package/src/preview/resolvePreview.test.ts +42 -25
- package/src/preview/resolvePreview.ts +29 -10
- package/src/preview/{previewStore.ts → types.ts} +8 -17
- package/src/projection/getProjectionState.test.ts +16 -16
- package/src/projection/getProjectionState.ts +2 -1
- package/src/projection/projectionQuery.ts +2 -3
- package/src/projection/types.ts +1 -1
- package/src/query/queryStore.ts +2 -1
- package/src/releases/getPerspectiveState.ts +7 -6
- package/src/releases/releasesStore.test.ts +20 -5
- package/src/releases/releasesStore.ts +20 -8
- package/src/store/createStateSourceAction.test.ts +62 -0
- package/src/store/createStateSourceAction.ts +34 -39
- package/src/telemetry/__telemetry__/sdk.telemetry.ts +42 -0
- package/src/telemetry/devMode.test.ts +52 -0
- package/src/telemetry/devMode.ts +40 -0
- package/src/telemetry/initTelemetry.test.ts +225 -0
- package/src/telemetry/initTelemetry.ts +205 -0
- package/src/telemetry/telemetryManager.test.ts +263 -0
- package/src/telemetry/telemetryManager.ts +187 -0
- package/src/users/usersStore.test.ts +1 -0
- package/src/users/usersStore.ts +5 -1
- package/src/utils/createFetcherStore.test.ts +6 -4
- package/src/utils/createFetcherStore.ts +2 -1
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/setCleanupTimeout.ts +24 -0
- package/src/preview/previewQuery.test.ts +0 -236
- package/src/preview/previewQuery.ts +0 -153
- package/src/preview/previewStore.test.ts +0 -36
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
- package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
- 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
|
|
131
|
-
|
|
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
|
|
140
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/utils/ids.test.ts
CHANGED
|
@@ -1,34 +1,6 @@
|
|
|
1
1
|
import {describe, expect, it} from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
})
|