@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.
Files changed (53) hide show
  1. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +233 -12
  2. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -1
  3. package/dist/_chunks-es/_internal.js +1 -0
  4. package/dist/_chunks-es/_internal.js.map +1 -1
  5. package/dist/_chunks-es/createGroqSearchFilter.js +284 -6
  6. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  7. package/dist/_chunks-es/telemetryManager.js +2 -2
  8. package/dist/_exports/_internal.d.ts +2 -2
  9. package/dist/_exports/_internal.d.ts.map +1 -1
  10. package/dist/_exports/_internal.js +3 -1
  11. package/dist/index.d.ts +599 -185
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +492 -337
  14. package/dist/index.js.map +1 -1
  15. package/package.json +7 -7
  16. package/src/_exports/_internal.ts +11 -0
  17. package/src/_exports/index.ts +57 -12
  18. package/src/access/checkPermissions.test.ts +55 -0
  19. package/src/access/checkPermissions.ts +54 -0
  20. package/src/applications/applications.test-d.ts +83 -0
  21. package/src/applications/applications.test.ts +130 -0
  22. package/src/applications/applications.ts +364 -0
  23. package/src/applications/userApplications.test.ts +126 -0
  24. package/src/applications/userApplications.ts +261 -0
  25. package/src/auth/getOrganizationVerificationState.test.ts +7 -7
  26. package/src/auth/getOrganizationVerificationState.ts +4 -4
  27. package/src/document/documentStore.concurrency.test.ts +313 -22
  28. package/src/installations/installations.test-d.ts +68 -0
  29. package/src/installations/installations.test.ts +62 -0
  30. package/src/installations/installations.ts +202 -0
  31. package/src/organization/organization.test-d.ts +18 -84
  32. package/src/organization/organization.test.ts +11 -11
  33. package/src/organization/organization.ts +26 -54
  34. package/src/organizations/organizations.test-d.ts +11 -32
  35. package/src/organizations/organizations.test.ts +8 -8
  36. package/src/organizations/organizations.ts +29 -54
  37. package/src/project/project.test-d.ts +19 -75
  38. package/src/project/project.test.ts +57 -10
  39. package/src/project/project.ts +33 -52
  40. package/src/projects/projects.test-d.ts +15 -21
  41. package/src/projects/projects.test.ts +8 -8
  42. package/src/projects/projects.ts +25 -49
  43. package/src/store/fetcherStore.test-d.ts +121 -0
  44. package/src/store/fetcherStore.test.ts +299 -0
  45. package/src/store/fetcherStore.ts +801 -0
  46. package/src/utils/buildQuery.test.ts +14 -0
  47. package/src/utils/buildQuery.ts +17 -0
  48. package/src/utils/includeShape.ts +36 -0
  49. package/src/utils/setCleanupTimeout.ts +21 -8
  50. package/dist/_chunks-es/version.js +0 -7
  51. package/dist/_chunks-es/version.js.map +0 -1
  52. package/src/utils/defineIntent.test.ts +0 -477
  53. package/src/utils/defineIntent.ts +0 -244
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "2.18.0",
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.1",
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.2.0",
63
+ "@sanity/types": "^6.5.0",
64
64
  "groq": "3.88.1-typegen-experimental.0",
65
- "groq-js": "^1.30.3",
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.5.8",
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.4",
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'
@@ -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
- getOrganizationsState,
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 {getProjectsState, type ProjectsOptions, resolveProjects} from '../projects/projects'
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
+ })