@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.
Files changed (197) hide show
  1. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +1147 -0
  2. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
  3. package/dist/_chunks-es/_internal.js +134 -0
  4. package/dist/_chunks-es/_internal.js.map +1 -0
  5. package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
  6. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
  7. package/dist/_chunks-es/telemetryManager.js +93 -0
  8. package/dist/_chunks-es/telemetryManager.js.map +1 -0
  9. package/dist/_exports/_internal.d.ts +95 -0
  10. package/dist/_exports/_internal.d.ts.map +1 -0
  11. package/dist/_exports/_internal.js +24 -0
  12. package/dist/_exports/_internal.js.map +1 -0
  13. package/dist/index.d.ts +1079 -1076
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +1967 -2302
  16. package/dist/index.js.map +1 -1
  17. package/package.json +48 -47
  18. package/src/_exports/_internal.ts +26 -0
  19. package/src/_exports/index.ts +120 -11
  20. package/src/access/checkPermissions.test.ts +55 -0
  21. package/src/access/checkPermissions.ts +54 -0
  22. package/src/agent/agentActions.test.ts +16 -60
  23. package/src/agent/agentActions.ts +25 -38
  24. package/src/applications/applications.test-d.ts +83 -0
  25. package/src/applications/applications.test.ts +130 -0
  26. package/src/applications/applications.ts +364 -0
  27. package/src/applications/userApplications.test.ts +126 -0
  28. package/src/applications/userApplications.ts +261 -0
  29. package/src/auth/authLogger.ts +30 -0
  30. package/src/auth/authMode.test.ts +25 -0
  31. package/src/auth/authMode.ts +6 -3
  32. package/src/auth/authStore.test.ts +194 -13
  33. package/src/auth/authStore.ts +59 -26
  34. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  35. package/src/auth/getOrganizationVerificationState.ts +4 -4
  36. package/src/auth/handleAuthCallback.test.ts +35 -1
  37. package/src/auth/handleAuthCallback.ts +25 -6
  38. package/src/auth/logout.test.ts +75 -2
  39. package/src/auth/logout.ts +23 -4
  40. package/src/auth/refreshStampedToken.test.ts +219 -469
  41. package/src/auth/refreshStampedToken.ts +79 -195
  42. package/src/auth/studioAuth.ts +1 -1
  43. package/src/auth/studioModeAuth.ts +2 -1
  44. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  45. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  46. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  47. package/src/client/clientStore.test.ts +62 -37
  48. package/src/client/clientStore.ts +45 -75
  49. package/src/client/liveEvents.test.ts +151 -0
  50. package/src/client/liveEvents.ts +107 -0
  51. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  52. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  53. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  54. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  55. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  56. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  57. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  58. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  59. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  60. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  61. package/src/comlink/node/getNodeState.test.ts +1 -1
  62. package/src/comlink/node/getNodeState.ts +2 -1
  63. package/src/config/__tests__/handles.test.ts +18 -12
  64. package/src/config/handles.ts +25 -7
  65. package/src/config/sanityConfig.ts +109 -79
  66. package/src/datasets/datasets.test.ts +2 -2
  67. package/src/datasets/datasets.ts +10 -4
  68. package/src/document/actions.test.ts +115 -33
  69. package/src/document/actions.ts +212 -15
  70. package/src/document/applyDocumentActions.test.ts +38 -12
  71. package/src/document/applyDocumentActions.ts +30 -9
  72. package/src/document/documentConstants.ts +7 -0
  73. package/src/document/documentStore.concurrency.test.ts +1306 -0
  74. package/src/document/documentStore.test.ts +673 -278
  75. package/src/document/documentStore.ts +162 -71
  76. package/src/document/events.test.ts +57 -2
  77. package/src/document/events.ts +82 -26
  78. package/src/document/listen.test.ts +56 -0
  79. package/src/document/listen.ts +74 -21
  80. package/src/document/listenerEventOperators.test.ts +311 -0
  81. package/src/document/listenerEventOperators.ts +217 -0
  82. package/src/document/patchOperations.test.ts +49 -2
  83. package/src/document/patchOperations.ts +50 -0
  84. package/src/document/permissions.test.ts +105 -60
  85. package/src/document/permissions.ts +44 -19
  86. package/src/document/processActions/create.ts +138 -0
  87. package/src/document/processActions/delete.ts +100 -0
  88. package/src/document/processActions/discard.ts +63 -0
  89. package/src/document/processActions/edit.ts +154 -0
  90. package/src/document/processActions/processActions.ts +218 -0
  91. package/src/document/processActions/publish.ts +120 -0
  92. package/src/document/processActions/releaseArchive.ts +77 -0
  93. package/src/document/processActions/releaseCreate.ts +59 -0
  94. package/src/document/processActions/releaseDelete.ts +65 -0
  95. package/src/document/processActions/releaseEdit.ts +37 -0
  96. package/src/document/processActions/releasePublish.ts +45 -0
  97. package/src/document/processActions/releaseSchedule.ts +87 -0
  98. package/src/document/processActions/releaseUtil.ts +31 -0
  99. package/src/document/processActions/shared.ts +308 -0
  100. package/src/document/processActions/unpublish.ts +85 -0
  101. package/src/document/processActions.test.ts +1129 -121
  102. package/src/document/reducers.test.ts +639 -64
  103. package/src/document/reducers.ts +187 -48
  104. package/src/document/resourceRules.test.ts +178 -0
  105. package/src/document/resourceRules.ts +117 -0
  106. package/src/document/sharedListener.test.ts +16 -14
  107. package/src/document/sharedListener.ts +24 -8
  108. package/src/document/util.ts +14 -0
  109. package/src/favorites/favorites.test.ts +5 -6
  110. package/src/installations/installations.test-d.ts +68 -0
  111. package/src/installations/installations.test.ts +62 -0
  112. package/src/installations/installations.ts +202 -0
  113. package/src/organization/organization.test-d.ts +36 -0
  114. package/src/organization/organization.test.ts +138 -0
  115. package/src/organization/organization.ts +138 -0
  116. package/src/organizations/organizations.test-d.ts +56 -0
  117. package/src/organizations/organizations.test.ts +150 -0
  118. package/src/organizations/organizations.ts +107 -0
  119. package/src/presence/bifurTransport.test.ts +46 -6
  120. package/src/presence/bifurTransport.ts +19 -2
  121. package/src/presence/presenceStore.test.ts +94 -23
  122. package/src/presence/presenceStore.ts +85 -29
  123. package/src/preview/getPreviewState.test.ts +137 -0
  124. package/src/preview/getPreviewState.ts +69 -0
  125. package/src/preview/previewProjectionUtils.test.ts +9 -22
  126. package/src/preview/previewProjectionUtils.ts +7 -3
  127. package/src/preview/resolvePreview.test.ts +64 -0
  128. package/src/preview/resolvePreview.ts +43 -0
  129. package/src/preview/types.ts +10 -0
  130. package/src/project/project.test-d.ts +37 -0
  131. package/src/project/project.test.ts +157 -12
  132. package/src/project/project.ts +138 -25
  133. package/src/projection/getProjectionState.test.ts +18 -20
  134. package/src/projection/getProjectionState.ts +23 -3
  135. package/src/projection/projectionQuery.ts +5 -5
  136. package/src/projection/projectionStore.test.ts +1 -1
  137. package/src/projection/resolveProjection.test.ts +1 -2
  138. package/src/projection/resolveProjection.ts +16 -0
  139. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  140. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  141. package/src/projection/types.ts +8 -0
  142. package/src/projects/projects.test-d.ts +32 -0
  143. package/src/projects/projects.test.ts +108 -42
  144. package/src/projects/projects.ts +63 -27
  145. package/src/query/queryStore.test.ts +259 -123
  146. package/src/query/queryStore.ts +78 -63
  147. package/src/query/reducers.test.ts +0 -42
  148. package/src/query/reducers.ts +2 -18
  149. package/src/releases/getPerspectiveState.test.ts +17 -23
  150. package/src/releases/getPerspectiveState.ts +8 -20
  151. package/src/releases/observeReleases.test.ts +127 -0
  152. package/src/releases/observeReleases.ts +102 -0
  153. package/src/releases/releasesStore.test.ts +100 -53
  154. package/src/releases/releasesStore.ts +69 -44
  155. package/src/releases/utils/sortReleases.test.ts +38 -2
  156. package/src/releases/utils/sortReleases.ts +15 -10
  157. package/src/store/createActionBinder.test.ts +87 -90
  158. package/src/store/createActionBinder.ts +79 -25
  159. package/src/store/createSanityInstance.test.ts +83 -13
  160. package/src/store/createSanityInstance.ts +77 -2
  161. package/src/store/createStateSourceAction.test.ts +64 -2
  162. package/src/store/createStateSourceAction.ts +39 -44
  163. package/src/store/createStoreInstance.test.ts +4 -2
  164. package/src/store/fetcherStore.test-d.ts +121 -0
  165. package/src/store/fetcherStore.test.ts +299 -0
  166. package/src/store/fetcherStore.ts +801 -0
  167. package/src/telemetry/environment.test.ts +119 -0
  168. package/src/telemetry/environment.ts +92 -0
  169. package/src/telemetry/events.ts +42 -0
  170. package/src/telemetry/initTelemetry.test.ts +432 -0
  171. package/src/telemetry/initTelemetry.ts +218 -0
  172. package/src/telemetry/telemetryManager.test.ts +327 -0
  173. package/src/telemetry/telemetryManager.ts +199 -0
  174. package/src/users/reducers.test.ts +6 -1
  175. package/src/users/reducers.ts +5 -6
  176. package/src/users/types.ts +4 -4
  177. package/src/users/usersStore.test.ts +16 -12
  178. package/src/users/usersStore.ts +5 -1
  179. package/src/utils/buildQuery.test.ts +14 -0
  180. package/src/utils/buildQuery.ts +17 -0
  181. package/src/utils/createFetcherStore.test.ts +7 -5
  182. package/src/utils/createFetcherStore.ts +8 -5
  183. package/src/utils/getStagingApiHost.test.ts +21 -0
  184. package/src/utils/getStagingApiHost.ts +14 -0
  185. package/src/utils/ids.test.ts +1 -29
  186. package/src/utils/ids.ts +0 -10
  187. package/src/utils/includeShape.ts +36 -0
  188. package/src/utils/isImportError.test.ts +72 -0
  189. package/src/utils/isImportError.ts +34 -0
  190. package/src/utils/logger.test.ts +12 -0
  191. package/src/utils/logger.ts +8 -3
  192. package/src/utils/object.test.ts +95 -0
  193. package/src/utils/object.ts +142 -0
  194. package/src/utils/setCleanupTimeout.ts +37 -0
  195. package/src/document/processActions.ts +0 -707
  196. package/src/utils/defineIntent.test.ts +0 -477
  197. package/src/utils/defineIntent.ts +0 -244
