@sanity/sdk 2.7.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.
- package/dist/index.d.ts +228 -239
- package/dist/index.js +287 -454
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/_exports/index.ts +16 -17
- package/src/agent/agentActions.test.ts +60 -16
- package/src/agent/agentActions.ts +29 -20
- package/src/auth/authMode.test.ts +0 -25
- package/src/auth/authMode.ts +3 -6
- package/src/auth/authStore.test.ts +129 -66
- package/src/auth/authStore.ts +9 -11
- package/src/auth/dashboardAuth.ts +2 -2
- package/src/auth/getOrganizationVerificationState.test.ts +10 -11
- package/src/auth/handleAuthCallback.test.ts +0 -12
- package/src/auth/handleAuthCallback.ts +9 -3
- package/src/auth/logout.test.ts +0 -6
- package/src/auth/refreshStampedToken.test.ts +121 -17
- package/src/auth/standaloneAuth.ts +9 -3
- package/src/auth/studioAuth.ts +35 -8
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +9 -3
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -1
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +0 -2
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
- package/src/auth/utils.ts +33 -0
- package/src/client/clientStore.test.ts +14 -61
- package/src/client/clientStore.ts +52 -28
- package/src/comlink/controller/actions/destroyController.test.ts +1 -4
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +1 -4
- package/src/comlink/controller/actions/getOrCreateController.test.ts +1 -4
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +1 -4
- package/src/comlink/node/actions/getOrCreateNode.test.ts +1 -4
- package/src/comlink/node/actions/releaseNode.test.ts +1 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/config/__tests__/handles.test.ts +12 -18
- package/src/config/handles.ts +7 -25
- package/src/config/sanityConfig.ts +99 -52
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +4 -10
- package/src/document/actions.test.ts +33 -4
- package/src/document/actions.ts +3 -10
- package/src/document/applyDocumentActions.test.ts +17 -18
- package/src/document/applyDocumentActions.ts +9 -12
- package/src/document/documentStore.test.ts +303 -133
- package/src/document/documentStore.ts +70 -61
- package/src/document/permissions.test.ts +44 -8
- package/src/document/processActions.test.ts +77 -7
- package/src/document/reducers.test.ts +35 -3
- package/src/document/sharedListener.test.ts +13 -13
- package/src/document/sharedListener.ts +8 -3
- package/src/favorites/favorites.test.ts +10 -2
- package/src/presence/presenceStore.test.ts +34 -9
- package/src/presence/presenceStore.ts +29 -13
- package/src/preview/previewProjectionUtils.test.ts +192 -0
- package/src/preview/previewProjectionUtils.ts +88 -0
- package/src/preview/{previewStore.ts → types.ts} +6 -25
- package/src/project/project.test.ts +1 -1
- package/src/project/project.ts +14 -20
- package/src/projection/getProjectionState.test.ts +4 -2
- package/src/projection/getProjectionState.ts +2 -21
- package/src/projection/projectionQuery.ts +2 -3
- package/src/projection/projectionStore.test.ts +3 -3
- package/src/projection/resolveProjection.test.ts +2 -1
- package/src/projection/resolveProjection.ts +2 -18
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +2 -2
- package/src/projection/subscribeToStateAndFetchBatches.ts +23 -36
- package/src/projection/types.ts +1 -9
- package/src/projects/projects.test.ts +1 -1
- package/src/query/queryStore.test.ts +86 -28
- package/src/query/queryStore.ts +23 -38
- package/src/releases/getPerspectiveState.test.ts +14 -13
- package/src/releases/getPerspectiveState.ts +6 -6
- package/src/releases/releasesStore.test.ts +21 -6
- package/src/releases/releasesStore.ts +18 -8
- package/src/store/createActionBinder.test.ts +114 -111
- package/src/store/createActionBinder.ts +52 -101
- package/src/store/createSanityInstance.test.ts +13 -83
- package/src/store/createSanityInstance.ts +2 -78
- package/src/store/createStateSourceAction.test.ts +2 -2
- package/src/store/createStateSourceAction.ts +5 -5
- package/src/store/createStoreInstance.test.ts +2 -4
- package/src/users/reducers.test.ts +1 -6
- package/src/users/reducers.ts +2 -2
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +12 -15
- package/src/utils/createFetcherStore.test.ts +1 -1
- package/src/utils/logger.test.ts +0 -12
- package/src/utils/logger.ts +3 -8
- package/src/preview/getPreviewState.test.ts +0 -120
- package/src/preview/getPreviewState.ts +0 -91
- 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/resolvePreview.test.ts +0 -47
- package/src/preview/resolvePreview.ts +0 -20
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
- package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
- 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
|
-
})
|