@sanity/sdk 2.9.0 → 2.11.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 +295 -69
- package/dist/_chunks-es/_internal.js +3 -14
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +129 -59
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/version.js +1 -1
- package/dist/_exports/_internal.d.ts +16 -2
- package/dist/_exports/_internal.js +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +275 -149
- package/dist/index.js.map +1 -1
- package/package.json +11 -15
- package/src/_exports/_internal.ts +1 -0
- package/src/_exports/index.ts +33 -2
- package/src/agent/agentActions.ts +21 -25
- package/src/client/clientStore.test.ts +24 -60
- package/src/client/clientStore.ts +49 -56
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/node/actions/getOrCreateNode.test.ts +5 -2
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +3 -3
- package/src/config/sanityConfig.ts +72 -13
- package/src/document/applyDocumentActions.test.ts +7 -7
- package/src/document/applyDocumentActions.ts +5 -5
- package/src/document/documentStore.test.ts +68 -62
- package/src/document/documentStore.ts +33 -38
- package/src/document/processActions.ts +2 -2
- package/src/document/reducers.ts +4 -4
- package/src/document/sharedListener.ts +5 -7
- package/src/organization/organization.test-d.ts +102 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +166 -0
- package/src/organizations/organizations.test-d.ts +77 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +132 -0
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +13 -1
- package/src/presence/presenceStore.test.ts +101 -5
- package/src/presence/presenceStore.ts +96 -24
- package/src/preview/getPreviewState.ts +1 -1
- package/src/preview/previewProjectionUtils.test.ts +4 -4
- package/src/preview/previewProjectionUtils.ts +6 -7
- package/src/preview/resolvePreview.ts +5 -1
- package/src/project/project.test-d.ts +93 -0
- package/src/project/project.test.ts +108 -10
- package/src/project/project.ts +152 -26
- package/src/projection/getProjectionState.ts +4 -4
- package/src/projection/projectionStore.test.ts +2 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +11 -15
- package/src/projects/projects.test-d.ts +38 -0
- package/src/projects/projects.test.ts +104 -38
- package/src/projects/projects.ts +74 -14
- package/src/query/queryStore.test.ts +12 -12
- package/src/query/queryStore.ts +10 -11
- package/src/query/reducers.ts +3 -3
- package/src/releases/getPerspectiveState.ts +5 -5
- package/src/releases/releasesStore.test.ts +6 -6
- package/src/releases/releasesStore.ts +9 -9
- package/src/store/createActionBinder.test.ts +31 -31
- package/src/store/createActionBinder.ts +43 -38
- package/src/store/createSanityInstance.ts +5 -6
- package/src/telemetry/devMode.test.ts +8 -0
- package/src/telemetry/devMode.ts +10 -9
- package/src/telemetry/initTelemetry.test.ts +0 -17
- package/src/telemetry/initTelemetry.ts +2 -12
- package/src/users/reducers.ts +3 -4
- package/src/utils/createFetcherStore.ts +6 -4
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
|
@@ -1,20 +1,45 @@
|
|
|
1
|
-
import {type SanityClient} from '@sanity/client'
|
|
2
1
|
import {createSelector} from 'reselect'
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
catchError,
|
|
4
|
+
combineLatest,
|
|
5
|
+
distinctUntilChanged,
|
|
6
|
+
EMPTY,
|
|
7
|
+
filter,
|
|
8
|
+
first,
|
|
9
|
+
map,
|
|
10
|
+
type Observable,
|
|
11
|
+
of,
|
|
12
|
+
Subscription,
|
|
13
|
+
switchMap,
|
|
14
|
+
} from 'rxjs'
|
|
4
15
|
|
|
5
16
|
import {getTokenState} from '../auth/authStore'
|
|
6
17
|
import {getClient} from '../client/clientStore'
|
|
7
|
-
import {
|
|
8
|
-
|
|
18
|
+
import {
|
|
19
|
+
type DocumentResource,
|
|
20
|
+
isCanvasResource,
|
|
21
|
+
isDatasetResource,
|
|
22
|
+
isMediaLibraryResource,
|
|
23
|
+
} from '../config/sanityConfig'
|
|
24
|
+
import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
|
|
25
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
26
|
+
import {
|
|
27
|
+
createStateSourceAction,
|
|
28
|
+
type SelectorContext,
|
|
29
|
+
type StateSource,
|
|
30
|
+
} from '../store/createStateSourceAction'
|
|
9
31
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
10
32
|
import {type SanityUser} from '../users/types'
|
|
11
33
|
import {getUserState} from '../users/usersStore'
|
|
12
34
|
import {createBifurTransport} from './bifurTransport'
|
|
13
35
|
import {type PresenceLocation, type TransportEvent, type UserPresence} from './types'
|
|
14
36
|
|
|
37
|
+
const PRESENCE_API_VERSION = '2026-03-30'
|
|
38
|
+
|
|
15
39
|
type PresenceStoreState = {
|
|
16
40
|
locations: Map<string, {userId: string; locations: PresenceLocation[]}>
|
|
17
41
|
users: Record<string, SanityUser | undefined>
|
|
42
|
+
organizationId?: string
|
|
18
43
|
}
|
|
19
44
|
|
|
20
45
|
const getInitialState = (): PresenceStoreState => ({
|
|
@@ -23,27 +48,40 @@ const getInitialState = (): PresenceStoreState => ({
|
|
|
23
48
|
})
|
|
24
49
|
|
|
25
50
|
/** @public */
|
|
26
|
-
export const presenceStore = defineStore<PresenceStoreState,
|
|
51
|
+
export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
|
|
27
52
|
name: 'presence',
|
|
28
53
|
getInitialState,
|
|
29
|
-
initialize: (context: StoreContext<PresenceStoreState,
|
|
54
|
+
initialize: (context: StoreContext<PresenceStoreState, BoundResourceKey>) => {
|
|
30
55
|
const {
|
|
31
56
|
instance,
|
|
32
57
|
state,
|
|
33
|
-
key: {
|
|
58
|
+
key: {resource},
|
|
34
59
|
} = context
|
|
60
|
+
|
|
61
|
+
if (isMediaLibraryResource(resource)) {
|
|
62
|
+
throw new Error('Presence is not supported for media library resources.')
|
|
63
|
+
}
|
|
64
|
+
|
|
35
65
|
const sessionId = crypto.randomUUID()
|
|
36
66
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
67
|
+
// Dataset resources must use the project hostname so the socket URL is project-specific.
|
|
68
|
+
// Canvas resources use the global API endpoint via the resource config.
|
|
69
|
+
const client = isDatasetResource(resource)
|
|
70
|
+
? getClient(instance, {
|
|
71
|
+
apiVersion: PRESENCE_API_VERSION,
|
|
72
|
+
projectId: resource.projectId,
|
|
73
|
+
dataset: resource.dataset,
|
|
74
|
+
useProjectHostname: true,
|
|
75
|
+
})
|
|
76
|
+
: getClient(instance, {
|
|
77
|
+
apiVersion: PRESENCE_API_VERSION,
|
|
78
|
+
resource,
|
|
79
|
+
})
|
|
42
80
|
|
|
43
81
|
const token$ = getTokenState(instance).observable.pipe(distinctUntilChanged())
|
|
44
82
|
|
|
45
83
|
const [incomingEvents$, dispatch] = createBifurTransport({
|
|
46
|
-
client
|
|
84
|
+
client,
|
|
47
85
|
token$,
|
|
48
86
|
sessionId,
|
|
49
87
|
})
|
|
@@ -81,6 +119,22 @@ export const presenceStore = defineStore<PresenceStoreState, BoundDatasetKey>({
|
|
|
81
119
|
|
|
82
120
|
dispatch({type: 'rollCall'}).subscribe()
|
|
83
121
|
|
|
122
|
+
// Canvas resources need the organizationId to resolve users — fetch it once from the canvas endpoint
|
|
123
|
+
if (isCanvasResource(resource)) {
|
|
124
|
+
const globalClient = getClient(instance, {apiVersion: PRESENCE_API_VERSION})
|
|
125
|
+
subscription.add(
|
|
126
|
+
globalClient.observable
|
|
127
|
+
.request<{organizationId: string}>({
|
|
128
|
+
uri: `/canvases/${resource.canvasId}`,
|
|
129
|
+
tag: 'canvases.get',
|
|
130
|
+
})
|
|
131
|
+
.pipe(catchError(() => EMPTY))
|
|
132
|
+
.subscribe(({organizationId}) => {
|
|
133
|
+
state.set('presence/organizationId', (prev) => ({...prev, organizationId}))
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
84
138
|
return () => {
|
|
85
139
|
dispatch({type: 'disconnect'}).subscribe()
|
|
86
140
|
subscription.unsubscribe()
|
|
@@ -114,13 +168,13 @@ const selectPresence = createSelector(
|
|
|
114
168
|
},
|
|
115
169
|
)
|
|
116
170
|
|
|
117
|
-
|
|
118
|
-
export const getPresence = bindActionByDataset(
|
|
171
|
+
const _getPresence = bindActionByResource(
|
|
119
172
|
presenceStore,
|
|
120
173
|
createStateSourceAction({
|
|
121
|
-
selector: (context: SelectorContext<PresenceStoreState
|
|
174
|
+
selector: (context: SelectorContext<PresenceStoreState>): UserPresence[] =>
|
|
122
175
|
selectPresence(context.state),
|
|
123
|
-
onSubscribe: (context: StoreContext<PresenceStoreState,
|
|
176
|
+
onSubscribe: (context: StoreContext<PresenceStoreState, BoundResourceKey>) => {
|
|
177
|
+
const resource = context.key.resource
|
|
124
178
|
const userIds$ = context.state.observable.pipe(
|
|
125
179
|
map((state) =>
|
|
126
180
|
Array.from(state.locations.values())
|
|
@@ -130,26 +184,34 @@ export const getPresence = bindActionByDataset(
|
|
|
130
184
|
distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i])),
|
|
131
185
|
)
|
|
132
186
|
|
|
133
|
-
|
|
187
|
+
// For canvas resources, wait for organizationId to be fetched and stored in state.
|
|
188
|
+
// For dataset resources, emit undefined immediately so the stream isn't blocked.
|
|
189
|
+
const organizationId$: Observable<string | undefined> = isCanvasResource(resource)
|
|
190
|
+
? context.state.observable.pipe(
|
|
191
|
+
map((s) => s.organizationId),
|
|
192
|
+
filter((id): id is string => id !== undefined),
|
|
193
|
+
first(),
|
|
194
|
+
)
|
|
195
|
+
: of(undefined)
|
|
196
|
+
|
|
197
|
+
const subscription = combineLatest([userIds$, organizationId$])
|
|
134
198
|
.pipe(
|
|
135
|
-
switchMap((userIds) => {
|
|
199
|
+
switchMap(([userIds, organizationId]) => {
|
|
136
200
|
if (userIds.length === 0) {
|
|
137
201
|
return of([])
|
|
138
202
|
}
|
|
139
203
|
const userObservables = userIds.map((userId) =>
|
|
140
204
|
getUserState(context.instance, {
|
|
141
205
|
userId,
|
|
142
|
-
|
|
143
|
-
|
|
206
|
+
...(isDatasetResource(resource)
|
|
207
|
+
? {resourceType: 'project', projectId: resource.projectId}
|
|
208
|
+
: {resourceType: 'organization', organizationId}),
|
|
144
209
|
}).pipe(filter((v): v is NonNullable<typeof v> => !!v)),
|
|
145
210
|
)
|
|
146
211
|
return combineLatest(userObservables)
|
|
147
212
|
}),
|
|
148
213
|
)
|
|
149
214
|
.subscribe((users) => {
|
|
150
|
-
if (!users) {
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
215
|
context.state.set('presence/users', (prevState) => ({
|
|
154
216
|
...prevState,
|
|
155
217
|
users: {
|
|
@@ -167,3 +229,13 @@ export const getPresence = bindActionByDataset(
|
|
|
167
229
|
},
|
|
168
230
|
}),
|
|
169
231
|
)
|
|
232
|
+
|
|
233
|
+
/** @beta */
|
|
234
|
+
export function getPresence(
|
|
235
|
+
instance: SanityInstance,
|
|
236
|
+
params?: {resource?: DocumentResource},
|
|
237
|
+
): StateSource<UserPresence[]> {
|
|
238
|
+
// bit of a hack to support the old bound action by dataset
|
|
239
|
+
// in reality, this will always be passed a resource
|
|
240
|
+
return _getPresence(instance, params ?? {})
|
|
241
|
+
}
|
|
@@ -52,7 +52,7 @@ export function getPreviewState(
|
|
|
52
52
|
return {data: null, isPending: current?.isPending ?? false}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const previewValue = transformProjectionToPreview(instance, current.data, options.
|
|
55
|
+
const previewValue = transformProjectionToPreview(instance, current.data, options.resource)
|
|
56
56
|
|
|
57
57
|
return {
|
|
58
58
|
data: previewValue,
|
|
@@ -156,7 +156,7 @@ describe('transformProjectionToPreview', () => {
|
|
|
156
156
|
})
|
|
157
157
|
})
|
|
158
158
|
|
|
159
|
-
it('calls getClient with the provided
|
|
159
|
+
it('calls getClient with the provided resource', async () => {
|
|
160
160
|
const projectionResult: PreviewQueryResult = {
|
|
161
161
|
_id: 'doc1',
|
|
162
162
|
_type: 'article',
|
|
@@ -166,14 +166,14 @@ describe('transformProjectionToPreview', () => {
|
|
|
166
166
|
media: null,
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
const
|
|
169
|
+
const resource = {mediaLibraryId: 'test-library'}
|
|
170
170
|
|
|
171
|
-
transformProjectionToPreview(instance, projectionResult,
|
|
171
|
+
transformProjectionToPreview(instance, projectionResult, resource)
|
|
172
172
|
|
|
173
173
|
const {getClient} = await import('../client/clientStore')
|
|
174
174
|
expect(getClient).toHaveBeenCalledWith(instance, {
|
|
175
175
|
apiVersion: 'v2025-05-06',
|
|
176
|
-
|
|
176
|
+
resource,
|
|
177
177
|
})
|
|
178
178
|
})
|
|
179
179
|
})
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {type SanityClient} from '@sanity/client'
|
|
2
2
|
import {createImageUrlBuilder} from '@sanity/image-url'
|
|
3
|
-
import {isObject} from 'lodash-es'
|
|
4
3
|
|
|
5
4
|
import {getClient} from '../client/clientStore'
|
|
6
|
-
import {type
|
|
5
|
+
import {type DocumentResource} from '../config/sanityConfig'
|
|
7
6
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
7
|
+
import {isObject} from '../utils/object'
|
|
8
8
|
import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
|
|
9
9
|
import {type PreviewQueryResult, type PreviewValue} from './types'
|
|
10
10
|
|
|
@@ -66,22 +66,21 @@ function findFirstDefined(
|
|
|
66
66
|
*
|
|
67
67
|
* @param projectionResult - The raw projection result from GROQ
|
|
68
68
|
* @param instance - The Sanity instance to use for client configuration
|
|
69
|
-
* @param
|
|
69
|
+
* @param resource - Data resource for the preview
|
|
70
70
|
* @internal
|
|
71
71
|
*/
|
|
72
72
|
export function transformProjectionToPreview(
|
|
73
73
|
instance: SanityInstance,
|
|
74
74
|
projectionResult: PreviewQueryResult,
|
|
75
|
-
|
|
75
|
+
resource?: DocumentResource,
|
|
76
76
|
): PreviewValue {
|
|
77
77
|
const title = findFirstDefined(TITLE_CANDIDATES, projectionResult.titleCandidates)
|
|
78
78
|
const subtitle = findFirstDefined(SUBTITLE_CANDIDATES, projectionResult.subtitleCandidates, title)
|
|
79
79
|
|
|
80
|
-
// Get a client for the
|
|
80
|
+
// Get a client for the resource (if provided) or use the instance config
|
|
81
81
|
const client = getClient(instance, {
|
|
82
82
|
apiVersion: API_VERSION,
|
|
83
|
-
|
|
84
|
-
source: source && !isDatasetSource(source) ? source : undefined,
|
|
83
|
+
resource,
|
|
85
84
|
})
|
|
86
85
|
|
|
87
86
|
return {
|
|
@@ -30,7 +30,11 @@ export async function resolvePreview(
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Transform to preview format
|
|
33
|
-
const previewValue = transformProjectionToPreview(
|
|
33
|
+
const previewValue = transformProjectionToPreview(
|
|
34
|
+
instance,
|
|
35
|
+
projectionResult.data,
|
|
36
|
+
options.resource,
|
|
37
|
+
)
|
|
34
38
|
|
|
35
39
|
return {
|
|
36
40
|
data: previewValue,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {expectTypeOf, test} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
4
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
5
|
+
import {
|
|
6
|
+
getProjectState,
|
|
7
|
+
type Project,
|
|
8
|
+
type ProjectBase,
|
|
9
|
+
type ProjectMember,
|
|
10
|
+
resolveProject,
|
|
11
|
+
} from './project'
|
|
12
|
+
|
|
13
|
+
const instance = {} as SanityInstance
|
|
14
|
+
|
|
15
|
+
test('resolveProject — default call: members and features both included by default', () => {
|
|
16
|
+
expectTypeOf(resolveProject(instance)).resolves.toEqualTypeOf<Project<true, true>>()
|
|
17
|
+
type Result = Awaited<ReturnType<typeof resolveProject<true, true>>>
|
|
18
|
+
expectTypeOf<Result['members']>().toEqualTypeOf<ProjectMember[]>()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('resolveProject — includeMembers: false drops members from the type', () => {
|
|
22
|
+
expectTypeOf(resolveProject(instance, {includeMembers: false})).resolves.toEqualTypeOf<
|
|
23
|
+
Project<false, true>
|
|
24
|
+
>()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('resolveProject — includeFeatures: false drops features from the type', () => {
|
|
28
|
+
expectTypeOf(resolveProject(instance, {includeFeatures: false})).resolves.toEqualTypeOf<
|
|
29
|
+
Project<true, false>
|
|
30
|
+
>()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('resolveProject — both flags false → bare base shape', () => {
|
|
34
|
+
expectTypeOf(
|
|
35
|
+
resolveProject(instance, {includeMembers: false, includeFeatures: false}),
|
|
36
|
+
).resolves.toEqualTypeOf<Project<false, false>>()
|
|
37
|
+
type Result = Awaited<ReturnType<typeof resolveProject<false, false>>>
|
|
38
|
+
expectTypeOf<Result['id']>().toEqualTypeOf<string>()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('resolveProject — rejects non-boolean flag values', () => {
|
|
42
|
+
// @ts-expect-error — includeMembers must be a boolean
|
|
43
|
+
void resolveProject(instance, {includeMembers: 'yes'})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('resolveProject — projectId alone does not change the data shape', () => {
|
|
47
|
+
expectTypeOf(resolveProject(instance, {projectId: 'p'})).resolves.toEqualTypeOf<
|
|
48
|
+
Project<true, true>
|
|
49
|
+
>()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('resolveProject — non-literal boolean flag makes members optional', () => {
|
|
53
|
+
const includeMembers = false as boolean
|
|
54
|
+
expectTypeOf(resolveProject(instance, {includeMembers})).resolves.toEqualTypeOf<
|
|
55
|
+
Project<boolean, true>
|
|
56
|
+
>()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('getProjectState — default call returns members + features StateSource', () => {
|
|
60
|
+
expectTypeOf(getProjectState(instance)).toEqualTypeOf<
|
|
61
|
+
StateSource<Project<true, true> | undefined>
|
|
62
|
+
>()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('getProjectState — both flags false narrows to the bare base shape', () => {
|
|
66
|
+
expectTypeOf(
|
|
67
|
+
getProjectState(instance, {includeMembers: false, includeFeatures: false}),
|
|
68
|
+
).toEqualTypeOf<StateSource<Project<false, false> | undefined>>()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('Project — wide boolean for IncludeMembers makes members optional', () => {
|
|
72
|
+
expectTypeOf<Project<boolean, true>>().toEqualTypeOf<
|
|
73
|
+
ProjectBase & {members?: ProjectMember[]} & {features: string[]}
|
|
74
|
+
>()
|
|
75
|
+
expectTypeOf<Pick<Project<boolean, true>, 'members'>>().toEqualTypeOf<{
|
|
76
|
+
members?: ProjectMember[]
|
|
77
|
+
}>()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('Project — wide boolean for IncludeFeatures makes features optional', () => {
|
|
81
|
+
expectTypeOf<Project<true, boolean>>().toEqualTypeOf<
|
|
82
|
+
ProjectBase & {members: ProjectMember[]} & {features?: string[]}
|
|
83
|
+
>()
|
|
84
|
+
expectTypeOf<Pick<Project<true, boolean>, 'features'>>().toEqualTypeOf<{
|
|
85
|
+
features?: string[]
|
|
86
|
+
}>()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('Project — both wide booleans make both fields optional', () => {
|
|
90
|
+
expectTypeOf<Project<boolean, boolean>>().toEqualTypeOf<
|
|
91
|
+
ProjectBase & {members?: ProjectMember[]} & {features?: string[]}
|
|
92
|
+
>()
|
|
93
|
+
})
|
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import {type SanityClient} from '@sanity/client'
|
|
2
2
|
import {of} from 'rxjs'
|
|
3
|
-
import {describe, it} from 'vitest'
|
|
3
|
+
import {afterEach, beforeEach, describe, it} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {getClientState} from '../client/clientStore'
|
|
6
|
-
import {createSanityInstance} from '../store/createSanityInstance'
|
|
6
|
+
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
7
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
|
-
import {resolveProject} from './project'
|
|
8
|
+
import {getProjectCacheKey, resolveProject} from './project'
|
|
9
9
|
|
|
10
10
|
vi.mock('../client/clientStore')
|
|
11
11
|
|
|
12
12
|
describe('project', () => {
|
|
13
|
-
|
|
14
|
-
const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
13
|
+
let instance: SanityInstance
|
|
15
14
|
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
instance.dispose()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('calls `client.observable.request` against `/projects/<id>` and returns the result', async () => {
|
|
16
24
|
const project = {id: 'a'}
|
|
17
|
-
const
|
|
25
|
+
const request = vi.fn().mockReturnValue(of(project))
|
|
18
26
|
|
|
19
27
|
const mockClient = {
|
|
20
|
-
observable: {
|
|
21
|
-
projects: {getById} as unknown as SanityClient['observable']['projects'],
|
|
22
|
-
},
|
|
28
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
23
29
|
} as SanityClient
|
|
24
30
|
|
|
25
31
|
vi.mocked(getClientState).mockReturnValue({
|
|
@@ -28,6 +34,98 @@ describe('project', () => {
|
|
|
28
34
|
|
|
29
35
|
const result = await resolveProject(instance, {projectId: 'a'})
|
|
30
36
|
expect(result).toEqual(project)
|
|
31
|
-
expect(
|
|
37
|
+
expect(request).toHaveBeenCalledWith({
|
|
38
|
+
uri: '/projects/a',
|
|
39
|
+
query: {includeMembers: 'true', includeFeatures: 'true'},
|
|
40
|
+
tag: 'project.get',
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('serializes query params (booleans → strings) and respects flags', async () => {
|
|
45
|
+
const request = vi.fn().mockReturnValue(of({id: 'a'}))
|
|
46
|
+
const mockClient = {
|
|
47
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
48
|
+
} as SanityClient
|
|
49
|
+
|
|
50
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
51
|
+
observable: of(mockClient),
|
|
52
|
+
} as StateSource<SanityClient>)
|
|
53
|
+
|
|
54
|
+
await resolveProject(instance, {
|
|
55
|
+
projectId: 'a',
|
|
56
|
+
includeMembers: false,
|
|
57
|
+
includeFeatures: false,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
expect(request).toHaveBeenCalledWith({
|
|
61
|
+
uri: '/projects/a',
|
|
62
|
+
query: {
|
|
63
|
+
includeMembers: 'false',
|
|
64
|
+
includeFeatures: 'false',
|
|
65
|
+
},
|
|
66
|
+
tag: 'project.get',
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('falls back to the instance projectId when none is provided in options', async () => {
|
|
71
|
+
const request = vi.fn().mockReturnValue(of({id: 'p'}))
|
|
72
|
+
const mockClient = {
|
|
73
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
74
|
+
} as SanityClient
|
|
75
|
+
|
|
76
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
77
|
+
observable: of(mockClient),
|
|
78
|
+
} as StateSource<SanityClient>)
|
|
79
|
+
|
|
80
|
+
await resolveProject(instance)
|
|
81
|
+
expect(request).toHaveBeenCalledWith({
|
|
82
|
+
uri: '/projects/p',
|
|
83
|
+
query: {includeMembers: 'true', includeFeatures: 'true'},
|
|
84
|
+
tag: 'project.get',
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('project cache key generation', () => {
|
|
90
|
+
const mockInstance = {config: {projectId: 'p'}} as SanityInstance
|
|
91
|
+
|
|
92
|
+
it('default call includes :members and :features (both default-true)', () => {
|
|
93
|
+
expect(getProjectCacheKey(mockInstance)).toBe('project:p:members:features')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('treats undefined and the matching default as the same key', () => {
|
|
97
|
+
expect(getProjectCacheKey(mockInstance)).toBe(
|
|
98
|
+
getProjectCacheKey(mockInstance, {includeMembers: true, includeFeatures: true}),
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('explicit includeFeatures: false drops the :features segment', () => {
|
|
103
|
+
expect(getProjectCacheKey(mockInstance, {includeFeatures: false})).toBe('project:p:members')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('explicit includeMembers: false drops the :members segment', () => {
|
|
107
|
+
expect(getProjectCacheKey(mockInstance, {includeMembers: false})).toBe('project:p:features')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('combines all segments in order', () => {
|
|
111
|
+
expect(
|
|
112
|
+
getProjectCacheKey(mockInstance, {
|
|
113
|
+
projectId: 'a',
|
|
114
|
+
includeMembers: true,
|
|
115
|
+
includeFeatures: true,
|
|
116
|
+
}),
|
|
117
|
+
).toBe('project:a:members:features')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('produces distinct keys for each meaningful option permutation', () => {
|
|
121
|
+
const keys = new Set([
|
|
122
|
+
getProjectCacheKey(mockInstance),
|
|
123
|
+
getProjectCacheKey(mockInstance, {includeMembers: false}),
|
|
124
|
+
getProjectCacheKey(mockInstance, {includeFeatures: false}),
|
|
125
|
+
getProjectCacheKey(mockInstance, {includeMembers: false, includeFeatures: false}),
|
|
126
|
+
getProjectCacheKey(mockInstance, {projectId: 'a'}),
|
|
127
|
+
getProjectCacheKey(mockInstance, {projectId: 'b'}),
|
|
128
|
+
])
|
|
129
|
+
expect(keys.size).toBe(6)
|
|
32
130
|
})
|
|
33
131
|
})
|