@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
|
@@ -69,7 +69,7 @@ describe('presenceStore', () => {
|
|
|
69
69
|
mockGetUserState = vi.fn(() => of(mockUser))
|
|
70
70
|
vi.mocked(getUserState).mockImplementation(mockGetUserState)
|
|
71
71
|
|
|
72
|
-
instance = createSanityInstance(
|
|
72
|
+
instance = createSanityInstance()
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
afterEach(() => {
|
|
@@ -77,8 +77,9 @@ describe('presenceStore', () => {
|
|
|
77
77
|
})
|
|
78
78
|
|
|
79
79
|
describe('getPresence', () => {
|
|
80
|
+
const key = {resource: {projectId: 'test-project', dataset: 'test-dataset'}}
|
|
80
81
|
it('creates bifur transport with correct parameters', () => {
|
|
81
|
-
getPresence(instance)
|
|
82
|
+
getPresence(instance, key)
|
|
82
83
|
|
|
83
84
|
expect(createBifurTransport).toHaveBeenCalledWith({
|
|
84
85
|
client: mockClient,
|
|
@@ -88,18 +89,18 @@ describe('presenceStore', () => {
|
|
|
88
89
|
})
|
|
89
90
|
|
|
90
91
|
it('sends rollCall message on initialization', () => {
|
|
91
|
-
getPresence(instance)
|
|
92
|
+
getPresence(instance, key)
|
|
92
93
|
|
|
93
94
|
expect(mockDispatchMessage).toHaveBeenCalledWith({type: 'rollCall'})
|
|
94
95
|
})
|
|
95
96
|
|
|
96
97
|
it('returns empty array when no users present', () => {
|
|
97
|
-
const source = getPresence(instance)
|
|
98
|
+
const source = getPresence(instance, key)
|
|
98
99
|
expect(source.getCurrent()).toEqual([])
|
|
99
100
|
})
|
|
100
101
|
|
|
101
102
|
it('handles state events from other users', async () => {
|
|
102
|
-
const source = getPresence(instance)
|
|
103
|
+
const source = getPresence(instance, key)
|
|
103
104
|
|
|
104
105
|
// Subscribe to initialize the store
|
|
105
106
|
const unsubscribe = source.subscribe(() => {})
|
|
@@ -136,7 +137,7 @@ describe('presenceStore', () => {
|
|
|
136
137
|
})
|
|
137
138
|
|
|
138
139
|
it('ignores events from own session', async () => {
|
|
139
|
-
const source = getPresence(instance)
|
|
140
|
+
const source = getPresence(instance, key)
|
|
140
141
|
const unsubscribe = source.subscribe(() => {})
|
|
141
142
|
|
|
142
143
|
await firstValueFrom(of(null).pipe(delay(10)))
|
|
@@ -158,7 +159,7 @@ describe('presenceStore', () => {
|
|
|
158
159
|
})
|
|
159
160
|
|
|
160
161
|
it('handles disconnect events', async () => {
|
|
161
|
-
const source = getPresence(instance)
|
|
162
|
+
const source = getPresence(instance, key)
|
|
162
163
|
const unsubscribe = source.subscribe(() => {})
|
|
163
164
|
|
|
164
165
|
await firstValueFrom(of(null).pipe(delay(10)))
|
|
@@ -190,7 +191,7 @@ describe('presenceStore', () => {
|
|
|
190
191
|
})
|
|
191
192
|
|
|
192
193
|
it('fetches user data for present users', async () => {
|
|
193
|
-
const source = getPresence(instance)
|
|
194
|
+
const source = getPresence(instance, key)
|
|
194
195
|
const unsubscribe = source.subscribe(() => {})
|
|
195
196
|
|
|
196
197
|
await firstValueFrom(of(null).pipe(delay(10)))
|
|
@@ -222,7 +223,7 @@ describe('presenceStore', () => {
|
|
|
222
223
|
})
|
|
223
224
|
|
|
224
225
|
it('handles presence events correctly', async () => {
|
|
225
|
-
const source = getPresence(instance)
|
|
226
|
+
const source = getPresence(instance, key)
|
|
226
227
|
const unsubscribe = source.subscribe(() => {})
|
|
227
228
|
|
|
228
229
|
await firstValueFrom(of(null).pipe(delay(10)))
|
|
@@ -243,5 +244,29 @@ describe('presenceStore', () => {
|
|
|
243
244
|
|
|
244
245
|
unsubscribe()
|
|
245
246
|
})
|
|
247
|
+
|
|
248
|
+
it('should throw an error when initialized with a media library resource', () => {
|
|
249
|
+
const mediaLibraryResource = {mediaLibraryId: 'ml123'}
|
|
250
|
+
|
|
251
|
+
expect(() => {
|
|
252
|
+
getPresence(instance, {resource: mediaLibraryResource})
|
|
253
|
+
}).toThrow('Presence is not supported for media library resources')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should throw an error when initialized with a canvas resource', () => {
|
|
257
|
+
const canvasResource = {canvasId: 'canvas123'}
|
|
258
|
+
|
|
259
|
+
expect(() => {
|
|
260
|
+
getPresence(instance, {resource: canvasResource})
|
|
261
|
+
}).toThrow('Presence is not supported for canvas resources')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('should work with a dataset resource', () => {
|
|
265
|
+
const datasetResource = {projectId: 'test-project', dataset: 'test-dataset'}
|
|
266
|
+
|
|
267
|
+
expect(() => {
|
|
268
|
+
getPresence(instance, {resource: datasetResource})
|
|
269
|
+
}).not.toThrow()
|
|
270
|
+
})
|
|
246
271
|
})
|
|
247
272
|
})
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {type SanityClient} from '@sanity/client'
|
|
2
1
|
import {createSelector} from 'reselect'
|
|
3
2
|
import {combineLatest, distinctUntilChanged, filter, map, of, Subscription, switchMap} from 'rxjs'
|
|
4
3
|
|
|
5
4
|
import {getTokenState} from '../auth/authStore'
|
|
6
5
|
import {getClient} from '../client/clientStore'
|
|
7
|
-
import {
|
|
6
|
+
import {isCanvasResource, isDatasetResource, isMediaLibraryResource} from '../config/sanityConfig'
|
|
7
|
+
import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
|
|
8
8
|
import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
|
|
9
9
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
10
10
|
import {type SanityUser} from '../users/types'
|
|
@@ -23,27 +23,40 @@ const getInitialState = (): PresenceStoreState => ({
|
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
/** @public */
|
|
26
|
-
export const presenceStore = defineStore<PresenceStoreState,
|
|
26
|
+
export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
|
|
27
27
|
name: 'presence',
|
|
28
28
|
getInitialState,
|
|
29
|
-
initialize: (context: StoreContext<PresenceStoreState,
|
|
29
|
+
initialize: (context: StoreContext<PresenceStoreState, BoundResourceKey>) => {
|
|
30
30
|
const {
|
|
31
31
|
instance,
|
|
32
32
|
state,
|
|
33
|
-
key: {
|
|
33
|
+
key: {resource},
|
|
34
34
|
} = context
|
|
35
|
+
|
|
36
|
+
// Presence is only supported for dataset resources
|
|
37
|
+
if (isMediaLibraryResource(resource)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'Presence is not supported for media library resources. Presence tracking requires a dataset resource with a projectId.',
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isCanvasResource(resource)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'Presence is not supported for canvas resources. Presence tracking requires a dataset resource with a projectId.',
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
const sessionId = crypto.randomUUID()
|
|
36
50
|
|
|
37
51
|
const client = getClient(instance, {
|
|
38
52
|
apiVersion: '2022-06-30',
|
|
39
|
-
|
|
40
|
-
dataset,
|
|
53
|
+
resource,
|
|
41
54
|
})
|
|
42
55
|
|
|
43
56
|
const token$ = getTokenState(instance).observable.pipe(distinctUntilChanged())
|
|
44
57
|
|
|
45
58
|
const [incomingEvents$, dispatch] = createBifurTransport({
|
|
46
|
-
client
|
|
59
|
+
client,
|
|
47
60
|
token$,
|
|
48
61
|
sessionId,
|
|
49
62
|
})
|
|
@@ -114,13 +127,13 @@ const selectPresence = createSelector(
|
|
|
114
127
|
},
|
|
115
128
|
)
|
|
116
129
|
|
|
117
|
-
/** @
|
|
118
|
-
export const getPresence =
|
|
130
|
+
/** @beta */
|
|
131
|
+
export const getPresence = bindActionByResource(
|
|
119
132
|
presenceStore,
|
|
120
133
|
createStateSourceAction({
|
|
121
|
-
selector: (context: SelectorContext<PresenceStoreState
|
|
134
|
+
selector: (context: SelectorContext<PresenceStoreState>): UserPresence[] =>
|
|
122
135
|
selectPresence(context.state),
|
|
123
|
-
onSubscribe: (context: StoreContext<PresenceStoreState,
|
|
136
|
+
onSubscribe: (context: StoreContext<PresenceStoreState, BoundResourceKey>) => {
|
|
124
137
|
const userIds$ = context.state.observable.pipe(
|
|
125
138
|
map((state) =>
|
|
126
139
|
Array.from(state.locations.values())
|
|
@@ -140,7 +153,10 @@ export const getPresence = bindActionByDataset(
|
|
|
140
153
|
getUserState(context.instance, {
|
|
141
154
|
userId,
|
|
142
155
|
resourceType: 'project',
|
|
143
|
-
projectId:
|
|
156
|
+
projectId:
|
|
157
|
+
context.key.resource && isDatasetResource(context.key.resource)
|
|
158
|
+
? context.key.resource.projectId
|
|
159
|
+
: undefined,
|
|
144
160
|
}).pipe(filter((v): v is NonNullable<typeof v> => !!v)),
|
|
145
161
|
)
|
|
146
162
|
return combineLatest(userObservables)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {createSanityInstance} from '../store/createSanityInstance'
|
|
5
|
+
import {normalizeMedia, transformProjectionToPreview} from './previewProjectionUtils'
|
|
6
|
+
import {type PreviewQueryResult} from './types'
|
|
7
|
+
|
|
8
|
+
// Mock the getClient function
|
|
9
|
+
vi.mock('../client/clientStore', () => ({
|
|
10
|
+
getClient: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
const mockClient = {
|
|
14
|
+
config: () => ({projectId: 'test-project', dataset: 'test-dataset'}),
|
|
15
|
+
} as unknown as SanityClient
|
|
16
|
+
|
|
17
|
+
describe('normalizeMedia', () => {
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
vi.clearAllMocks()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns null if media is null or undefined', () => {
|
|
23
|
+
expect(normalizeMedia(null, mockClient)).toBeNull()
|
|
24
|
+
expect(normalizeMedia(undefined, mockClient)).toBeNull()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('returns null if media does not have a valid asset', () => {
|
|
28
|
+
const invalidMedia1 = {media: {_ref: 'image-abc123-200x200-png'}} // Missing `asset` property
|
|
29
|
+
const invalidMedia2 = {asset: {ref: 'image-abc123-200x200-png'}} // Incorrect property name `ref`
|
|
30
|
+
expect(normalizeMedia(invalidMedia1, mockClient)).toBeNull()
|
|
31
|
+
expect(normalizeMedia(invalidMedia2, mockClient)).toBeNull()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('returns null if media is not an object', () => {
|
|
35
|
+
expect(normalizeMedia(123, mockClient)).toBeNull()
|
|
36
|
+
expect(normalizeMedia('invalid', mockClient)).toBeNull()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns a normalized URL for valid image asset objects', () => {
|
|
40
|
+
const validMedia = {type: 'image-asset', _ref: 'image-abc123-200x200-png'}
|
|
41
|
+
const result = normalizeMedia(validMedia, mockClient)
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
type: 'image-asset',
|
|
44
|
+
_ref: 'image-abc123-200x200-png',
|
|
45
|
+
url: 'https://cdn.sanity.io/images/test-project/test-dataset/abc123-200x200.png',
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('handles image assets with expected URL format', () => {
|
|
50
|
+
const media = {type: 'image-asset', _ref: 'image-xyz456-400x400-jpg'}
|
|
51
|
+
const result = normalizeMedia(media, mockClient)
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
type: 'image-asset',
|
|
54
|
+
_ref: 'image-xyz456-400x400-jpg',
|
|
55
|
+
url: 'https://cdn.sanity.io/images/test-project/test-dataset/xyz456-400x400.jpg',
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('transformProjectionToPreview', () => {
|
|
61
|
+
const instance = createSanityInstance()
|
|
62
|
+
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
vi.clearAllMocks()
|
|
65
|
+
|
|
66
|
+
// Mock getClient to return our mock client
|
|
67
|
+
const {getClient} = await import('../client/clientStore')
|
|
68
|
+
vi.mocked(getClient).mockReturnValue(mockClient)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('transforms projection result with title and subtitle', () => {
|
|
72
|
+
const projectionResult: PreviewQueryResult = {
|
|
73
|
+
_id: 'doc1',
|
|
74
|
+
_type: 'article',
|
|
75
|
+
_updatedAt: '2026-01-01',
|
|
76
|
+
titleCandidates: {title: 'My Title'},
|
|
77
|
+
subtitleCandidates: {description: 'My Description'},
|
|
78
|
+
media: null,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = transformProjectionToPreview(
|
|
82
|
+
instance,
|
|
83
|
+
{projectId: 'p', dataset: 'd'},
|
|
84
|
+
projectionResult,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
expect(result).toEqual({
|
|
88
|
+
title: 'My Title',
|
|
89
|
+
subtitle: 'My Description',
|
|
90
|
+
media: null,
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('uses fallback title when no title candidates exist', () => {
|
|
95
|
+
const projectionResult: PreviewQueryResult = {
|
|
96
|
+
_id: 'doc1',
|
|
97
|
+
_type: 'article',
|
|
98
|
+
_updatedAt: '2026-01-01',
|
|
99
|
+
titleCandidates: {},
|
|
100
|
+
subtitleCandidates: {},
|
|
101
|
+
media: null,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = transformProjectionToPreview(
|
|
105
|
+
instance,
|
|
106
|
+
{projectId: 'p', dataset: 'd'},
|
|
107
|
+
projectionResult,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
expect(result.title).toBe('article: doc1')
|
|
111
|
+
expect(result.subtitle).toBeUndefined()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('transforms projection result with media', () => {
|
|
115
|
+
const projectionResult: PreviewQueryResult = {
|
|
116
|
+
_id: 'doc1',
|
|
117
|
+
_type: 'article',
|
|
118
|
+
_updatedAt: '2026-01-01',
|
|
119
|
+
titleCandidates: {title: 'My Title'},
|
|
120
|
+
subtitleCandidates: {},
|
|
121
|
+
media: {type: 'image-asset', _ref: 'image-abc123-200x200-png', url: ''},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const result = transformProjectionToPreview(
|
|
125
|
+
instance,
|
|
126
|
+
{projectId: 'p', dataset: 'd'},
|
|
127
|
+
projectionResult,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
expect(result).toEqual({
|
|
131
|
+
title: 'My Title',
|
|
132
|
+
subtitle: undefined,
|
|
133
|
+
media: {
|
|
134
|
+
type: 'image-asset',
|
|
135
|
+
_ref: 'image-abc123-200x200-png',
|
|
136
|
+
url: 'https://cdn.sanity.io/images/test-project/test-dataset/abc123-200x200.png',
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('includes status when provided', () => {
|
|
142
|
+
const projectionResult: PreviewQueryResult = {
|
|
143
|
+
_id: 'doc1',
|
|
144
|
+
_type: 'article',
|
|
145
|
+
_updatedAt: '2026-01-01',
|
|
146
|
+
titleCandidates: {title: 'My Title'},
|
|
147
|
+
subtitleCandidates: {},
|
|
148
|
+
media: null,
|
|
149
|
+
_status: {
|
|
150
|
+
lastEditedPublishedAt: '2026-01-01',
|
|
151
|
+
lastEditedDraftAt: '2026-01-02',
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const result = transformProjectionToPreview(
|
|
156
|
+
instance,
|
|
157
|
+
{projectId: 'p', dataset: 'd'},
|
|
158
|
+
projectionResult,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
expect(result).toEqual({
|
|
162
|
+
title: 'My Title',
|
|
163
|
+
subtitle: undefined,
|
|
164
|
+
media: null,
|
|
165
|
+
_status: {
|
|
166
|
+
lastEditedPublishedAt: '2026-01-01',
|
|
167
|
+
lastEditedDraftAt: '2026-01-02',
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('calls getClient with the provided resource', async () => {
|
|
173
|
+
const projectionResult: PreviewQueryResult = {
|
|
174
|
+
_id: 'doc1',
|
|
175
|
+
_type: 'article',
|
|
176
|
+
_updatedAt: '2026-01-01',
|
|
177
|
+
titleCandidates: {title: 'My Title'},
|
|
178
|
+
subtitleCandidates: {},
|
|
179
|
+
media: null,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const resource = {mediaLibraryId: 'test-library'}
|
|
183
|
+
|
|
184
|
+
transformProjectionToPreview(instance, resource, projectionResult)
|
|
185
|
+
|
|
186
|
+
const {getClient} = await import('../client/clientStore')
|
|
187
|
+
expect(getClient).toHaveBeenCalledWith(instance, {
|
|
188
|
+
apiVersion: 'v2025-05-06',
|
|
189
|
+
resource,
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {createImageUrlBuilder} from '@sanity/image-url'
|
|
3
|
+
import {isObject} from 'lodash-es'
|
|
4
|
+
|
|
5
|
+
import {getClient} from '../client/clientStore'
|
|
6
|
+
import {type DocumentResource} from '../config/sanityConfig'
|
|
7
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
8
|
+
import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
|
|
9
|
+
import {type PreviewQueryResult, type PreviewValue} from './types'
|
|
10
|
+
|
|
11
|
+
const API_VERSION = 'v2025-05-06'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Checks if the provided value has `_ref` property that is a string and starts with `image-`
|
|
15
|
+
*/
|
|
16
|
+
function hasImageRef<T>(value: unknown): value is T & {_ref: string} {
|
|
17
|
+
return isObject(value) && '_ref' in value && typeof (value as {_ref: unknown})._ref === 'string'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalizes a media asset to a preview value.
|
|
22
|
+
* Adds a url to a media asset reference using `@sanity/image-url`.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeMedia(media: unknown, client: SanityClient): PreviewValue['media'] {
|
|
27
|
+
if (!media) return null
|
|
28
|
+
if (!hasImageRef(media)) return null
|
|
29
|
+
|
|
30
|
+
const builder = createImageUrlBuilder(client)
|
|
31
|
+
const url = builder.image({_ref: media._ref}).url()
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
type: 'image-asset',
|
|
35
|
+
_ref: media._ref,
|
|
36
|
+
url,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Finds a single field value from a set of candidates based on a priority list of field names.
|
|
42
|
+
* Returns the first non-empty string value found from the candidates matching the priority list order.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
function findFirstDefined(
|
|
47
|
+
fieldsToSearch: string[],
|
|
48
|
+
candidates: Record<string, unknown>,
|
|
49
|
+
exclude?: unknown,
|
|
50
|
+
): string | undefined {
|
|
51
|
+
if (!candidates) return undefined
|
|
52
|
+
|
|
53
|
+
for (const field of fieldsToSearch) {
|
|
54
|
+
const value = candidates[field]
|
|
55
|
+
if (typeof value === 'string' && value.trim() !== '' && value !== exclude) {
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Transforms a projection result (with titleCandidates, subtitleCandidates, media)
|
|
65
|
+
* into a PreviewValue (with title, subtitle, media).
|
|
66
|
+
*
|
|
67
|
+
* @param projectionResult - The raw projection result from GROQ
|
|
68
|
+
* @param instance - The Sanity instance to use for client configuration
|
|
69
|
+
* @param resource - Data resource for the preview
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
export function transformProjectionToPreview(
|
|
73
|
+
instance: SanityInstance,
|
|
74
|
+
resource: DocumentResource,
|
|
75
|
+
projectionResult: PreviewQueryResult,
|
|
76
|
+
): PreviewValue {
|
|
77
|
+
const title = findFirstDefined(TITLE_CANDIDATES, projectionResult.titleCandidates)
|
|
78
|
+
const subtitle = findFirstDefined(SUBTITLE_CANDIDATES, projectionResult.subtitleCandidates, title)
|
|
79
|
+
|
|
80
|
+
const client = getClient(instance, {apiVersion: API_VERSION, resource})
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
title: String(title || `${projectionResult._type}: ${projectionResult._id}`),
|
|
84
|
+
subtitle: subtitle || undefined,
|
|
85
|
+
media: normalizeMedia(projectionResult.media, client),
|
|
86
|
+
...(projectionResult._status && {_status: projectionResult._status}),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {type
|
|
2
|
-
import {defineStore} from '../store/defineStore'
|
|
3
|
-
import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
|
|
1
|
+
import {type DocumentStatus} from '../projection/types'
|
|
4
2
|
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
5
7
|
export interface PreviewQueryResult {
|
|
6
8
|
_id: string
|
|
7
9
|
_type: string
|
|
@@ -9,6 +11,7 @@ export interface PreviewQueryResult {
|
|
|
9
11
|
titleCandidates: Record<string, unknown>
|
|
10
12
|
subtitleCandidates: Record<string, unknown>
|
|
11
13
|
media?: PreviewMedia | null
|
|
14
|
+
_status?: DocumentStatus
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -71,25 +74,3 @@ export type ValuePending<T> = {
|
|
|
71
74
|
data: T | null
|
|
72
75
|
isPending: boolean
|
|
73
76
|
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* @public
|
|
77
|
-
*/
|
|
78
|
-
export interface PreviewStoreState {
|
|
79
|
-
values: {[TDocumentId in string]?: ValuePending<PreviewValue>}
|
|
80
|
-
subscriptions: {[TDocumentId in string]?: {[TSubscriptionId in string]?: true}}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export const previewStore = defineStore<PreviewStoreState, BoundDatasetKey>({
|
|
84
|
-
name: 'Preview',
|
|
85
|
-
getInitialState() {
|
|
86
|
-
return {
|
|
87
|
-
subscriptions: {},
|
|
88
|
-
values: {},
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
initialize: (context) => {
|
|
92
|
-
const subscription = subscribeToStateAndFetchBatches(context)
|
|
93
|
-
return () => subscription.unsubscribe
|
|
94
|
-
},
|
|
95
|
-
})
|
|
@@ -11,7 +11,7 @@ vi.mock('../client/clientStore')
|
|
|
11
11
|
|
|
12
12
|
describe('project', () => {
|
|
13
13
|
it('calls the `client.observable.projects.getById` method on the client and returns the result', async () => {
|
|
14
|
-
const instance = createSanityInstance(
|
|
14
|
+
const instance = createSanityInstance()
|
|
15
15
|
|
|
16
16
|
const project = {id: 'a'}
|
|
17
17
|
const getById = vi.fn().mockReturnValue(of(project))
|
package/src/project/project.ts
CHANGED
|
@@ -1,38 +1,32 @@
|
|
|
1
1
|
import {switchMap} from 'rxjs'
|
|
2
2
|
|
|
3
3
|
import {getClientState} from '../client/clientStore'
|
|
4
|
-
import {type ProjectHandle} from '../config/sanityConfig'
|
|
5
4
|
import {createFetcherStore} from '../utils/createFetcherStore'
|
|
6
5
|
|
|
7
6
|
const API_VERSION = 'v2025-02-19'
|
|
8
7
|
|
|
8
|
+
/** @public */
|
|
9
|
+
export type ProjectHandle = {
|
|
10
|
+
projectId: string
|
|
11
|
+
}
|
|
9
12
|
const project = createFetcherStore({
|
|
10
13
|
name: 'Project',
|
|
11
|
-
getKey: (
|
|
12
|
-
const projectId = options?.projectId
|
|
14
|
+
getKey: (_instance, options: ProjectHandle) => {
|
|
15
|
+
const projectId = options?.projectId
|
|
13
16
|
if (!projectId) {
|
|
14
17
|
throw new Error('A projectId is required to use the project API.')
|
|
15
18
|
}
|
|
16
19
|
return projectId
|
|
17
20
|
},
|
|
18
|
-
fetcher:
|
|
19
|
-
|
|
20
|
-
(options: ProjectHandle = {}) => {
|
|
21
|
-
const projectId = options.projectId ?? instance.config.projectId
|
|
21
|
+
fetcher: (instance) => (options: ProjectHandle) => {
|
|
22
|
+
const projectId = options.projectId
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
client.observable.projects.getById(
|
|
30
|
-
// non-null assertion is fine with the above throwing
|
|
31
|
-
(projectId ?? instance.config.projectId)!,
|
|
32
|
-
),
|
|
33
|
-
),
|
|
34
|
-
)
|
|
35
|
-
},
|
|
24
|
+
return getClientState(instance, {
|
|
25
|
+
apiVersion: API_VERSION,
|
|
26
|
+
scope: 'global',
|
|
27
|
+
projectId,
|
|
28
|
+
}).observable.pipe(switchMap((client) => client.observable.projects.getById(projectId!)))
|
|
29
|
+
},
|
|
36
30
|
})
|
|
37
31
|
|
|
38
32
|
/** @public */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {NEVER} from 'rxjs'
|
|
2
2
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
3
|
|
|
4
|
+
import {type DocumentResource} from '../config/sanityConfig'
|
|
4
5
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
5
6
|
import {type StoreState} from '../store/createStoreState'
|
|
6
7
|
import {hashString} from '../utils/hashString'
|
|
@@ -30,7 +31,8 @@ vi.mock('./subscribeToStateAndFetchBatches.ts')
|
|
|
30
31
|
|
|
31
32
|
describe('getProjectionState', () => {
|
|
32
33
|
let instance: SanityInstance
|
|
33
|
-
const
|
|
34
|
+
const resource: DocumentResource = {projectId: 'p', dataset: 'd'}
|
|
35
|
+
const docHandle = {documentId: 'exampleId', documentType: 'exampleType', resource}
|
|
34
36
|
const projection1 = '{exampleProjection1}'
|
|
35
37
|
const hash1 = hashString(projection1)
|
|
36
38
|
const projection2 = '{exampleProjection2}'
|
|
@@ -48,7 +50,7 @@ describe('getProjectionState', () => {
|
|
|
48
50
|
return NEVER.subscribe()
|
|
49
51
|
})
|
|
50
52
|
|
|
51
|
-
instance = createSanityInstance(
|
|
53
|
+
instance = createSanityInstance()
|
|
52
54
|
vi.useFakeTimers() // Enable fake timers for each test
|
|
53
55
|
})
|
|
54
56
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import {DocumentId, getPublishedId} from '@sanity/id-utils'
|
|
2
|
-
import {type SanityProjectionResult} from 'groq'
|
|
3
2
|
import {omit} from 'lodash-es'
|
|
4
3
|
|
|
5
4
|
import {type DocumentHandle} from '../config/sanityConfig'
|
|
6
|
-
import {
|
|
5
|
+
import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
|
|
7
6
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
8
7
|
import {
|
|
9
8
|
createStateSourceAction,
|
|
@@ -25,24 +24,6 @@ export interface ProjectionOptions<
|
|
|
25
24
|
projection: TProjection
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
/**
|
|
29
|
-
* @beta
|
|
30
|
-
*/
|
|
31
|
-
export function getProjectionState<
|
|
32
|
-
TProjection extends string = string,
|
|
33
|
-
TDocumentType extends string = string,
|
|
34
|
-
TDataset extends string = string,
|
|
35
|
-
TProjectId extends string = string,
|
|
36
|
-
>(
|
|
37
|
-
instance: SanityInstance,
|
|
38
|
-
options: ProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
|
|
39
|
-
): StateSource<
|
|
40
|
-
| ProjectionValuePending<
|
|
41
|
-
SanityProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
|
|
42
|
-
>
|
|
43
|
-
| undefined
|
|
44
|
-
>
|
|
45
|
-
|
|
46
27
|
/**
|
|
47
28
|
* @beta
|
|
48
29
|
*/
|
|
@@ -71,7 +52,7 @@ export function getProjectionState(
|
|
|
71
52
|
/**
|
|
72
53
|
* @beta
|
|
73
54
|
*/
|
|
74
|
-
export const _getProjectionState =
|
|
55
|
+
export const _getProjectionState = bindActionByResourceAndPerspective(
|
|
75
56
|
projectionStore,
|
|
76
57
|
createStateSourceAction({
|
|
77
58
|
selector: (
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {type ClientPerspective} from '@sanity/client'
|
|
2
1
|
import {DocumentId} from '@sanity/id-utils'
|
|
3
2
|
|
|
4
|
-
import {type
|
|
3
|
+
import {type PerspectiveHandle} from '../config/sanityConfig'
|
|
5
4
|
import {getPublishedId} from '../utils/ids'
|
|
6
5
|
import {
|
|
7
6
|
type DocumentProjections,
|
|
@@ -68,7 +67,7 @@ interface ProcessProjectionQueryOptions {
|
|
|
68
67
|
ids: Set<string>
|
|
69
68
|
results: ProjectionQueryResult[]
|
|
70
69
|
documentStatuses?: ProjectionStoreState['documentStatuses']
|
|
71
|
-
perspective:
|
|
70
|
+
perspective: NonNullable<PerspectiveHandle['perspective']>
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
export function processProjectionQuery({
|