@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.1
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/createGroqSearchFilter.d.ts +1147 -0
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
- package/dist/_chunks-es/_internal.js +134 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +93 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_exports/_internal.d.ts +95 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +24 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +1079 -1076
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1967 -2302
- package/dist/index.js.map +1 -1
- package/package.json +48 -47
- package/src/_exports/_internal.ts +26 -0
- package/src/_exports/index.ts +120 -11
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- package/src/agent/agentActions.test.ts +16 -60
- package/src/agent/agentActions.ts +25 -38
- package/src/applications/applications.test-d.ts +83 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +364 -0
- package/src/applications/userApplications.test.ts +126 -0
- package/src/applications/userApplications.ts +261 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authMode.test.ts +25 -0
- package/src/auth/authMode.ts +6 -3
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +59 -26
- package/src/auth/getOrganizationVerificationState.test.ts +18 -17
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +35 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +75 -2
- package/src/auth/logout.ts +23 -4
- package/src/auth/refreshStampedToken.test.ts +219 -469
- package/src/auth/refreshStampedToken.ts +79 -195
- package/src/auth/studioAuth.ts +1 -1
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
- package/src/client/clientStore.test.ts +62 -37
- package/src/client/clientStore.ts +45 -75
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/comlink/controller/actions/destroyController.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
- package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +7 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +109 -79
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +10 -4
- package/src/document/actions.test.ts +115 -33
- package/src/document/actions.ts +212 -15
- package/src/document/applyDocumentActions.test.ts +38 -12
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +673 -278
- package/src/document/documentStore.ts +162 -71
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +82 -26
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +74 -21
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +49 -2
- package/src/document/patchOperations.ts +50 -0
- package/src/document/permissions.test.ts +105 -60
- package/src/document/permissions.ts +44 -19
- package/src/document/processActions/create.ts +138 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +154 -0
- package/src/document/processActions/processActions.ts +218 -0
- package/src/document/processActions/publish.ts +120 -0
- package/src/document/processActions/releaseArchive.ts +77 -0
- package/src/document/processActions/releaseCreate.ts +59 -0
- package/src/document/processActions/releaseDelete.ts +65 -0
- package/src/document/processActions/releaseEdit.ts +37 -0
- package/src/document/processActions/releasePublish.ts +45 -0
- package/src/document/processActions/releaseSchedule.ts +87 -0
- package/src/document/processActions/releaseUtil.ts +31 -0
- package/src/document/processActions/shared.ts +308 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +1129 -121
- package/src/document/reducers.test.ts +639 -64
- package/src/document/reducers.ts +187 -48
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/src/document/sharedListener.test.ts +16 -14
- package/src/document/sharedListener.ts +24 -8
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +68 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +202 -0
- package/src/organization/organization.test-d.ts +36 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +138 -0
- package/src/organizations/organizations.test-d.ts +56 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +107 -0
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +94 -23
- package/src/presence/presenceStore.ts +85 -29
- package/src/preview/getPreviewState.test.ts +137 -0
- package/src/preview/getPreviewState.ts +69 -0
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/resolvePreview.test.ts +64 -0
- package/src/preview/resolvePreview.ts +43 -0
- package/src/preview/types.ts +10 -0
- package/src/project/project.test-d.ts +37 -0
- package/src/project/project.test.ts +157 -12
- package/src/project/project.ts +138 -25
- package/src/projection/getProjectionState.test.ts +18 -20
- package/src/projection/getProjectionState.ts +23 -3
- package/src/projection/projectionQuery.ts +5 -5
- package/src/projection/projectionStore.test.ts +1 -1
- package/src/projection/resolveProjection.test.ts +1 -2
- package/src/projection/resolveProjection.ts +16 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
- package/src/projection/types.ts +8 -0
- package/src/projects/projects.test-d.ts +32 -0
- package/src/projects/projects.test.ts +108 -42
- package/src/projects/projects.ts +63 -27
- package/src/query/queryStore.test.ts +259 -123
- package/src/query/queryStore.ts +78 -63
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +2 -18
- package/src/releases/getPerspectiveState.test.ts +17 -23
- package/src/releases/getPerspectiveState.ts +8 -20
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +100 -53
- package/src/releases/releasesStore.ts +69 -44
- package/src/releases/utils/sortReleases.test.ts +38 -2
- package/src/releases/utils/sortReleases.ts +15 -10
- package/src/store/createActionBinder.test.ts +87 -90
- package/src/store/createActionBinder.ts +79 -25
- package/src/store/createSanityInstance.test.ts +83 -13
- package/src/store/createSanityInstance.ts +77 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +39 -44
- package/src/store/createStoreInstance.test.ts +4 -2
- package/src/store/fetcherStore.test-d.ts +121 -0
- package/src/store/fetcherStore.test.ts +299 -0
- package/src/store/fetcherStore.ts +801 -0
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/events.ts +42 -0
- package/src/telemetry/initTelemetry.test.ts +432 -0
- package/src/telemetry/initTelemetry.ts +218 -0
- package/src/telemetry/telemetryManager.test.ts +327 -0
- package/src/telemetry/telemetryManager.ts +199 -0
- package/src/users/reducers.test.ts +6 -1
- package/src/users/reducers.ts +5 -6
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +16 -12
- package/src/users/usersStore.ts +5 -1
- package/src/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/createFetcherStore.test.ts +7 -5
- package/src/utils/createFetcherStore.ts +8 -5
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/includeShape.ts +36 -0
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/logger.test.ts +12 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +37 -0
- package/src/document/processActions.ts +0 -707
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import {switchMap} from 'rxjs'
|
|
2
|
+
|
|
3
|
+
import {getClientState} from '../client/clientStore'
|
|
4
|
+
import {defineFetcher} from '../store/fetcherStore'
|
|
5
|
+
import {buildQuery} from '../utils/buildQuery'
|
|
6
|
+
import {type Included} from '../utils/includeShape'
|
|
7
|
+
|
|
8
|
+
// The Applications API is currently only available on vX.
|
|
9
|
+
const API_VERSION = 'vX'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Related data the Installations API can embed in a response. Each token adds
|
|
13
|
+
* a top-level field to the {@link Installation} item.
|
|
14
|
+
*
|
|
15
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export type InstallationInclude = 'activeConfig' | 'access' | 'interfaces'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A resource an installation accesses, embedded when `access` is included.
|
|
22
|
+
*
|
|
23
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
export interface InstallationAccess {
|
|
27
|
+
id: string
|
|
28
|
+
resourceType: 'datasets' | 'canvases' | 'dashboards' | 'media-libraries'
|
|
29
|
+
resourceId: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* An interface exposed by an installation, embedded when `interfaces` is
|
|
34
|
+
* included.
|
|
35
|
+
*
|
|
36
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export interface InstallationInterface {
|
|
40
|
+
id: string
|
|
41
|
+
type: 'app' | 'worker' | 'asset_source' | 'panel'
|
|
42
|
+
name: string
|
|
43
|
+
title: string
|
|
44
|
+
version: string
|
|
45
|
+
/** Module federation module ID; resolved from the host mf-manifest at runtime */
|
|
46
|
+
moduleId: string
|
|
47
|
+
metadata: {group?: string; priority?: number} | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The active config of an installation, embedded when `activeConfig` is
|
|
52
|
+
* included. `null` when nothing is deployed.
|
|
53
|
+
*
|
|
54
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
export interface InstallationActiveConfig {
|
|
58
|
+
id: string
|
|
59
|
+
installationId: string
|
|
60
|
+
deployedBy: string | null
|
|
61
|
+
version: string
|
|
62
|
+
isActive: boolean
|
|
63
|
+
createdAt: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The always-present fields of an active singleton installation returned by the
|
|
68
|
+
* Installations API (`/installations`), before any {@link InstallationInclude}.
|
|
69
|
+
*
|
|
70
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
73
|
+
export interface InstallationBase {
|
|
74
|
+
id: string
|
|
75
|
+
applicationId: string
|
|
76
|
+
organizationId: string
|
|
77
|
+
installedBy: string | null
|
|
78
|
+
createdAt: string
|
|
79
|
+
updatedAt: string
|
|
80
|
+
application: {
|
|
81
|
+
title: string
|
|
82
|
+
slug: string | null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* An active singleton installation as returned by the Installations API
|
|
88
|
+
* (`/installations`). `activeConfig`, `access` and `interfaces` are top-level
|
|
89
|
+
* fields, each present only when the matching {@link InstallationInclude} token
|
|
90
|
+
* was requested.
|
|
91
|
+
*
|
|
92
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
93
|
+
* @public
|
|
94
|
+
*/
|
|
95
|
+
export type Installation<Include extends InstallationInclude = never> = InstallationBase &
|
|
96
|
+
Included<
|
|
97
|
+
InstallationInclude,
|
|
98
|
+
'activeConfig',
|
|
99
|
+
Include,
|
|
100
|
+
{activeConfig: InstallationActiveConfig | null}
|
|
101
|
+
> &
|
|
102
|
+
Included<InstallationInclude, 'access', Include, {access: InstallationAccess[]}> &
|
|
103
|
+
Included<InstallationInclude, 'interfaces', Include, {interfaces: InstallationInterface[]}>
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Options for listing an organization's installations.
|
|
107
|
+
*
|
|
108
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
export interface InstallationsOptions<Include extends InstallationInclude = InstallationInclude> {
|
|
112
|
+
organizationId: string
|
|
113
|
+
include?: Include[]
|
|
114
|
+
/** Page size (1-100, default 50), or `'none'` to disable pagination */
|
|
115
|
+
limit?: number | 'none'
|
|
116
|
+
/** Cursor from a previous response's `nextCursor` */
|
|
117
|
+
cursor?: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The cursor-paginated envelope returned by `GET /installations`.
|
|
122
|
+
*
|
|
123
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
124
|
+
* @public
|
|
125
|
+
*/
|
|
126
|
+
export interface InstallationsResponse<Include extends InstallationInclude = never> {
|
|
127
|
+
nextCursor: string | null
|
|
128
|
+
data: Installation<Include>[]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The API takes `include` as a comma-separated list; sorted so key and query agree */
|
|
132
|
+
function serializeInclude(include: InstallationInclude[] | undefined): string | undefined {
|
|
133
|
+
return include?.length ? [...include].sort().join(',') : undefined
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Fetcher for an organization's installations (`GET /installations`), on the
|
|
138
|
+
* shared fetcher cache.
|
|
139
|
+
*
|
|
140
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
143
|
+
export const installations = defineFetcher<
|
|
144
|
+
[options: InstallationsOptions],
|
|
145
|
+
InstallationsResponse<InstallationInclude>
|
|
146
|
+
>({
|
|
147
|
+
name: 'installations',
|
|
148
|
+
getKey: (_instance, options) =>
|
|
149
|
+
[
|
|
150
|
+
options.organizationId,
|
|
151
|
+
serializeInclude(options.include) ?? '',
|
|
152
|
+
options.limit ?? '',
|
|
153
|
+
options.cursor ?? '',
|
|
154
|
+
].join(':'),
|
|
155
|
+
fetch: (instance) => (options) =>
|
|
156
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
157
|
+
switchMap((client) =>
|
|
158
|
+
client.observable.request<InstallationsResponse<InstallationInclude>>({
|
|
159
|
+
uri: '/installations',
|
|
160
|
+
query: buildQuery({
|
|
161
|
+
organizationId: options.organizationId,
|
|
162
|
+
include: serializeInclude(options.include),
|
|
163
|
+
limit: options.limit,
|
|
164
|
+
cursor: options.cursor,
|
|
165
|
+
}),
|
|
166
|
+
tag: 'installations.list',
|
|
167
|
+
}),
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
tags: (data) => [
|
|
171
|
+
{type: 'installation', id: 'LIST'},
|
|
172
|
+
...data.data.map((installation) => ({type: 'installation', id: installation.id})),
|
|
173
|
+
],
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Fetcher for a single installation (`GET /installations/:id`), on the shared
|
|
178
|
+
* fetcher cache. No pre-seeding from {@link installations}: its list keys
|
|
179
|
+
* include the organization, which this fetcher's params can't reconstruct.
|
|
180
|
+
*
|
|
181
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
182
|
+
* @internal
|
|
183
|
+
*/
|
|
184
|
+
export const installation = defineFetcher<
|
|
185
|
+
[installationId: string, options?: {include?: InstallationInclude[]}],
|
|
186
|
+
Installation<InstallationInclude>
|
|
187
|
+
>({
|
|
188
|
+
name: 'installation',
|
|
189
|
+
getKey: (_instance, installationId, options) =>
|
|
190
|
+
`${installationId}:${serializeInclude(options?.include) ?? ''}`,
|
|
191
|
+
fetch: (instance) => (installationId, options) =>
|
|
192
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
193
|
+
switchMap((client) =>
|
|
194
|
+
client.observable.request<Installation<InstallationInclude>>({
|
|
195
|
+
uri: `/installations/${installationId}`,
|
|
196
|
+
query: buildQuery({include: serializeInclude(options?.include)}),
|
|
197
|
+
tag: 'installations.get',
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
tags: (data) => [{type: 'installation', id: data.id}],
|
|
202
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {expectTypeOf, test} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
4
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
5
|
+
import {type FetcherSnapshot} from '../store/fetcherStore'
|
|
6
|
+
import {type Organization, organization, type OrganizationMember} from './organization'
|
|
7
|
+
|
|
8
|
+
const instance = {} as SanityInstance
|
|
9
|
+
|
|
10
|
+
test('organization.resolveState — resolves the wide-boolean detail shape', () => {
|
|
11
|
+
expectTypeOf(
|
|
12
|
+
organization.resolveState(instance, {organizationId: 'org_1'}),
|
|
13
|
+
).resolves.toEqualTypeOf<Organization<boolean, boolean>>()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('Organization — literal flags narrow members and features', () => {
|
|
17
|
+
expectTypeOf<Organization<true, true>['members']>().toEqualTypeOf<OrganizationMember[]>()
|
|
18
|
+
expectTypeOf<Organization<true, true>['features']>().toEqualTypeOf<string[]>()
|
|
19
|
+
expectTypeOf<keyof Organization<false, false> & ('members' | 'features')>().toEqualTypeOf<never>()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('organization.getState — exposes the snapshot envelope', () => {
|
|
23
|
+
expectTypeOf(organization.getState(instance, {organizationId: 'org_1'})).toEqualTypeOf<
|
|
24
|
+
StateSource<FetcherSnapshot<Organization<boolean, boolean>>>
|
|
25
|
+
>()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('organization — requires organizationId in options', () => {
|
|
29
|
+
// @ts-expect-error organizationId is required
|
|
30
|
+
void organization.resolveState(instance, {})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('organization — rejects non-boolean flag values', () => {
|
|
34
|
+
// @ts-expect-error includeMembers must be a boolean
|
|
35
|
+
void organization.resolveState(instance, {organizationId: 'org_1', includeMembers: 'yes'})
|
|
36
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {of} from 'rxjs'
|
|
3
|
+
import {afterEach, beforeEach, describe, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {getClientState} from '../client/clientStore'
|
|
6
|
+
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
|
+
import {getOrganizationCacheKey, organization} from './organization'
|
|
9
|
+
|
|
10
|
+
vi.mock('../client/clientStore')
|
|
11
|
+
|
|
12
|
+
describe('organization', () => {
|
|
13
|
+
let instance: SanityInstance
|
|
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 `/organizations/<id>` and returns the result', async () => {
|
|
24
|
+
const detail = {id: 'org_1'}
|
|
25
|
+
const request = vi.fn().mockReturnValue(of(detail))
|
|
26
|
+
|
|
27
|
+
const mockClient = {
|
|
28
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
29
|
+
} as SanityClient
|
|
30
|
+
|
|
31
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
32
|
+
observable: of(mockClient),
|
|
33
|
+
} as StateSource<SanityClient>)
|
|
34
|
+
|
|
35
|
+
const result = await organization.resolveState(instance, {organizationId: 'org_1'})
|
|
36
|
+
expect(result).toEqual(detail)
|
|
37
|
+
expect(request).toHaveBeenCalledWith({
|
|
38
|
+
uri: '/organizations/org_1',
|
|
39
|
+
query: {includeMembers: 'false', includeFeatures: 'false'},
|
|
40
|
+
tag: 'organizations.get',
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('serializes query params (booleans → strings) and respects flags', async () => {
|
|
45
|
+
const request = vi.fn().mockReturnValue(of({id: 'org_1'}))
|
|
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 organization.resolveState(instance, {
|
|
55
|
+
organizationId: 'org_1',
|
|
56
|
+
includeMembers: true,
|
|
57
|
+
includeFeatures: true,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
expect(request).toHaveBeenCalledWith({
|
|
61
|
+
uri: '/organizations/org_1',
|
|
62
|
+
query: {
|
|
63
|
+
includeMembers: 'true',
|
|
64
|
+
includeFeatures: 'true',
|
|
65
|
+
},
|
|
66
|
+
tag: 'organizations.get',
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('throws when no organizationId is provided', async () => {
|
|
71
|
+
await expect(
|
|
72
|
+
organization.resolveState(instance, {organizationId: ''} as never),
|
|
73
|
+
).rejects.toThrow('An organizationId is required to use the organization API.')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('organization cache key generation', () => {
|
|
78
|
+
let instance: SanityInstance
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
instance = createSanityInstance({})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
instance.dispose()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('default call excludes :members and :features (both default-false)', () => {
|
|
89
|
+
expect(getOrganizationCacheKey(instance, {organizationId: 'org_1'})).toBe('organization:org_1')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('treats undefined and the matching default as the same key', () => {
|
|
93
|
+
expect(getOrganizationCacheKey(instance, {organizationId: 'org_1'})).toBe(
|
|
94
|
+
getOrganizationCacheKey(instance, {
|
|
95
|
+
organizationId: 'org_1',
|
|
96
|
+
includeMembers: false,
|
|
97
|
+
includeFeatures: false,
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('explicit includeMembers: true appends :members', () => {
|
|
103
|
+
expect(getOrganizationCacheKey(instance, {organizationId: 'org_1', includeMembers: true})).toBe(
|
|
104
|
+
'organization:org_1:members',
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('explicit includeFeatures: true appends :features', () => {
|
|
109
|
+
expect(
|
|
110
|
+
getOrganizationCacheKey(instance, {organizationId: 'org_1', includeFeatures: true}),
|
|
111
|
+
).toBe('organization:org_1:features')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('combines all segments in order', () => {
|
|
115
|
+
expect(
|
|
116
|
+
getOrganizationCacheKey(instance, {
|
|
117
|
+
organizationId: 'org_1',
|
|
118
|
+
includeMembers: true,
|
|
119
|
+
includeFeatures: true,
|
|
120
|
+
}),
|
|
121
|
+
).toBe('organization:org_1:members:features')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('produces distinct keys for each meaningful option permutation', () => {
|
|
125
|
+
const keys = new Set([
|
|
126
|
+
getOrganizationCacheKey(instance, {organizationId: 'org_1'}),
|
|
127
|
+
getOrganizationCacheKey(instance, {organizationId: 'org_1', includeMembers: true}),
|
|
128
|
+
getOrganizationCacheKey(instance, {organizationId: 'org_1', includeFeatures: true}),
|
|
129
|
+
getOrganizationCacheKey(instance, {
|
|
130
|
+
organizationId: 'org_1',
|
|
131
|
+
includeMembers: true,
|
|
132
|
+
includeFeatures: true,
|
|
133
|
+
}),
|
|
134
|
+
getOrganizationCacheKey(instance, {organizationId: 'org_2'}),
|
|
135
|
+
])
|
|
136
|
+
expect(keys.size).toBe(5)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {switchMap} from 'rxjs'
|
|
2
|
+
|
|
3
|
+
import {getClientState} from '../client/clientStore'
|
|
4
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
5
|
+
import {defineFetcher} from '../store/fetcherStore'
|
|
6
|
+
import {buildQuery} from '../utils/buildQuery'
|
|
7
|
+
|
|
8
|
+
const API_VERSION = 'v2025-02-19'
|
|
9
|
+
|
|
10
|
+
/** @public */
|
|
11
|
+
export interface OrganizationMember {
|
|
12
|
+
sanityUserId: string
|
|
13
|
+
isCurrentUser: boolean
|
|
14
|
+
user: {
|
|
15
|
+
id: string
|
|
16
|
+
displayName: string
|
|
17
|
+
familyName: string
|
|
18
|
+
givenName: string
|
|
19
|
+
middleName: string | null
|
|
20
|
+
imageUrl: string | null
|
|
21
|
+
email: string
|
|
22
|
+
loginProvider: string
|
|
23
|
+
}
|
|
24
|
+
roles: Array<{
|
|
25
|
+
name: string
|
|
26
|
+
title: string
|
|
27
|
+
description?: string
|
|
28
|
+
}>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The base fields returned from `/organizations/<id>` for every organization.
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export interface OrganizationBase {
|
|
36
|
+
id: string
|
|
37
|
+
name: string
|
|
38
|
+
slug: string | null
|
|
39
|
+
createdAt: string
|
|
40
|
+
createdByUserId: string
|
|
41
|
+
updatedAt: string
|
|
42
|
+
deletedAt: string | null
|
|
43
|
+
dashboardStatus: 'enabled' | 'disabled'
|
|
44
|
+
aiFeaturesStatus: 'enabled' | 'disabled'
|
|
45
|
+
mediaLibraryStatus: 'enabled' | 'disabled'
|
|
46
|
+
requestAccessStatus: 'allowed' | 'disabled'
|
|
47
|
+
telemetryConsentStatus: 'allowed' | 'msa_denied' | 'customer_denied'
|
|
48
|
+
oauthAppsStatus: 'allowed' | 'blocked'
|
|
49
|
+
defaultRoleName: string
|
|
50
|
+
domains: string[] | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @public */
|
|
54
|
+
export interface OrganizationOptions<
|
|
55
|
+
IncludeMembers extends boolean = false,
|
|
56
|
+
IncludeFeatures extends boolean = false,
|
|
57
|
+
> {
|
|
58
|
+
includeMembers?: IncludeMembers
|
|
59
|
+
includeFeatures?: IncludeFeatures
|
|
60
|
+
organizationId: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* An `Organization` with `members` and/or `features` conditionally included
|
|
65
|
+
* based on the query options used to fetch it.
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export type Organization<
|
|
69
|
+
IncludeMembers extends boolean = false,
|
|
70
|
+
IncludeFeatures extends boolean = false,
|
|
71
|
+
> = OrganizationBase &
|
|
72
|
+
// `boolean extends T` is non-distributive — true only when T is the wide
|
|
73
|
+
// `boolean`, in which case the field is optional. Literal `true`/`false`
|
|
74
|
+
// fall through to the strict branch.
|
|
75
|
+
(boolean extends IncludeMembers
|
|
76
|
+
? {members?: OrganizationMember[]}
|
|
77
|
+
: IncludeMembers extends true
|
|
78
|
+
? {members: OrganizationMember[]}
|
|
79
|
+
: unknown) &
|
|
80
|
+
(boolean extends IncludeFeatures
|
|
81
|
+
? {features?: string[]}
|
|
82
|
+
: IncludeFeatures extends true
|
|
83
|
+
? {features: string[]}
|
|
84
|
+
: unknown)
|
|
85
|
+
|
|
86
|
+
function resolveOrganizationId(options?: OrganizationOptions<boolean, boolean>) {
|
|
87
|
+
const organizationId = options?.organizationId
|
|
88
|
+
if (!organizationId) {
|
|
89
|
+
throw new Error('An organizationId is required to use the organization API.')
|
|
90
|
+
}
|
|
91
|
+
return organizationId
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeOrganizationOptions(options?: OrganizationOptions<boolean, boolean>) {
|
|
95
|
+
return {
|
|
96
|
+
includeMembers: options?.includeMembers ?? false,
|
|
97
|
+
includeFeatures: options?.includeFeatures ?? false,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @internal */
|
|
102
|
+
export function getOrganizationCacheKey(
|
|
103
|
+
_instance: SanityInstance,
|
|
104
|
+
options?: OrganizationOptions<boolean, boolean>,
|
|
105
|
+
): string {
|
|
106
|
+
const organizationId = resolveOrganizationId(options)
|
|
107
|
+
const {includeMembers, includeFeatures} = normalizeOrganizationOptions(options)
|
|
108
|
+
const membersKey = includeMembers ? ':members' : ''
|
|
109
|
+
const featuresKey = includeFeatures ? ':features' : ''
|
|
110
|
+
return `organization:${organizationId}${membersKey}${featuresKey}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Fetcher for a single organization (`GET /organizations/:id`), on the shared
|
|
115
|
+
* fetcher cache. No pre-seeding from the organizations list: it returns a
|
|
116
|
+
* subset of the detail fields, and a partial seed would serve as fresh for
|
|
117
|
+
* the whole stale window.
|
|
118
|
+
*
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
export const organization = defineFetcher<
|
|
122
|
+
[options?: OrganizationOptions<boolean, boolean>],
|
|
123
|
+
Organization<boolean, boolean>
|
|
124
|
+
>({
|
|
125
|
+
name: 'organization',
|
|
126
|
+
getKey: getOrganizationCacheKey,
|
|
127
|
+
fetch: (instance) => (options) =>
|
|
128
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
129
|
+
switchMap((client) =>
|
|
130
|
+
client.observable.request<Organization<boolean, boolean>>({
|
|
131
|
+
uri: `/organizations/${resolveOrganizationId(options)}`,
|
|
132
|
+
query: buildQuery(normalizeOrganizationOptions(options)),
|
|
133
|
+
tag: 'organizations.get',
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
tags: (data) => [{type: 'organization', id: data.id}],
|
|
138
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {expectTypeOf, test} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {type OrganizationMember} from '../organization/organization'
|
|
4
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
5
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
6
|
+
import {type FetcherSnapshot} from '../store/fetcherStore'
|
|
7
|
+
import {type Organizations, organizations} from './organizations'
|
|
8
|
+
|
|
9
|
+
const instance = {} as SanityInstance
|
|
10
|
+
|
|
11
|
+
test('organizations.resolveState — resolves the wide-boolean list shape', () => {
|
|
12
|
+
expectTypeOf(organizations.resolveState(instance)).resolves.toEqualTypeOf<
|
|
13
|
+
Organizations<boolean, boolean>
|
|
14
|
+
>()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('organizations.getState — exposes the snapshot envelope', () => {
|
|
18
|
+
expectTypeOf(organizations.getState(instance)).toEqualTypeOf<
|
|
19
|
+
StateSource<FetcherSnapshot<Organizations<boolean, boolean>>>
|
|
20
|
+
>()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('organizations — rejects non-boolean flag values', () => {
|
|
24
|
+
// @ts-expect-error includeMembers must be a boolean
|
|
25
|
+
void organizations.resolveState(instance, {includeMembers: 'yes'})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('Organizations — list items expose the documented subset of keys', () => {
|
|
29
|
+
type Keys = keyof Organizations<false, false>[number]
|
|
30
|
+
expectTypeOf<Keys>().toEqualTypeOf<
|
|
31
|
+
| 'id'
|
|
32
|
+
| 'name'
|
|
33
|
+
| 'slug'
|
|
34
|
+
| 'createdAt'
|
|
35
|
+
| 'updatedAt'
|
|
36
|
+
| 'defaultRoleName'
|
|
37
|
+
| 'dashboardStatus'
|
|
38
|
+
| 'aiFeaturesStatus'
|
|
39
|
+
>()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('Organizations<true, false>[number] exposes members[]', () => {
|
|
43
|
+
type Item = Organizations<true, false>[number]
|
|
44
|
+
expectTypeOf<Item['members']>().toEqualTypeOf<OrganizationMember[]>()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('Organizations<false, true>[number] exposes features[]', () => {
|
|
48
|
+
type Item = Organizations<false, true>[number]
|
|
49
|
+
expectTypeOf<Item['features']>().toEqualTypeOf<string[]>()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('Organizations<true, true>[number] exposes both members[] and features[]', () => {
|
|
53
|
+
type Item = Organizations<true, true>[number]
|
|
54
|
+
expectTypeOf<Item['members']>().toEqualTypeOf<OrganizationMember[]>()
|
|
55
|
+
expectTypeOf<Item['features']>().toEqualTypeOf<string[]>()
|
|
56
|
+
})
|