@@ -7,14 +7,6 @@ import {type SanityInstance} from '../store/createSanityInstance'
7
7
 
8
8
  const API_VERSION = 'vX'
9
9
 
10
- /**
11
- * Options that all agent actions accept for targeting a specific resource.
12
- * @alpha
13
- */
14
- export interface AgentResourceOptions {
15
- resource: DocumentResource
16
- }
17
-
18
10
  /** @alpha */
19
11
  export type AgentGenerateOptions = Parameters<
20
12
  SanityClient['observable']['agent']['action']['generate']
@@ -66,13 +58,12 @@ export type AgentPatchResult = Awaited<ReturnType<SanityClient['agent']['action'
66
58
  */
67
59
  export function agentGenerate(
68
60
  instance: SanityInstance,
69
- options: AgentGenerateOptions & AgentResourceOptions,
61
+ options: AgentGenerateOptions,
62
+ resource?: DocumentResource,
70
63
  ): AgentGenerateResult {
71
- const {resource, ...agentOptions} = options
72
- return getClientState(instance, {
73
- apiVersion: API_VERSION,
74
- resource,
75
- }).observable.pipe(switchMap((client) => client.observable.agent.action.generate(agentOptions)))
64
+ return getClientState(instance, {apiVersion: API_VERSION, resource}).observable.pipe(
65
+ switchMap((client) => client.observable.agent.action.generate(options)),
66
+ )
76
67
  }
77
68
 
78
69
  /**
@@ -84,13 +75,12 @@ export function agentGenerate(
84
75
  */
85
76
  export function agentTransform(
86
77
  instance: SanityInstance,
87
- options: AgentTransformOptions & AgentResourceOptions,
78
+ options: AgentTransformOptions,
79
+ resource?: DocumentResource,
88
80
  ): AgentTransformResult {
89
- const {resource, ...agentOptions} = options
90
- return getClientState(instance, {
91
- apiVersion: API_VERSION,
92
- resource,
93
- }).observable.pipe(switchMap((client) => client.observable.agent.action.transform(agentOptions)))
81
+ return getClientState(instance, {apiVersion: API_VERSION, resource}).observable.pipe(
82
+ switchMap((client) => client.observable.agent.action.transform(options)),
83
+ )
94
84
  }
95
85
 
96
86
  /**
@@ -102,13 +92,12 @@ export function agentTransform(
102
92
  */
103
93
  export function agentTranslate(
104
94
  instance: SanityInstance,
105
- options: AgentTranslateOptions & AgentResourceOptions,
95
+ options: AgentTranslateOptions,
96
+ resource?: DocumentResource,
106
97
  ): AgentTranslateResult {
107
- const {resource, ...agentOptions} = options
108
- return getClientState(instance, {
109
- apiVersion: API_VERSION,
110
- resource,
111
- }).observable.pipe(switchMap((client) => client.observable.agent.action.translate(agentOptions)))
98
+ return getClientState(instance, {apiVersion: API_VERSION, resource}).observable.pipe(
99
+ switchMap((client) => client.observable.agent.action.translate(options)),
100
+ )
112
101
  }
113
102
 
114
103
  /**
@@ -120,13 +109,12 @@ export function agentTranslate(
120
109
  */
121
110
  export function agentPrompt(
122
111
  instance: SanityInstance,
123
- options: AgentPromptOptions & AgentResourceOptions,
112
+ options: AgentPromptOptions,
113
+ resource?: DocumentResource,
124
114
  ): Observable<AgentPromptResult> {
125
- const {resource, ...agentOptions} = options
126
- return getClientState(instance, {
127
- apiVersion: API_VERSION,
128
- resource,
129
- }).observable.pipe(switchMap((client) => from(client.agent.action.prompt(agentOptions))))
115
+ return getClientState(instance, {apiVersion: API_VERSION, resource}).observable.pipe(
116
+ switchMap((client) => from(client.agent.action.prompt(options))),
117
+ )
130
118
  }
131
119
 
132
120
  /**
@@ -138,11 +126,10 @@ export function agentPrompt(
138
126
  */
139
127
  export function agentPatch(
140
128
  instance: SanityInstance,
141
- options: AgentPatchOptions & AgentResourceOptions,
129
+ options: AgentPatchOptions,
130
+ resource?: DocumentResource,
142
131
  ): Observable<AgentPatchResult> {
143
- const {resource, ...agentOptions} = options
144
- return getClientState(instance, {
145
- apiVersion: API_VERSION,
146
- resource,
147
- }).observable.pipe(switchMap((client) => from(client.agent.action.patch(agentOptions))))
132
+ return getClientState(instance, {apiVersion: API_VERSION, resource}).observable.pipe(
133
+ switchMap((client) => from(client.agent.action.patch(options))),
134
+ )
148
135
  }
@@ -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
+ })
@@ -0,0 +1,364 @@
1
+ import {switchMap} from 'rxjs'
2
+
3
+ import {getClientState} from '../client/clientStore'
4
+ import {defineFetcher, defineMutation} from '../store/fetcherStore'
5
+ import {buildQuery} from '../utils/buildQuery'
6
+ import {type Included, type IncludesAny} 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 Applications API can embed in a response. `interfaces`,
13
+ * `workspaces` and `access` are children of the active deployment and are
14
+ * embedded under {@link Application.activeDeployment}; `config.studio` and
15
+ * `config.mfManifest` are embedded under {@link Application.config}.
16
+ *
17
+ * @see https://www.sanity.io/docs/http-reference/applications-api
18
+ * @public
19
+ */
20
+ export type ApplicationInclude =
21
+ | 'interfaces'
22
+ | 'workspaces'
23
+ | 'access'
24
+ | 'config.studio'
25
+ | 'config.mfManifest'
26
+ | 'activeDeployment'
27
+
28
+ /**
29
+ * An interface exposed by a deployment, embedded when `interfaces` is included.
30
+ *
31
+ * @see https://www.sanity.io/docs/http-reference/applications-api
32
+ * @public
33
+ */
34
+ export interface ApplicationInterface {
35
+ id: string
36
+ type: 'app' | 'worker' | 'asset_source' | 'panel'
37
+ name: string
38
+ title: string
39
+ version: string
40
+ /** Module federation module ID; resolved from the host mf-manifest at runtime */
41
+ moduleId: string
42
+ metadata: {group?: string; priority?: number} | null
43
+ }
44
+
45
+ /**
46
+ * A studio workspace of a deployment, embedded when `workspaces` is included.
47
+ * Studio applications only.
48
+ *
49
+ * @see https://www.sanity.io/docs/http-reference/applications-api
50
+ * @public
51
+ */
52
+ export interface ApplicationWorkspace {
53
+ id: string
54
+ name: string
55
+ title: string | null
56
+ subtitle: string | null
57
+ projectId: string
58
+ dataset: string
59
+ schemaDescriptorId: string | null
60
+ basePath: string | null
61
+ /** Sanitized SVG icon markup */
62
+ icon: string | null
63
+ }
64
+
65
+ /**
66
+ * A resource an application accesses, embedded when `access` is included.
67
+ *
68
+ * @see https://www.sanity.io/docs/http-reference/applications-api
69
+ * @public
70
+ */
71
+ export interface ApplicationAccess {
72
+ id: string
73
+ resourceType: 'datasets' | 'canvases' | 'dashboards' | 'media-libraries'
74
+ resourceId: string
75
+ }
76
+
77
+ /**
78
+ * Studio configuration, embedded under `config` when `config.studio` is included.
79
+ *
80
+ * @see https://www.sanity.io/docs/http-reference/applications-api
81
+ * @public
82
+ */
83
+ export interface ApplicationStudioConfig {
84
+ projectId: string
85
+ /** Release channel (`next`, `stable`, `latest`) or a pinned semver; null if unset */
86
+ autoUpdatingVersion: string | null
87
+ }
88
+
89
+ /**
90
+ * The active deployment as embedded under {@link Application.activeDeployment}.
91
+ * Its `interfaces`, `workspaces` and `access` children are each present only
92
+ * when the matching {@link ApplicationInclude} token was requested. `workspaces`
93
+ * is omitted (not `null`) for non-studio applications even when requested.
94
+ *
95
+ * @see https://www.sanity.io/docs/http-reference/applications-api
96
+ * @public
97
+ */
98
+ export type ApplicationDeployment<Include extends ApplicationInclude = never> = {
99
+ id: string
100
+ applicationId: string
101
+ size: number | null
102
+ version: string | null
103
+ isAutoUpdating: boolean | null
104
+ isActiveDeployment: boolean
105
+ deployedBy: string | null
106
+ createdAt: string
107
+ updatedAt: string
108
+ } & Included<ApplicationInclude, 'interfaces', Include, {interfaces: ApplicationInterface[]}> &
109
+ Included<ApplicationInclude, 'workspaces', Include, {workspaces?: ApplicationWorkspace[]}> &
110
+ Included<ApplicationInclude, 'access', Include, {access: ApplicationAccess[]}>
111
+
112
+ /**
113
+ * The always-present fields of an org-owned application returned by the
114
+ * Applications API (`/applications`), before any {@link ApplicationInclude}.
115
+ *
116
+ * @see https://www.sanity.io/docs/http-reference/applications-api
117
+ * @public
118
+ */
119
+ export interface ApplicationBase {
120
+ id: string
121
+ type: 'studio' | 'coreApp'
122
+ title: string
123
+ icon: string | null
124
+ isSingleton: boolean
125
+ visibility: 'default' | 'unlisted' | 'disabled'
126
+ slug: string | null
127
+ externalUrl: string | null
128
+ organizationId: string
129
+ createdAt: string
130
+ updatedAt: string
131
+ }
132
+
133
+ // Requesting any of these tokens forces the deployment summary into the response.
134
+ type DeploymentToken = 'activeDeployment' | 'interfaces' | 'workspaces' | 'access'
135
+
136
+ // `config` is present when either config child was requested; each child is
137
+ // gated on its own token.
138
+ type ApplicationConfigShape<Include extends ApplicationInclude> = Included<
139
+ ApplicationInclude,
140
+ 'config.studio',
141
+ Include,
142
+ {config: {studio: ApplicationStudioConfig}}
143
+ > &
144
+ Included<ApplicationInclude, 'config.mfManifest', Include, {config: {mfManifest: unknown}}>
145
+
146
+ // `activeDeployment` is present when any deployment-related token was requested;
147
+ // a wide include array makes it optional.
148
+ type ApplicationDeploymentShape<Include extends ApplicationInclude> = [ApplicationInclude] extends [
149
+ Include,
150
+ ]
151
+ ? {activeDeployment?: ApplicationDeployment<Include> | null}
152
+ : IncludesAny<Include, DeploymentToken> extends true
153
+ ? {activeDeployment: ApplicationDeployment<Include> | null}
154
+ : unknown
155
+
156
+ /**
157
+ * An org-owned application as returned by the Applications API
158
+ * (`/applications`). `config` and `activeDeployment` (with its
159
+ * `interfaces`/`workspaces`/`access` children) are present only when the
160
+ * corresponding {@link ApplicationInclude} tokens were requested.
161
+ *
162
+ * @see https://www.sanity.io/docs/http-reference/applications-api
163
+ * @public
164
+ */
165
+ export type Application<Include extends ApplicationInclude = never> = ApplicationBase &
166
+ ApplicationConfigShape<Include> &
167
+ ApplicationDeploymentShape<Include>
168
+
169
+ /**
170
+ * Options for listing an organization's applications.
171
+ *
172
+ * @see https://www.sanity.io/docs/http-reference/applications-api
173
+ * @public
174
+ */
175
+ export interface ApplicationsOptions<Include extends ApplicationInclude = ApplicationInclude> {
176
+ organizationId: string
177
+ /** Filter by application type */
178
+ type?: string
179
+ include?: Include[]
180
+ /** Page size (1-100, default 50), or `'none'` to disable pagination */
181
+ limit?: number | 'none'
182
+ /** Cursor from a previous response's `nextCursor` */
183
+ cursor?: string
184
+ }
185
+
186
+ /**
187
+ * The cursor-paginated envelope returned by `GET /applications`.
188
+ *
189
+ * @see https://www.sanity.io/docs/http-reference/applications-api
190
+ * @public
191
+ */
192
+ export interface ApplicationsResponse<Include extends ApplicationInclude = never> {
193
+ nextCursor: string | null
194
+ data: Application<Include>[]
195
+ }
196
+
197
+ /** The API takes `include` as a comma-separated list; sorted so key and query agree */
198
+ function serializeInclude(include: ApplicationInclude[] | undefined): string | undefined {
199
+ return include?.length ? [...include].sort().join(',') : undefined
200
+ }
201
+
202
+ /**
203
+ * Fetcher for an organization's applications (`GET /applications`), on the
204
+ * shared fetcher cache.
205
+ *
206
+ * @see https://www.sanity.io/docs/http-reference/applications-api
207
+ * @internal
208
+ */
209
+ export const applications = defineFetcher<
210
+ [options: ApplicationsOptions],
211
+ ApplicationsResponse<ApplicationInclude>
212
+ >({
213
+ name: 'applications',
214
+ getKey: (_instance, options) =>
215
+ [
216
+ options.organizationId,
217
+ options.type ?? '',
218
+ serializeInclude(options.include) ?? '',
219
+ options.limit ?? '',
220
+ options.cursor ?? '',
221
+ ].join(':'),
222
+ fetch: (instance) => (options) =>
223
+ getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
224
+ switchMap((client) =>
225
+ client.observable.request<ApplicationsResponse<ApplicationInclude>>({
226
+ uri: '/applications',
227
+ query: buildQuery({
228
+ organizationId: options.organizationId,
229
+ type: options.type,
230
+ include: serializeInclude(options.include),
231
+ limit: options.limit,
232
+ cursor: options.cursor,
233
+ }),
234
+ tag: 'applications.list',
235
+ }),
236
+ ),
237
+ ),
238
+ tags: (data) => [
239
+ {type: 'application', id: 'LIST'},
240
+ ...data.data.map((app) => ({type: 'application', id: app.id})),
241
+ ],
242
+ })
243
+
244
+ /**
245
+ * Fetcher for a single application (`GET /applications/:id`), on the shared
246
+ * fetcher cache. No pre-seeding from {@link applications}: its list keys
247
+ * include the organization, which this fetcher's params can't reconstruct.
248
+ *
249
+ * @see https://www.sanity.io/docs/http-reference/applications-api
250
+ * @internal
251
+ */
252
+ export const application = defineFetcher<
253
+ [applicationId: string, options?: {include?: ApplicationInclude[]}],
254
+ Application<ApplicationInclude>
255
+ >({
256
+ name: 'application',
257
+ getKey: (_instance, applicationId, options) =>
258
+ `${applicationId}:${serializeInclude(options?.include) ?? ''}`,
259
+ fetch: (instance) => (applicationId, options) =>
260
+ getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
261
+ switchMap((client) =>
262
+ client.observable.request<Application<ApplicationInclude>>({
263
+ uri: `/applications/${applicationId}`,
264
+ query: buildQuery({include: serializeInclude(options?.include)}),
265
+ tag: 'applications.get',
266
+ }),
267
+ ),
268
+ ),
269
+ tags: (data) => [{type: 'application', id: data.id}],
270
+ })
271
+
272
+ /**
273
+ * Confirmation returned by the API's soft-delete endpoints.
274
+ *
275
+ * @see https://www.sanity.io/docs/http-reference/applications-api
276
+ * @internal
277
+ */
278
+ export interface DeletedResult {
279
+ deleted: boolean
280
+ }
281
+
282
+ /**
283
+ * Dashboard visibility for an application. The same set of values backs a user
284
+ * application's `dashboardStatus`.
285
+ *
286
+ * @see https://www.sanity.io/docs/http-reference/applications-api
287
+ * @internal
288
+ */
289
+ export type ApplicationVisibility = ApplicationBase['visibility']
290
+
291
+ /**
292
+ * Mutable properties of an application (`PATCH /applications/:id`) — the
293
+ * writable subset of {@link ApplicationBase}, each optional, keyed by id.
294
+ *
295
+ * @see https://www.sanity.io/docs/http-reference/applications-api
296
+ * @internal
297
+ */
298
+ export type UpdateApplicationInput = {applicationId: string} & Partial<
299
+ Pick<ApplicationBase, 'title' | 'icon' | 'visibility'>
300
+ >
301
+
302
+ /**
303
+ * Mutation to update an application (`PATCH /applications/:id`). Invalidates the
304
+ * updated application and the list so active entries reconverge on server truth.
305
+ *
306
+ * @see https://www.sanity.io/docs/http-reference/applications-api
307
+ * @internal
308
+ */
309
+ export const updateApplication = defineMutation<UpdateApplicationInput, Application>({
310
+ name: 'updateApplication',
311
+ mutationFn:
312
+ (instance) =>
313
+ ({applicationId, ...body}) =>
314
+ getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
315
+ switchMap((client) =>
316
+ client.observable.request<Application>({
317
+ uri: `/applications/${applicationId}`,
318
+ method: 'PATCH',
319
+ body,
320
+ tag: 'applications.update',
321
+ }),
322
+ ),
323
+ ),
324
+ invalidates: (_result, {applicationId}) => [
325
+ {type: 'application', id: 'LIST'},
326
+ {type: 'application', id: applicationId},
327
+ ],
328
+ })
329
+
330
+ /**
331
+ * Input for {@link deleteApplication}.
332
+ *
333
+ * @internal
334
+ */
335
+ export interface DeleteApplicationInput {
336
+ applicationId: string
337
+ }
338
+
339
+ /**
340
+ * Mutation to soft-delete an application (`DELETE /applications/:id`).
341
+ * Invalidates the deleted application and the list.
342
+ *
343
+ * @see https://www.sanity.io/docs/http-reference/applications-api
344
+ * @internal
345
+ */
346
+ export const deleteApplication = defineMutation<DeleteApplicationInput, DeletedResult>({
347
+ name: 'deleteApplication',
348
+ mutationFn:
349
+ (instance) =>
350
+ ({applicationId}) =>
351
+ getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
352
+ switchMap((client) =>
353
+ client.observable.request<DeletedResult>({
354
+ uri: `/applications/${applicationId}`,
355
+ method: 'DELETE',
356
+ tag: 'applications.delete',
357
+ }),
358
+ ),
359
+ ),
360
+ invalidates: (_result, {applicationId}) => [
361
+ {type: 'application', id: 'LIST'},
362
+ {type: 'application', id: applicationId},
363
+ ],
364
+ })