@sanity/sdk 2.18.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 +233 -12
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -1
- package/dist/_chunks-es/_internal.js +1 -0
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +284 -6
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/telemetryManager.js +2 -2
- package/dist/_exports/_internal.d.ts +2 -2
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +3 -1
- package/dist/index.d.ts +599 -185
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +492 -337
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/_exports/_internal.ts +11 -0
- package/src/_exports/index.ts +57 -12
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- 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/getOrganizationVerificationState.test.ts +7 -7
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/document/documentStore.concurrency.test.ts +313 -22
- 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 +18 -84
- package/src/organization/organization.test.ts +11 -11
- package/src/organization/organization.ts +26 -54
- package/src/organizations/organizations.test-d.ts +11 -32
- package/src/organizations/organizations.test.ts +8 -8
- package/src/organizations/organizations.ts +29 -54
- package/src/project/project.test-d.ts +19 -75
- package/src/project/project.test.ts +57 -10
- package/src/project/project.ts +33 -52
- package/src/projects/projects.test-d.ts +15 -21
- package/src/projects/projects.test.ts +8 -8
- package/src/projects/projects.ts +25 -49
- 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/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/includeShape.ts +36 -0
- package/src/utils/setCleanupTimeout.ts +21 -8
- package/dist/_chunks-es/version.js +0 -7
- package/dist/_chunks-es/version.js.map +0 -1
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-rc.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@sanity/bifur-client": "^1.0.0",
|
|
53
|
-
"@sanity/client": "^7.23.
|
|
53
|
+
"@sanity/client": "^7.23.2",
|
|
54
54
|
"@sanity/comlink": "^4.0.1",
|
|
55
55
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
56
56
|
"@sanity/diff-patch": "^6.0.0",
|
|
@@ -60,26 +60,26 @@
|
|
|
60
60
|
"@sanity/message-protocol": "^0.23.0",
|
|
61
61
|
"@sanity/mutate": "^0.18.1",
|
|
62
62
|
"@sanity/telemetry": "^1.1.0",
|
|
63
|
-
"@sanity/types": "^6.
|
|
63
|
+
"@sanity/types": "^6.5.0",
|
|
64
64
|
"groq": "3.88.1-typegen-experimental.0",
|
|
65
|
-
"groq-js": "^
|
|
65
|
+
"groq-js": "^2.0.0",
|
|
66
66
|
"reselect": "^5.1.1",
|
|
67
67
|
"rxjs": "^7.8.2",
|
|
68
68
|
"zustand": "^5.0.13"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@sanity/browserslist-config": "^1.0.5",
|
|
72
|
-
"@sanity/pkg-utils": "^10.
|
|
72
|
+
"@sanity/pkg-utils": "^10.9.2",
|
|
73
73
|
"@types/node": "^24.13.3",
|
|
74
74
|
"@vitest/coverage-v8": "^4.1.10",
|
|
75
75
|
"eslint": "^10.6.0",
|
|
76
76
|
"oxfmt": "^0.58.0",
|
|
77
77
|
"rollup-plugin-visualizer": "^7.0.1",
|
|
78
78
|
"typescript": "^6.0.3",
|
|
79
|
-
"vite": "^8.1.
|
|
79
|
+
"vite": "^8.1.5",
|
|
80
80
|
"vitest": "^4.1.10",
|
|
81
|
-
"@repo/config-test": "0.0.1",
|
|
82
81
|
"@repo/config-eslint": "0.0.0",
|
|
82
|
+
"@repo/config-test": "0.0.1",
|
|
83
83
|
"@repo/package.config": "0.0.1",
|
|
84
84
|
"@repo/tsconfig": "0.0.1",
|
|
85
85
|
"@repo/package.bundle": "3.82.0"
|
|
@@ -9,6 +9,17 @@ export {
|
|
|
9
9
|
export {PREVIEW_PROJECTION} from '../preview/previewConstants'
|
|
10
10
|
export {transformProjectionToPreview} from '../preview/previewProjectionUtils'
|
|
11
11
|
export {getQueryKey, parseQueryKey} from '../query/queryStore' // only used for memoizing in React, not needed for actual functionality
|
|
12
|
+
export {
|
|
13
|
+
type CacheTag,
|
|
14
|
+
type CacheWriter,
|
|
15
|
+
defineFetcher,
|
|
16
|
+
defineMutation,
|
|
17
|
+
type Fetcher,
|
|
18
|
+
type FetcherDefinition,
|
|
19
|
+
type FetcherSnapshot,
|
|
20
|
+
type MutationDefinition,
|
|
21
|
+
type MutationResult,
|
|
22
|
+
} from '../store/fetcherStore'
|
|
12
23
|
export {getTelemetryManager, initTelemetry, trackHookMounted} from '../telemetry/initTelemetry'
|
|
13
24
|
export {getUsersKey, parseUsersKey} from '../users/reducers' // only used for memoizing in React, not needed for actual functionality
|
|
14
25
|
export {createGroqSearchFilter} from '../utils/createGroqSearchFilter'
|
package/src/_exports/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {type SanityProject as _SanityProject} from '@sanity/client'
|
|
|
5
5
|
*/
|
|
6
6
|
export type SanityProject = _SanityProject
|
|
7
7
|
|
|
8
|
+
export {checkPermissions} from '../access/checkPermissions'
|
|
9
|
+
export {type AccessResourceType} from '../access/checkPermissions'
|
|
8
10
|
export type {
|
|
9
11
|
AgentGenerateOptions,
|
|
10
12
|
AgentGenerateResult,
|
|
@@ -24,6 +26,44 @@ export {
|
|
|
24
26
|
agentTransform,
|
|
25
27
|
agentTranslate,
|
|
26
28
|
} from '../agent/agentActions'
|
|
29
|
+
export {
|
|
30
|
+
application,
|
|
31
|
+
applications,
|
|
32
|
+
deleteApplication,
|
|
33
|
+
updateApplication,
|
|
34
|
+
} from '../applications/applications'
|
|
35
|
+
export {
|
|
36
|
+
type Application,
|
|
37
|
+
type ApplicationAccess,
|
|
38
|
+
type ApplicationBase,
|
|
39
|
+
type ApplicationDeployment,
|
|
40
|
+
type ApplicationInclude,
|
|
41
|
+
type ApplicationInterface,
|
|
42
|
+
type ApplicationsOptions,
|
|
43
|
+
type ApplicationsResponse,
|
|
44
|
+
type ApplicationStudioConfig,
|
|
45
|
+
type ApplicationVisibility,
|
|
46
|
+
type ApplicationWorkspace,
|
|
47
|
+
type DeleteApplicationInput,
|
|
48
|
+
type DeletedResult,
|
|
49
|
+
type UpdateApplicationInput,
|
|
50
|
+
} from '../applications/applications'
|
|
51
|
+
export {
|
|
52
|
+
createUserApplication,
|
|
53
|
+
deleteUserApplication,
|
|
54
|
+
updateUserApplication,
|
|
55
|
+
userApplication,
|
|
56
|
+
userApplications,
|
|
57
|
+
} from '../applications/userApplications'
|
|
58
|
+
export {
|
|
59
|
+
type CreateUserApplicationInput,
|
|
60
|
+
type DeleteUserApplicationInput,
|
|
61
|
+
type UpdateUserApplicationInput,
|
|
62
|
+
type UserApplication,
|
|
63
|
+
type UserApplicationDeployment,
|
|
64
|
+
type UserApplicationsOptions,
|
|
65
|
+
type UserApplicationWriteFields,
|
|
66
|
+
} from '../applications/userApplications'
|
|
27
67
|
export {isStudioConfig} from '../auth/authMode'
|
|
28
68
|
export {AuthStateType} from '../auth/authStateType'
|
|
29
69
|
export {
|
|
@@ -179,20 +219,26 @@ export {type DocumentPermissionsResult, type PermissionDeniedReason} from '../do
|
|
|
179
219
|
export {getReleaseDocumentId} from '../document/processActions/releaseUtil'
|
|
180
220
|
export type {FavoriteStatusResponse} from '../favorites/favorites'
|
|
181
221
|
export {getFavoritesState, resolveFavoritesState} from '../favorites/favorites'
|
|
222
|
+
export {installation, installations} from '../installations/installations'
|
|
223
|
+
export {
|
|
224
|
+
type Installation,
|
|
225
|
+
type InstallationAccess,
|
|
226
|
+
type InstallationActiveConfig,
|
|
227
|
+
type InstallationBase,
|
|
228
|
+
type InstallationInclude,
|
|
229
|
+
type InstallationInterface,
|
|
230
|
+
type InstallationsOptions,
|
|
231
|
+
type InstallationsResponse,
|
|
232
|
+
} from '../installations/installations'
|
|
233
|
+
export {organization} from '../organization/organization'
|
|
182
234
|
export {
|
|
183
|
-
getOrganizationState,
|
|
184
235
|
type Organization,
|
|
185
236
|
type OrganizationBase,
|
|
186
237
|
type OrganizationMember,
|
|
187
238
|
type OrganizationOptions,
|
|
188
|
-
resolveOrganization,
|
|
189
239
|
} from '../organization/organization'
|
|
190
|
-
export {
|
|
191
|
-
|
|
192
|
-
type Organizations,
|
|
193
|
-
type OrganizationsOptions,
|
|
194
|
-
resolveOrganizations,
|
|
195
|
-
} from '../organizations/organizations'
|
|
240
|
+
export {organizations} from '../organizations/organizations'
|
|
241
|
+
export {type Organizations, type OrganizationsOptions} from '../organizations/organizations'
|
|
196
242
|
export {getPresence} from '../presence/presenceStore'
|
|
197
243
|
export type {
|
|
198
244
|
DisconnectEvent,
|
|
@@ -214,20 +260,20 @@ export type {
|
|
|
214
260
|
ValuePending,
|
|
215
261
|
} from '../preview/types'
|
|
216
262
|
export {type OrgVerificationResult} from '../project/organizationVerification'
|
|
263
|
+
export {project} from '../project/project'
|
|
217
264
|
export {
|
|
218
|
-
getProjectState,
|
|
219
265
|
type Project,
|
|
220
266
|
type ProjectBase,
|
|
221
267
|
type ProjectMember,
|
|
222
268
|
type ProjectMemberRole,
|
|
223
269
|
type ProjectMetadata,
|
|
224
270
|
type ProjectOptions,
|
|
225
|
-
resolveProject,
|
|
226
271
|
} from '../project/project'
|
|
227
272
|
export {getProjectionState} from '../projection/getProjectionState'
|
|
228
273
|
export {resolveProjection} from '../projection/resolveProjection'
|
|
229
274
|
export {type ProjectionValuePending, type ValidProjection} from '../projection/types'
|
|
230
|
-
export {
|
|
275
|
+
export {projects} from '../projects/projects'
|
|
276
|
+
export {type ProjectsOptions} from '../projects/projects'
|
|
231
277
|
export {
|
|
232
278
|
getQueryKey,
|
|
233
279
|
getQueryState,
|
|
@@ -262,7 +308,6 @@ export {
|
|
|
262
308
|
} from '../users/usersStore'
|
|
263
309
|
export {type FetcherStore, type FetcherStoreState} from '../utils/createFetcherStore'
|
|
264
310
|
export {createGroqSearchFilter} from '../utils/createGroqSearchFilter'
|
|
265
|
-
export {defineIntent, type Intent, type IntentFilter} from '../utils/defineIntent'
|
|
266
311
|
export {getCorsErrorProjectId} from '../utils/getCorsErrorProjectId'
|
|
267
312
|
export {isImportError} from '../utils/isImportError'
|
|
268
313
|
export {CORE_SDK_VERSION} from '../version'
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {of} from 'rxjs'
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, vi} 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 {checkPermissions} from './checkPermissions'
|
|
9
|
+
|
|
10
|
+
vi.mock('../client/clientStore')
|
|
11
|
+
|
|
12
|
+
describe('checkPermissions', () => {
|
|
13
|
+
let instance: SanityInstance
|
|
14
|
+
const request = vi.fn()
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
18
|
+
request.mockReset()
|
|
19
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
20
|
+
observable: of({observable: {request}} as unknown as SanityClient),
|
|
21
|
+
} as StateSource<SanityClient>)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
instance.dispose()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('checks permissions on the global client and unwraps the data envelope', async () => {
|
|
29
|
+
request.mockReturnValue(of({data: {'sanity.project.read': true}}))
|
|
30
|
+
|
|
31
|
+
const result = await checkPermissions.resolveState(instance, 'project', 'p1', [
|
|
32
|
+
'sanity.project.read',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual({'sanity.project.read': true})
|
|
36
|
+
expect(getClientState).toHaveBeenCalledWith(instance, {
|
|
37
|
+
apiVersion: 'v2025-07-11',
|
|
38
|
+
scope: 'global',
|
|
39
|
+
})
|
|
40
|
+
expect(request).toHaveBeenCalledWith({
|
|
41
|
+
uri: '/access/project/p1/user-permissions/me/check',
|
|
42
|
+
query: {permissions: ['sanity.project.read']},
|
|
43
|
+
tag: 'access.check',
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('does not create distinct cache entries for different permission orderings', async () => {
|
|
48
|
+
request.mockReturnValue(of({data: {a: true, b: false}}))
|
|
49
|
+
|
|
50
|
+
await checkPermissions.resolveState(instance, 'organization', 'org1', ['b', 'a'])
|
|
51
|
+
await checkPermissions.resolveState(instance, 'organization', 'org1', ['a', 'b'])
|
|
52
|
+
|
|
53
|
+
expect(request).toHaveBeenCalledTimes(1)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {map, switchMap} from 'rxjs'
|
|
2
|
+
|
|
3
|
+
import {getClientState} from '../client/clientStore'
|
|
4
|
+
import {defineFetcher} from '../store/fetcherStore'
|
|
5
|
+
|
|
6
|
+
const API_VERSION = 'v2025-07-11'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The resource types the Access API can scope a permission check to.
|
|
10
|
+
*
|
|
11
|
+
* @see https://www.sanity.io/docs/http-reference/access-api
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export type AccessResourceType =
|
|
15
|
+
| 'organization'
|
|
16
|
+
| 'project'
|
|
17
|
+
| 'media-library'
|
|
18
|
+
| 'canvas'
|
|
19
|
+
| 'dashboard'
|
|
20
|
+
| 'view'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fetcher checking whether the current user holds the given permissions on a
|
|
24
|
+
* resource (`GET /access/:resourceType/:resourceId/user-permissions/me/check`),
|
|
25
|
+
* on the shared fetcher cache. Resolves to a map of each requested permission
|
|
26
|
+
* to whether the user has it. The key sorts the permission list, so permission
|
|
27
|
+
* order doesn't create distinct cache entries.
|
|
28
|
+
*
|
|
29
|
+
* @see https://www.sanity.io/docs/http-reference/access-api
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export const checkPermissions = defineFetcher<
|
|
33
|
+
[resourceType: AccessResourceType, resourceId: string, permissions: string[]],
|
|
34
|
+
Record<string, boolean>
|
|
35
|
+
>({
|
|
36
|
+
name: 'checkPermissions',
|
|
37
|
+
getKey: (_instance, resourceType, resourceId, permissions) =>
|
|
38
|
+
`${resourceType}:${resourceId}:${[...permissions].sort().join(',')}`,
|
|
39
|
+
fetch: (instance) => (resourceType, resourceId, permissions) =>
|
|
40
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
41
|
+
switchMap((client) =>
|
|
42
|
+
client.observable
|
|
43
|
+
.request<{data: Record<string, boolean>}>({
|
|
44
|
+
uri: `/access/${resourceType}/${resourceId}/user-permissions/me/check`,
|
|
45
|
+
query: {permissions},
|
|
46
|
+
tag: 'access.check',
|
|
47
|
+
})
|
|
48
|
+
.pipe(map((response) => response.data)),
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
tags: (_data, resourceType, resourceId) => [
|
|
52
|
+
{type: 'access', id: `${resourceType}:${resourceId}`},
|
|
53
|
+
],
|
|
54
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
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 {
|
|
7
|
+
type Application,
|
|
8
|
+
application,
|
|
9
|
+
type ApplicationBase,
|
|
10
|
+
type ApplicationDeployment,
|
|
11
|
+
type ApplicationInclude,
|
|
12
|
+
type ApplicationInterface,
|
|
13
|
+
applications,
|
|
14
|
+
type ApplicationsResponse,
|
|
15
|
+
type ApplicationStudioConfig,
|
|
16
|
+
} from './applications'
|
|
17
|
+
|
|
18
|
+
const instance = {} as SanityInstance
|
|
19
|
+
|
|
20
|
+
type ChildKeys = 'config' | 'activeDeployment'
|
|
21
|
+
type DeploymentChildKeys = 'interfaces' | 'workspaces' | 'access'
|
|
22
|
+
|
|
23
|
+
// Whether key `K` of `T` is optional, without an `{}`-based check.
|
|
24
|
+
type KeyModifier<T, K extends keyof T> =
|
|
25
|
+
Pick<T, K> extends Required<Pick<T, K>> ? 'required' : 'optional'
|
|
26
|
+
|
|
27
|
+
test('Application — no includes: only the base shape, no config or activeDeployment', () => {
|
|
28
|
+
expectTypeOf<Application>().toEqualTypeOf<ApplicationBase>()
|
|
29
|
+
expectTypeOf<Application<never>>().toEqualTypeOf<ApplicationBase>()
|
|
30
|
+
expectTypeOf<Extract<keyof Application, ChildKeys>>().toEqualTypeOf<never>()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('Application — config.studio adds a required config.studio', () => {
|
|
34
|
+
expectTypeOf<
|
|
35
|
+
Application<'config.studio'>['config']['studio']
|
|
36
|
+
>().toEqualTypeOf<ApplicationStudioConfig>()
|
|
37
|
+
// mfManifest not requested → not present under config
|
|
38
|
+
expectTypeOf<
|
|
39
|
+
Extract<keyof Application<'config.studio'>['config'], 'mfManifest'>
|
|
40
|
+
>().toEqualTypeOf<never>()
|
|
41
|
+
// requesting only config does not pull in the deployment
|
|
42
|
+
expectTypeOf<
|
|
43
|
+
Extract<keyof Application<'config.studio'>, 'activeDeployment'>
|
|
44
|
+
>().toEqualTypeOf<never>()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('Application — both config tokens merge under a single config', () => {
|
|
48
|
+
type Config = Application<'config.studio' | 'config.mfManifest'>['config']
|
|
49
|
+
expectTypeOf<Config['studio']>().toEqualTypeOf<ApplicationStudioConfig>()
|
|
50
|
+
expectTypeOf<Config['mfManifest']>().toEqualTypeOf<unknown>()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('Application — activeDeployment token adds the deployment summary, no children', () => {
|
|
54
|
+
expectTypeOf<
|
|
55
|
+
Application<'activeDeployment'>['activeDeployment']
|
|
56
|
+
>().toEqualTypeOf<ApplicationDeployment<'activeDeployment'> | null>()
|
|
57
|
+
type Deployment = NonNullable<Application<'activeDeployment'>['activeDeployment']>
|
|
58
|
+
expectTypeOf<Extract<keyof Deployment, DeploymentChildKeys>>().toEqualTypeOf<never>()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('Application — a child token forces the deployment in and adds only that child', () => {
|
|
62
|
+
type Deployment = NonNullable<Application<'interfaces'>['activeDeployment']>
|
|
63
|
+
expectTypeOf<Deployment['interfaces']>().toEqualTypeOf<ApplicationInterface[]>()
|
|
64
|
+
expectTypeOf<Extract<keyof Deployment, 'workspaces' | 'access'>>().toEqualTypeOf<never>()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('Application — a wide include array makes config and activeDeployment optional', () => {
|
|
68
|
+
type Wide = Application<ApplicationInclude>
|
|
69
|
+
expectTypeOf<KeyModifier<Wide, 'activeDeployment'>>().toEqualTypeOf<'optional'>()
|
|
70
|
+
expectTypeOf<KeyModifier<Wide, 'config'>>().toEqualTypeOf<'optional'>()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('applications.resolveState — resolves the wide list envelope', () => {
|
|
74
|
+
expectTypeOf(
|
|
75
|
+
applications.resolveState(instance, {organizationId: 'org_1'}),
|
|
76
|
+
).resolves.toEqualTypeOf<ApplicationsResponse<ApplicationInclude>>()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('application.getState — exposes the snapshot envelope', () => {
|
|
80
|
+
expectTypeOf(application.getState(instance, 'app_1')).toEqualTypeOf<
|
|
81
|
+
StateSource<FetcherSnapshot<Application<ApplicationInclude>>>
|
|
82
|
+
>()
|
|
83
|
+
})
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {of} from 'rxjs'
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, vi} 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 {defineMutation} from '../store/fetcherStore'
|
|
9
|
+
import {application, applications, deleteApplication, updateApplication} from './applications'
|
|
10
|
+
|
|
11
|
+
vi.mock('../client/clientStore')
|
|
12
|
+
|
|
13
|
+
describe('applications', () => {
|
|
14
|
+
let instance: SanityInstance
|
|
15
|
+
const request = vi.fn()
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
19
|
+
request.mockReset()
|
|
20
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
21
|
+
observable: of({observable: {request}} as unknown as SanityClient),
|
|
22
|
+
} as StateSource<SanityClient>)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
instance.dispose()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('lists applications for an organization, serialising include as a sorted CSV', async () => {
|
|
30
|
+
const response = {nextCursor: null, data: [{id: 'app1'}]}
|
|
31
|
+
request.mockReturnValue(of(response))
|
|
32
|
+
|
|
33
|
+
const result = await applications.resolveState(instance, {
|
|
34
|
+
organizationId: 'org1',
|
|
35
|
+
include: ['workspaces', 'access'],
|
|
36
|
+
limit: 'none',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
expect(result).toEqual(response)
|
|
40
|
+
expect(getClientState).toHaveBeenCalledWith(instance, {
|
|
41
|
+
apiVersion: 'vX',
|
|
42
|
+
scope: 'global',
|
|
43
|
+
})
|
|
44
|
+
expect(request).toHaveBeenCalledWith({
|
|
45
|
+
uri: '/applications',
|
|
46
|
+
query: {organizationId: 'org1', include: 'access,workspaces', limit: 'none'},
|
|
47
|
+
tag: 'applications.list',
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('fetches a single application by id', async () => {
|
|
52
|
+
const app = {id: 'app1', title: 'One'}
|
|
53
|
+
request.mockReturnValue(of(app))
|
|
54
|
+
|
|
55
|
+
const result = await application.resolveState(instance, 'app1')
|
|
56
|
+
|
|
57
|
+
expect(result).toEqual(app)
|
|
58
|
+
expect(request).toHaveBeenCalledWith({
|
|
59
|
+
uri: '/applications/app1',
|
|
60
|
+
query: {},
|
|
61
|
+
tag: 'applications.get',
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('refetches the list when a mutation invalidates the LIST tag', async () => {
|
|
66
|
+
let n = 0
|
|
67
|
+
request.mockImplementation(() => of({nextCursor: null, data: [{id: `app-${++n}`}]}))
|
|
68
|
+
|
|
69
|
+
const source = applications.getState(instance, {organizationId: 'org1'})
|
|
70
|
+
const unsubscribe = source.subscribe()
|
|
71
|
+
await vi.waitFor(() => expect(source.getCurrent().status).toBe('success'))
|
|
72
|
+
expect(request).toHaveBeenCalledTimes(1)
|
|
73
|
+
|
|
74
|
+
const install = defineMutation<undefined, {ok: boolean}>({
|
|
75
|
+
name: 'test-install-application',
|
|
76
|
+
mutationFn: () => () => of({ok: true}),
|
|
77
|
+
invalidates: [{type: 'application', id: 'LIST'}],
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const {invalidated} = await install(instance, undefined)
|
|
81
|
+
await invalidated
|
|
82
|
+
|
|
83
|
+
expect(request).toHaveBeenCalledTimes(2)
|
|
84
|
+
expect(source.getCurrent().data).toEqual({nextCursor: null, data: [{id: 'app-2'}]})
|
|
85
|
+
unsubscribe()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('updates an application via PATCH and refetches the invalidated list', async () => {
|
|
89
|
+
request.mockImplementation((opts) => {
|
|
90
|
+
const {method} = opts as {method?: string}
|
|
91
|
+
return method === 'PATCH'
|
|
92
|
+
? of({id: 'app1', title: 'New'})
|
|
93
|
+
: of({nextCursor: null, data: [{id: 'app1'}]})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const source = applications.getState(instance, {organizationId: 'org1'})
|
|
97
|
+
const unsubscribe = source.subscribe()
|
|
98
|
+
await vi.waitFor(() => expect(source.getCurrent().status).toBe('success'))
|
|
99
|
+
|
|
100
|
+
const {data, invalidated} = await updateApplication(instance, {
|
|
101
|
+
applicationId: 'app1',
|
|
102
|
+
title: 'New',
|
|
103
|
+
})
|
|
104
|
+
await invalidated
|
|
105
|
+
|
|
106
|
+
expect(data).toEqual({id: 'app1', title: 'New'})
|
|
107
|
+
expect(request).toHaveBeenCalledWith({
|
|
108
|
+
uri: '/applications/app1',
|
|
109
|
+
method: 'PATCH',
|
|
110
|
+
body: {title: 'New'},
|
|
111
|
+
tag: 'applications.update',
|
|
112
|
+
})
|
|
113
|
+
// initial list GET + PATCH + list refetch after invalidation
|
|
114
|
+
expect(request).toHaveBeenCalledTimes(3)
|
|
115
|
+
unsubscribe()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('deletes an application via DELETE with no body', async () => {
|
|
119
|
+
request.mockReturnValue(of({deleted: true}))
|
|
120
|
+
|
|
121
|
+
const {data} = await deleteApplication(instance, {applicationId: 'app1'})
|
|
122
|
+
|
|
123
|
+
expect(data).toEqual({deleted: true})
|
|
124
|
+
expect(request).toHaveBeenCalledWith({
|
|
125
|
+
uri: '/applications/app1',
|
|
126
|
+
method: 'DELETE',
|
|
127
|
+
tag: 'applications.delete',
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
})
|