@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
package/src/project/project.ts
CHANGED
|
@@ -2,40 +2,166 @@ import {switchMap} from 'rxjs'
|
|
|
2
2
|
|
|
3
3
|
import {getClientState} from '../client/clientStore'
|
|
4
4
|
import {type ProjectHandle} from '../config/sanityConfig'
|
|
5
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
6
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
5
7
|
import {createFetcherStore} from '../utils/createFetcherStore'
|
|
6
8
|
|
|
7
9
|
const API_VERSION = 'v2025-02-19'
|
|
8
10
|
|
|
11
|
+
/** @public */
|
|
12
|
+
export interface ProjectMemberRole {
|
|
13
|
+
name: string
|
|
14
|
+
title: string
|
|
15
|
+
description: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @public */
|
|
19
|
+
export interface ProjectMember {
|
|
20
|
+
id: string
|
|
21
|
+
createdAt: string
|
|
22
|
+
updatedAt: string
|
|
23
|
+
isCurrentUser: boolean
|
|
24
|
+
isRobot: boolean
|
|
25
|
+
roles: ProjectMemberRole[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @public */
|
|
29
|
+
export interface ProjectMetadata {
|
|
30
|
+
color?: string
|
|
31
|
+
externalStudioHost?: string
|
|
32
|
+
initialTemplate?: string
|
|
33
|
+
cliInitializedAt?: string
|
|
34
|
+
integration: 'manage' | 'cli'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The base fields returned from `/projects` for every project.
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
export interface ProjectBase {
|
|
42
|
+
id: string
|
|
43
|
+
displayName: string
|
|
44
|
+
studioHost: string | null
|
|
45
|
+
organizationId: string
|
|
46
|
+
metadata: ProjectMetadata
|
|
47
|
+
isBlocked: boolean
|
|
48
|
+
isDisabled: boolean
|
|
49
|
+
isDisabledByUser: boolean
|
|
50
|
+
activityFeedEnabled: boolean
|
|
51
|
+
createdAt: string
|
|
52
|
+
updatedAt: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A `Project` with `members` and/or `features` conditionally included
|
|
57
|
+
* based on the query options used to fetch it.
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
export type Project<
|
|
61
|
+
IncludeMembers extends boolean = true,
|
|
62
|
+
IncludeFeatures extends boolean = true,
|
|
63
|
+
> = ProjectBase &
|
|
64
|
+
// `boolean extends T` is non-distributive — true only when T is the wide
|
|
65
|
+
// `boolean`, in which case the field is optional. Literal `true`/`false`
|
|
66
|
+
// fall through to the strict branch.
|
|
67
|
+
(boolean extends IncludeMembers
|
|
68
|
+
? {members?: ProjectMember[]}
|
|
69
|
+
: IncludeMembers extends true
|
|
70
|
+
? {members: ProjectMember[]}
|
|
71
|
+
: unknown) &
|
|
72
|
+
(boolean extends IncludeFeatures
|
|
73
|
+
? {features?: string[]}
|
|
74
|
+
: IncludeFeatures extends true
|
|
75
|
+
? {features: string[]}
|
|
76
|
+
: unknown)
|
|
77
|
+
|
|
78
|
+
/** @public */
|
|
79
|
+
export interface ProjectOptions<
|
|
80
|
+
IncludeMembers extends boolean = true,
|
|
81
|
+
IncludeFeatures extends boolean = true,
|
|
82
|
+
> extends ProjectHandle {
|
|
83
|
+
includeMembers?: IncludeMembers
|
|
84
|
+
includeFeatures?: IncludeFeatures
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeProjectOptions(options?: ProjectOptions<boolean, boolean>) {
|
|
88
|
+
return {
|
|
89
|
+
includeMembers: options?.includeMembers ?? true,
|
|
90
|
+
includeFeatures: options?.includeFeatures ?? true,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveProjectId(instance: SanityInstance, options?: ProjectOptions<boolean, boolean>) {
|
|
95
|
+
const projectId = options?.projectId ?? instance.config.projectId
|
|
96
|
+
if (!projectId) {
|
|
97
|
+
throw new Error('A projectId is required to use the project API.')
|
|
98
|
+
}
|
|
99
|
+
return projectId
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @internal */
|
|
103
|
+
export function getProjectCacheKey(
|
|
104
|
+
instance: SanityInstance,
|
|
105
|
+
options?: ProjectOptions<boolean, boolean>,
|
|
106
|
+
): string {
|
|
107
|
+
const projectId = resolveProjectId(instance, options)
|
|
108
|
+
const {includeMembers, includeFeatures} = normalizeProjectOptions(options)
|
|
109
|
+
const membersKey = includeMembers ? ':members' : ''
|
|
110
|
+
const featuresKey = includeFeatures ? ':features' : ''
|
|
111
|
+
return `project:${projectId}${membersKey}${featuresKey}`
|
|
112
|
+
}
|
|
113
|
+
|
|
9
114
|
const project = createFetcherStore({
|
|
10
115
|
name: 'Project',
|
|
11
|
-
getKey:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
116
|
+
getKey: getProjectCacheKey,
|
|
117
|
+
fetcher: (instance) => (options?: ProjectOptions<boolean, boolean>) => {
|
|
118
|
+
const projectId = resolveProjectId(instance, options)
|
|
119
|
+
|
|
120
|
+
return getClientState(instance, {
|
|
121
|
+
apiVersion: API_VERSION,
|
|
122
|
+
scope: 'global',
|
|
123
|
+
}).observable.pipe(
|
|
124
|
+
switchMap((client) => {
|
|
125
|
+
const normalized = normalizeProjectOptions(options)
|
|
126
|
+
const query = Object.fromEntries(
|
|
127
|
+
Object.entries(normalized)
|
|
128
|
+
.filter(([, value]) => value !== undefined)
|
|
129
|
+
.map(([key, value]) => [key, String(value)]),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return client.observable.request({
|
|
133
|
+
uri: `/projects/${projectId}`,
|
|
134
|
+
query,
|
|
135
|
+
tag: 'project.get',
|
|
136
|
+
})
|
|
137
|
+
}),
|
|
138
|
+
)
|
|
17
139
|
},
|
|
18
|
-
fetcher:
|
|
19
|
-
(instance) =>
|
|
20
|
-
(options: ProjectHandle = {}) => {
|
|
21
|
-
const projectId = options.projectId ?? instance.config.projectId
|
|
22
|
-
|
|
23
|
-
return getClientState(instance, {
|
|
24
|
-
apiVersion: API_VERSION,
|
|
25
|
-
scope: 'global',
|
|
26
|
-
projectId,
|
|
27
|
-
}).observable.pipe(
|
|
28
|
-
switchMap((client) =>
|
|
29
|
-
client.observable.projects.getById(
|
|
30
|
-
// non-null assertion is fine with the above throwing
|
|
31
|
-
(projectId ?? instance.config.projectId)!,
|
|
32
|
-
),
|
|
33
|
-
),
|
|
34
|
-
)
|
|
35
|
-
},
|
|
36
140
|
})
|
|
37
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Public signature for the project state source. The conditional generics
|
|
144
|
+
* cannot flow through `BoundStoreAction`, so we declare the signature here
|
|
145
|
+
* and assign the (already-correct) runtime function to it.
|
|
146
|
+
*/
|
|
147
|
+
type GetProjectState = <
|
|
148
|
+
IncludeMembers extends boolean = true,
|
|
149
|
+
IncludeFeatures extends boolean = true,
|
|
150
|
+
>(
|
|
151
|
+
instance: SanityInstance,
|
|
152
|
+
options?: ProjectOptions<IncludeMembers, IncludeFeatures>,
|
|
153
|
+
) => StateSource<Project<IncludeMembers, IncludeFeatures> | undefined>
|
|
154
|
+
|
|
155
|
+
type ResolveProject = <
|
|
156
|
+
IncludeMembers extends boolean = true,
|
|
157
|
+
IncludeFeatures extends boolean = true,
|
|
158
|
+
>(
|
|
159
|
+
instance: SanityInstance,
|
|
160
|
+
options?: ProjectOptions<IncludeMembers, IncludeFeatures>,
|
|
161
|
+
) => Promise<Project<IncludeMembers, IncludeFeatures>>
|
|
162
|
+
|
|
38
163
|
/** @public */
|
|
39
|
-
export const getProjectState = project.getState
|
|
164
|
+
export const getProjectState: GetProjectState = project.getState
|
|
165
|
+
|
|
40
166
|
/** @public */
|
|
41
|
-
export const resolveProject = project.resolveState
|
|
167
|
+
export const resolveProject: ResolveProject = project.resolveState
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import {DocumentId, getPublishedId} from '@sanity/id-utils'
|
|
2
2
|
import {type SanityProjectionResult} from 'groq'
|
|
3
|
-
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,
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
} from '../store/createStateSourceAction'
|
|
13
12
|
import {hashString} from '../utils/hashString'
|
|
14
13
|
import {insecureRandomId} from '../utils/ids'
|
|
14
|
+
import {omitProperty} from '../utils/object'
|
|
15
15
|
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
16
16
|
import {projectionStore} from './projectionStore'
|
|
17
17
|
import {type ProjectionStoreState, type ProjectionValuePending} from './types'
|
|
@@ -72,7 +72,7 @@ export function getProjectionState(
|
|
|
72
72
|
/**
|
|
73
73
|
* @beta
|
|
74
74
|
*/
|
|
75
|
-
export const _getProjectionState =
|
|
75
|
+
export const _getProjectionState = bindActionByResourceAndPerspective(
|
|
76
76
|
projectionStore,
|
|
77
77
|
createStateSourceAction({
|
|
78
78
|
selector: (
|
|
@@ -113,7 +113,7 @@ export const _getProjectionState = bindActionBySourceAndPerspective(
|
|
|
113
113
|
return () => {
|
|
114
114
|
setCleanupTimeout(() => {
|
|
115
115
|
state.set('removeSubscription', (prev): Partial<ProjectionStoreState> => {
|
|
116
|
-
const documentSubscriptionsForHash =
|
|
116
|
+
const documentSubscriptionsForHash = omitProperty(
|
|
117
117
|
prev.subscriptions[documentId]?.[projectionHash],
|
|
118
118
|
subscriptionId,
|
|
119
119
|
)
|
|
@@ -30,7 +30,7 @@ describe('projectionStore', () => {
|
|
|
30
30
|
instance,
|
|
31
31
|
{
|
|
32
32
|
name: 'p.d',
|
|
33
|
-
|
|
33
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
34
34
|
perspective: 'drafts',
|
|
35
35
|
},
|
|
36
36
|
projectionStore,
|
|
@@ -42,7 +42,7 @@ describe('projectionStore', () => {
|
|
|
42
42
|
state,
|
|
43
43
|
key: {
|
|
44
44
|
name: 'p.d',
|
|
45
|
-
|
|
45
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
46
46
|
perspective: 'drafts',
|
|
47
47
|
},
|
|
48
48
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {type SanityProjectionResult} from 'groq'
|
|
2
2
|
import {filter, firstValueFrom} from 'rxjs'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
|
|
5
5
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
6
6
|
import {getProjectionState, type ProjectionOptions} from './getProjectionState'
|
|
7
7
|
import {projectionStore} from './projectionStore'
|
|
@@ -38,7 +38,7 @@ export function resolveProjection(
|
|
|
38
38
|
/**
|
|
39
39
|
* @beta
|
|
40
40
|
*/
|
|
41
|
-
const _resolveProjection =
|
|
41
|
+
const _resolveProjection = bindActionByResourceAndPerspective(
|
|
42
42
|
projectionStore,
|
|
43
43
|
(
|
|
44
44
|
{instance}: {instance: SanityInstance},
|
|
@@ -17,7 +17,7 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
17
17
|
let state: StoreState<ProjectionStoreState>
|
|
18
18
|
const key = {
|
|
19
19
|
name: 'test.test:drafts',
|
|
20
|
-
|
|
20
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
21
21
|
perspective: 'drafts' as const,
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {DocumentId} from '@sanity/id-utils'
|
|
2
2
|
import {
|
|
3
3
|
combineLatest,
|
|
4
4
|
debounceTime,
|
|
@@ -16,10 +16,10 @@ import {
|
|
|
16
16
|
tap,
|
|
17
17
|
} from 'rxjs'
|
|
18
18
|
|
|
19
|
-
import {isDatasetSource} from '../config/sanityConfig'
|
|
20
19
|
import {getQueryState, resolveQuery} from '../query/queryStore'
|
|
21
20
|
import {type BoundPerspectiveKey} from '../store/createActionBinder'
|
|
22
21
|
import {type StoreContext} from '../store/defineStore'
|
|
22
|
+
import {isDeepEqual} from '../utils/object'
|
|
23
23
|
import {
|
|
24
24
|
createProjectionQuery,
|
|
25
25
|
processProjectionQuery,
|
|
@@ -42,22 +42,22 @@ interface StatusQueryResult {
|
|
|
42
42
|
export const subscribeToStateAndFetchBatches = ({
|
|
43
43
|
state,
|
|
44
44
|
instance,
|
|
45
|
-
key: {
|
|
45
|
+
key: {resource, perspective},
|
|
46
46
|
}: StoreContext<ProjectionStoreState, BoundPerspectiveKey>): Subscription => {
|
|
47
47
|
const documentProjections$ = state.observable.pipe(
|
|
48
48
|
map((s) => s.documentProjections),
|
|
49
|
-
distinctUntilChanged(
|
|
49
|
+
distinctUntilChanged(isDeepEqual),
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
const activeDocumentIds$ = state.observable.pipe(
|
|
53
|
-
map(({subscriptions}) => new Set(Object.keys(subscriptions))),
|
|
53
|
+
map(({subscriptions}) => new Set(Object.keys(subscriptions).map((id) => DocumentId(id)))),
|
|
54
54
|
distinctUntilChanged(isSetEqual),
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
const pendingUpdateSubscription = activeDocumentIds$
|
|
58
58
|
.pipe(
|
|
59
59
|
debounceTime(BATCH_DEBOUNCE_TIME),
|
|
60
|
-
startWith(new Set<
|
|
60
|
+
startWith(new Set<DocumentId>()),
|
|
61
61
|
pairwise(),
|
|
62
62
|
tap(([prevIds, currIds]) => {
|
|
63
63
|
const newIds = [...currIds].filter((id) => !prevIds.has(id))
|
|
@@ -89,7 +89,7 @@ export const subscribeToStateAndFetchBatches = ({
|
|
|
89
89
|
|
|
90
90
|
const queryTrigger$ = combineLatest([activeDocumentIds$, documentProjections$]).pipe(
|
|
91
91
|
debounceTime(BATCH_DEBOUNCE_TIME),
|
|
92
|
-
distinctUntilChanged(
|
|
92
|
+
distinctUntilChanged(isDeepEqual),
|
|
93
93
|
)
|
|
94
94
|
|
|
95
95
|
const queryExecutionSubscription = queryTrigger$
|
|
@@ -112,8 +112,7 @@ export const subscribeToStateAndFetchBatches = ({
|
|
|
112
112
|
tag: PROJECTION_TAG,
|
|
113
113
|
perspective,
|
|
114
114
|
},
|
|
115
|
-
|
|
116
|
-
...(source && !isDatasetSource(source) ? {source} : {}),
|
|
115
|
+
resource,
|
|
117
116
|
})
|
|
118
117
|
|
|
119
118
|
const querySource$ = defer(() => {
|
|
@@ -127,8 +126,7 @@ export const subscribeToStateAndFetchBatches = ({
|
|
|
127
126
|
signal: controller.signal,
|
|
128
127
|
perspective,
|
|
129
128
|
},
|
|
130
|
-
|
|
131
|
-
...(source && !isDatasetSource(source) ? {source} : {}),
|
|
129
|
+
resource,
|
|
132
130
|
}),
|
|
133
131
|
).pipe(switchMap(() => observable))
|
|
134
132
|
}
|
|
@@ -153,8 +151,7 @@ export const subscribeToStateAndFetchBatches = ({
|
|
|
153
151
|
tag: PROJECTION_TAG,
|
|
154
152
|
perspective: 'raw',
|
|
155
153
|
},
|
|
156
|
-
|
|
157
|
-
...(source && !isDatasetSource(source) ? {source} : {}),
|
|
154
|
+
resource,
|
|
158
155
|
})
|
|
159
156
|
|
|
160
157
|
const statusQuerySource$ = defer(() => {
|
|
@@ -168,8 +165,7 @@ export const subscribeToStateAndFetchBatches = ({
|
|
|
168
165
|
signal: controller.signal,
|
|
169
166
|
perspective: 'raw',
|
|
170
167
|
},
|
|
171
|
-
|
|
172
|
-
...(source && !isDatasetSource(source) ? {source} : {}),
|
|
168
|
+
resource,
|
|
173
169
|
}),
|
|
174
170
|
).pipe(switchMap(() => observable))
|
|
175
171
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {expectTypeOf, test} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {type Project, type ProjectMember} from '../project/project'
|
|
4
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
5
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
6
|
+
import {getProjectsState, resolveProjects} from './projects'
|
|
7
|
+
|
|
8
|
+
const instance = {} as SanityInstance
|
|
9
|
+
|
|
10
|
+
test('resolveProjects — default call: features included, members omitted', () => {
|
|
11
|
+
expectTypeOf(resolveProjects(instance)).resolves.toEqualTypeOf<Project<false, true>[]>()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('resolveProjects — includeMembers: true adds members to the type', () => {
|
|
15
|
+
expectTypeOf(resolveProjects(instance, {includeMembers: true})).resolves.toEqualTypeOf<
|
|
16
|
+
Project<true, true>[]
|
|
17
|
+
>()
|
|
18
|
+
type Result = Awaited<ReturnType<typeof resolveProjects<true, true>>>
|
|
19
|
+
expectTypeOf<Result[number]['members']>().toEqualTypeOf<ProjectMember[]>()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('resolveProjects — includeFeatures: false drops features from the type', () => {
|
|
23
|
+
expectTypeOf(resolveProjects(instance, {includeFeatures: false})).resolves.toEqualTypeOf<
|
|
24
|
+
Project<false, false>[]
|
|
25
|
+
>()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('resolveProjects — organizationId alone does not change the data shape', () => {
|
|
29
|
+
expectTypeOf(resolveProjects(instance, {organizationId: 'org_123'})).resolves.toEqualTypeOf<
|
|
30
|
+
Project<false, true>[]
|
|
31
|
+
>()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('getProjectsState — default call returns features-only StateSource', () => {
|
|
35
|
+
expectTypeOf(getProjectsState(instance)).toEqualTypeOf<
|
|
36
|
+
StateSource<Project<false, true>[] | undefined>
|
|
37
|
+
>()
|
|
38
|
+
})
|
|
@@ -5,7 +5,7 @@ import {afterEach, beforeEach, describe, it} from 'vitest'
|
|
|
5
5
|
import {getClientState} from '../client/clientStore'
|
|
6
6
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
7
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
|
-
import {resolveProjects} from './projects'
|
|
8
|
+
import {getProjectsCacheKey, resolveProjects} from './projects'
|
|
9
9
|
|
|
10
10
|
vi.mock('../client/clientStore')
|
|
11
11
|
|
|
@@ -20,14 +20,12 @@ describe('projects', () => {
|
|
|
20
20
|
instance.dispose()
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
it('calls
|
|
23
|
+
it('calls `client.observable.request` against `/projects` and returns the result', async () => {
|
|
24
24
|
const projects = [{id: 'a'}, {id: 'b'}]
|
|
25
|
-
const
|
|
25
|
+
const request = vi.fn().mockReturnValue(of(projects))
|
|
26
26
|
|
|
27
27
|
const mockClient = {
|
|
28
|
-
observable: {
|
|
29
|
-
projects: {list} as unknown as SanityClient['observable']['projects'],
|
|
30
|
-
},
|
|
28
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
31
29
|
} as SanityClient
|
|
32
30
|
|
|
33
31
|
vi.mocked(getClientState).mockReturnValue({
|
|
@@ -36,41 +34,109 @@ describe('projects', () => {
|
|
|
36
34
|
|
|
37
35
|
const result = await resolveProjects(instance)
|
|
38
36
|
expect(result).toEqual(projects)
|
|
39
|
-
expect(
|
|
37
|
+
expect(request).toHaveBeenCalledWith({
|
|
38
|
+
uri: '/projects',
|
|
39
|
+
query: {includeMembers: 'false', includeFeatures: 'true', onlyExplicitMembership: 'false'},
|
|
40
|
+
tag: 'projects.get',
|
|
41
|
+
})
|
|
40
42
|
})
|
|
41
|
-
})
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const mockInstance = {} as SanityInstance
|
|
56
|
-
|
|
57
|
-
// Test default behavior (no options)
|
|
58
|
-
const defaultKey = mockGetKey(mockInstance)
|
|
59
|
-
expect(defaultKey).toBe('projects')
|
|
60
|
-
|
|
61
|
-
// Test with organizationId only
|
|
62
|
-
const orgKey = mockGetKey(mockInstance, {organizationId: 'org123'})
|
|
63
|
-
expect(orgKey).toBe('projects:org:org123')
|
|
64
|
-
|
|
65
|
-
// Test with includeMembers: false only
|
|
66
|
-
const noMembersKey = mockGetKey(mockInstance, {includeMembers: false})
|
|
67
|
-
expect(noMembersKey).toBe('projects:no-members')
|
|
68
|
-
|
|
69
|
-
// Test with both parameters
|
|
70
|
-
const bothKey = mockGetKey(mockInstance, {
|
|
44
|
+
it('serializes query params (booleans → strings) and omits undefined values', async () => {
|
|
45
|
+
const request = vi.fn().mockReturnValue(of([]))
|
|
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 resolveProjects(instance, {
|
|
71
55
|
organizationId: 'org123',
|
|
72
|
-
includeMembers:
|
|
56
|
+
includeMembers: true,
|
|
57
|
+
includeFeatures: false,
|
|
73
58
|
})
|
|
74
|
-
|
|
59
|
+
|
|
60
|
+
expect(request).toHaveBeenCalledWith({
|
|
61
|
+
uri: '/projects',
|
|
62
|
+
query: {
|
|
63
|
+
organizationId: 'org123',
|
|
64
|
+
includeMembers: 'true',
|
|
65
|
+
includeFeatures: 'false',
|
|
66
|
+
onlyExplicitMembership: 'false',
|
|
67
|
+
},
|
|
68
|
+
tag: 'projects.get',
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('projects cache key generation', () => {
|
|
74
|
+
let instance: SanityInstance
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
instance.dispose()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('default call includes :features (default-true) and excludes :members (default-false)', () => {
|
|
85
|
+
expect(getProjectsCacheKey(instance)).toBe('projects:features')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('treats undefined and the matching default as the same key', () => {
|
|
89
|
+
expect(getProjectsCacheKey(instance)).toBe(
|
|
90
|
+
getProjectsCacheKey(instance, {includeMembers: false, includeFeatures: true}),
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('treats raw and explicit defaults equivalently', () => {
|
|
95
|
+
expect(getProjectsCacheKey(instance, {organizationId: 'org123'})).toBe(
|
|
96
|
+
getProjectsCacheKey(instance, {
|
|
97
|
+
organizationId: 'org123',
|
|
98
|
+
includeMembers: false,
|
|
99
|
+
includeFeatures: true,
|
|
100
|
+
onlyExplicitMembership: false,
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('explicit includeFeatures: false drops the :features segment', () => {
|
|
106
|
+
expect(getProjectsCacheKey(instance, {includeFeatures: false})).toBe('projects')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('appends an org segment when organizationId is set', () => {
|
|
110
|
+
expect(getProjectsCacheKey(instance, {organizationId: 'org123'})).toBe(
|
|
111
|
+
'projects:org:org123:features',
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('appends :members when includeMembers is true', () => {
|
|
116
|
+
expect(getProjectsCacheKey(instance, {includeMembers: true})).toBe('projects:members:features')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('combines all segments in order', () => {
|
|
120
|
+
expect(
|
|
121
|
+
getProjectsCacheKey(instance, {
|
|
122
|
+
organizationId: 'org123',
|
|
123
|
+
includeMembers: true,
|
|
124
|
+
includeFeatures: true,
|
|
125
|
+
onlyExplicitMembership: true,
|
|
126
|
+
}),
|
|
127
|
+
).toBe('projects:org:org123:members:features:explicit')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('produces distinct keys for each meaningful option permutation', () => {
|
|
131
|
+
const keys = new Set([
|
|
132
|
+
getProjectsCacheKey(instance),
|
|
133
|
+
getProjectsCacheKey(instance, {includeMembers: true}),
|
|
134
|
+
getProjectsCacheKey(instance, {includeFeatures: false}),
|
|
135
|
+
getProjectsCacheKey(instance, {includeMembers: true, includeFeatures: false}),
|
|
136
|
+
getProjectsCacheKey(instance, {onlyExplicitMembership: true}),
|
|
137
|
+
getProjectsCacheKey(instance, {organizationId: 'a'}),
|
|
138
|
+
getProjectsCacheKey(instance, {organizationId: 'b'}),
|
|
139
|
+
])
|
|
140
|
+
expect(keys.size).toBe(7)
|
|
75
141
|
})
|
|
76
142
|
})
|