@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.2

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 (241) hide show
  1. package/README.md +12 -0
  2. package/dist/_exports/_internal.d.ts +163 -0
  3. package/dist/_exports/_internal.d.ts.map +1 -0
  4. package/dist/_exports/_internal.js +332 -0
  5. package/dist/_exports/_internal.js.map +1 -0
  6. package/dist/_exports/agent.d.ts +65 -0
  7. package/dist/_exports/agent.d.ts.map +1 -0
  8. package/dist/_exports/agent.js +71 -0
  9. package/dist/_exports/agent.js.map +1 -0
  10. package/dist/_exports/comlink.d.ts +118 -0
  11. package/dist/_exports/comlink.d.ts.map +1 -0
  12. package/dist/_exports/comlink.js +69 -0
  13. package/dist/_exports/comlink.js.map +1 -0
  14. package/dist/authStore-CtE9bLHW.js +754 -0
  15. package/dist/authStore-CtE9bLHW.js.map +1 -0
  16. package/dist/authStore-DKFX8fn7.d.ts +3179 -0
  17. package/dist/authStore-DKFX8fn7.d.ts.map +1 -0
  18. package/dist/clientStore-BtE2-q2i.js +90 -0
  19. package/dist/clientStore-BtE2-q2i.js.map +1 -0
  20. package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
  21. package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
  22. package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
  23. package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
  24. package/dist/getEnv-D5RGkQLu.js +8 -0
  25. package/dist/getEnv-D5RGkQLu.js.map +1 -0
  26. package/dist/getNodeState-CR3__B2D.js +67 -0
  27. package/dist/getNodeState-CR3__B2D.js.map +1 -0
  28. package/dist/index.d.ts +3 -2332
  29. package/dist/index.js +4171 -4332
  30. package/dist/index.js.map +1 -1
  31. package/dist/logger-DAtJb5RQ.js +173 -0
  32. package/dist/logger-DAtJb5RQ.js.map +1 -0
  33. package/dist/object-CxXlSVg7.js +83 -0
  34. package/dist/object-CxXlSVg7.js.map +1 -0
  35. package/dist/queryStore-DPf9sN3V.js +1346 -0
  36. package/dist/queryStore-DPf9sN3V.js.map +1 -0
  37. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  38. package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
  39. package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
  40. package/dist/telemetryManager-Dq_qCkeI.js +127 -0
  41. package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
  42. package/dist/version-BChjsi4u.js +9 -0
  43. package/dist/version-BChjsi4u.js.map +1 -0
  44. package/package.json +47 -52
  45. package/src/_exports/_internal.ts +27 -0
  46. package/src/_exports/agent.ts +19 -0
  47. package/src/_exports/comlink.ts +16 -0
  48. package/src/_exports/index.ts +125 -64
  49. package/src/access/checkPermissions.test.ts +55 -0
  50. package/src/access/checkPermissions.ts +54 -0
  51. package/src/agent/agentActions.test.ts +16 -60
  52. package/src/agent/agentActions.ts +25 -38
  53. package/src/applications/applications.test-d.ts +88 -0
  54. package/src/applications/applications.test.ts +130 -0
  55. package/src/applications/applications.ts +367 -0
  56. package/src/auth/authLogger.ts +30 -0
  57. package/src/auth/authStore.test.ts +194 -13
  58. package/src/auth/authStore.ts +58 -26
  59. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  60. package/src/auth/getOrganizationVerificationState.ts +4 -4
  61. package/src/auth/handleAuthCallback.test.ts +35 -1
  62. package/src/auth/handleAuthCallback.ts +25 -6
  63. package/src/auth/logout.test.ts +75 -2
  64. package/src/auth/logout.ts +23 -4
  65. package/src/auth/refreshStampedToken.test.ts +219 -469
  66. package/src/auth/refreshStampedToken.ts +79 -195
  67. package/src/auth/studioAuth.ts +1 -1
  68. package/src/auth/studioModeAuth.ts +2 -1
  69. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  70. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  71. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  72. package/src/client/clientStore.test.ts +62 -37
  73. package/src/client/clientStore.ts +45 -75
  74. package/src/client/liveEvents.test.ts +151 -0
  75. package/src/client/liveEvents.ts +107 -0
  76. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  77. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  78. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  79. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  80. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  81. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  82. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  83. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  84. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  85. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  86. package/src/comlink/node/getNodeState.test.ts +1 -1
  87. package/src/comlink/node/getNodeState.ts +2 -1
  88. package/src/comments/addonDatasetStore.test.ts +215 -0
  89. package/src/comments/addonDatasetStore.ts +249 -0
  90. package/src/comments/buildCommentThreads.test.ts +83 -0
  91. package/src/comments/buildCommentThreads.ts +54 -0
  92. package/src/comments/commentActions.test.ts +540 -0
  93. package/src/comments/commentActions.ts +555 -0
  94. package/src/comments/commentFieldPath.test.ts +40 -0
  95. package/src/comments/commentFieldPath.ts +44 -0
  96. package/src/comments/commentsConstants.ts +80 -0
  97. package/src/comments/commentsStore.test.ts +419 -0
  98. package/src/comments/commentsStore.ts +483 -0
  99. package/src/comments/normalizeComment.test.ts +116 -0
  100. package/src/comments/normalizeComment.ts +45 -0
  101. package/src/comments/observeComments.test.ts +193 -0
  102. package/src/comments/observeComments.ts +113 -0
  103. package/src/comments/reducers.test.ts +277 -0
  104. package/src/comments/reducers.ts +316 -0
  105. package/src/comments/types.ts +249 -0
  106. package/src/comments/weakenReferences.test.ts +47 -0
  107. package/src/comments/weakenReferences.ts +29 -0
  108. package/src/config/__tests__/handles.test.ts +18 -12
  109. package/src/config/handles.ts +25 -7
  110. package/src/config/sanityConfig.ts +40 -82
  111. package/src/datasets/datasets.test.ts +2 -2
  112. package/src/datasets/datasets.ts +10 -4
  113. package/src/document/actions.test.ts +115 -33
  114. package/src/document/actions.ts +212 -15
  115. package/src/document/applyDocumentActions.test.ts +35 -13
  116. package/src/document/applyDocumentActions.ts +30 -9
  117. package/src/document/documentConstants.ts +15 -0
  118. package/src/document/documentStore.concurrency.test.ts +1306 -0
  119. package/src/document/documentStore.test.ts +753 -273
  120. package/src/document/documentStore.ts +197 -75
  121. package/src/document/events.test.ts +57 -2
  122. package/src/document/events.ts +82 -26
  123. package/src/document/listen.test.ts +56 -0
  124. package/src/document/listen.ts +74 -21
  125. package/src/document/listenerEventOperators.test.ts +311 -0
  126. package/src/document/listenerEventOperators.ts +217 -0
  127. package/src/document/patchOperations.test.ts +76 -2
  128. package/src/document/patchOperations.ts +60 -1
  129. package/src/document/permissions.test.ts +107 -60
  130. package/src/document/permissions.ts +44 -19
  131. package/src/document/processActions/create.ts +138 -0
  132. package/src/document/processActions/delete.ts +100 -0
  133. package/src/document/processActions/discard.ts +63 -0
  134. package/src/document/processActions/edit.ts +154 -0
  135. package/src/document/processActions/processActions.ts +218 -0
  136. package/src/document/processActions/publish.ts +120 -0
  137. package/src/document/processActions/releaseArchive.ts +77 -0
  138. package/src/document/processActions/releaseCreate.ts +59 -0
  139. package/src/document/processActions/releaseDelete.ts +65 -0
  140. package/src/document/processActions/releaseEdit.ts +37 -0
  141. package/src/document/processActions/releasePublish.ts +45 -0
  142. package/src/document/processActions/releaseSchedule.ts +87 -0
  143. package/src/document/processActions/releaseUtil.ts +31 -0
  144. package/src/document/processActions/shared.ts +308 -0
  145. package/src/document/processActions/unpublish.ts +85 -0
  146. package/src/document/processActions.test.ts +1129 -121
  147. package/src/document/reducers.test.ts +772 -64
  148. package/src/document/reducers.ts +205 -51
  149. package/src/document/resourceRules.test.ts +178 -0
  150. package/src/document/resourceRules.ts +117 -0
  151. package/src/document/sharedListener.test.ts +16 -14
  152. package/src/document/sharedListener.ts +24 -8
  153. package/src/document/util.test.ts +46 -0
  154. package/src/document/util.ts +50 -0
  155. package/src/favorites/favorites.test.ts +5 -6
  156. package/src/installations/installations.test-d.ts +73 -0
  157. package/src/installations/installations.test.ts +62 -0
  158. package/src/installations/installations.ts +206 -0
  159. package/src/organization/organization.test-d.ts +36 -0
  160. package/src/organization/organization.test.ts +138 -0
  161. package/src/organization/organization.ts +138 -0
  162. package/src/organizations/organizations.test-d.ts +56 -0
  163. package/src/organizations/organizations.test.ts +150 -0
  164. package/src/organizations/organizations.ts +107 -0
  165. package/src/presence/bifurTransport.test.ts +201 -9
  166. package/src/presence/bifurTransport.ts +98 -14
  167. package/src/presence/paths.test.ts +70 -0
  168. package/src/presence/paths.ts +41 -0
  169. package/src/presence/presenceStore.test.ts +915 -24
  170. package/src/presence/presenceStore.ts +576 -104
  171. package/src/presence/types.ts +175 -3
  172. package/src/preview/previewProjectionUtils.test.ts +9 -22
  173. package/src/preview/previewProjectionUtils.ts +7 -3
  174. package/src/preview/types.ts +0 -15
  175. package/src/project/project.test-d.ts +37 -0
  176. package/src/project/project.test.ts +157 -12
  177. package/src/project/project.ts +138 -25
  178. package/src/projection/getProjectionState.test.ts +18 -20
  179. package/src/projection/getProjectionState.ts +23 -3
  180. package/src/projection/projectionQuery.ts +5 -5
  181. package/src/projection/projectionStore.test.ts +1 -9
  182. package/src/projection/resolveProjection.test.ts +1 -2
  183. package/src/projection/resolveProjection.ts +16 -0
  184. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  185. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  186. package/src/projects/projects.test-d.ts +32 -0
  187. package/src/projects/projects.test.ts +108 -42
  188. package/src/projects/projects.ts +63 -27
  189. package/src/query/queryStore.test.ts +259 -123
  190. package/src/query/queryStore.ts +85 -69
  191. package/src/query/reducers.test.ts +0 -42
  192. package/src/query/reducers.ts +2 -18
  193. package/src/releases/getPerspectiveState.test.ts +17 -23
  194. package/src/releases/getPerspectiveState.ts +8 -20
  195. package/src/releases/observeReleases.test.ts +127 -0
  196. package/src/releases/observeReleases.ts +102 -0
  197. package/src/releases/releasesStore.test.ts +100 -53
  198. package/src/releases/releasesStore.ts +69 -44
  199. package/src/releases/utils/sortReleases.test.ts +38 -2
  200. package/src/releases/utils/sortReleases.ts +15 -10
  201. package/src/store/createActionBinder.test.ts +109 -90
  202. package/src/store/createActionBinder.ts +36 -28
  203. package/src/store/createSanityInstance.test.ts +15 -13
  204. package/src/store/createSanityInstance.ts +14 -2
  205. package/src/store/createStateSourceAction.test.ts +64 -2
  206. package/src/store/createStateSourceAction.ts +38 -43
  207. package/src/store/createStoreInstance.test.ts +4 -2
  208. package/src/store/fetcherStore.test-d.ts +121 -0
  209. package/src/store/fetcherStore.test.ts +299 -0
  210. package/src/store/fetcherStore.ts +801 -0
  211. package/src/telemetry/environment.test.ts +119 -0
  212. package/src/telemetry/environment.ts +92 -0
  213. package/src/telemetry/events.ts +42 -0
  214. package/src/telemetry/initTelemetry.test.ts +432 -0
  215. package/src/telemetry/initTelemetry.ts +218 -0
  216. package/src/telemetry/telemetryManager.test.ts +327 -0
  217. package/src/telemetry/telemetryManager.ts +199 -0
  218. package/src/users/reducers.test.ts +3 -1
  219. package/src/users/reducers.ts +5 -6
  220. package/src/users/types.ts +4 -4
  221. package/src/users/usersStore.test.ts +16 -12
  222. package/src/users/usersStore.ts +5 -1
  223. package/src/utils/buildQuery.test.ts +14 -0
  224. package/src/utils/buildQuery.ts +17 -0
  225. package/src/utils/createFetcherStore.test.ts +7 -5
  226. package/src/utils/createFetcherStore.ts +8 -5
  227. package/src/utils/getStagingApiHost.test.ts +21 -0
  228. package/src/utils/getStagingApiHost.ts +14 -0
  229. package/src/utils/ids.test.ts +1 -29
  230. package/src/utils/ids.ts +0 -10
  231. package/src/utils/includeShape.ts +36 -0
  232. package/src/utils/isImportError.test.ts +72 -0
  233. package/src/utils/isImportError.ts +34 -0
  234. package/src/utils/logger.test.ts +12 -0
  235. package/src/utils/logger.ts +8 -3
  236. package/src/utils/object.test.ts +95 -0
  237. package/src/utils/object.ts +142 -0
  238. package/src/utils/setCleanupTimeout.ts +37 -0
  239. package/src/document/processActions.ts +0 -707
  240. package/src/utils/defineIntent.test.ts +0 -477
  241. package/src/utils/defineIntent.ts +0 -244
@@ -0,0 +1,206 @@
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
+ /** Stable, immutable identity of the installed singleton, distinct from `slug`. */
83
+ name: string
84
+ /** Qualified, globally-unique handle of the installed singleton (e.g. `sanity/<name>`). */
85
+ reference: string
86
+ slug: string | null
87
+ }
88
+ }
89
+
90
+ /**
91
+ * An active singleton installation as returned by the Installations API
92
+ * (`/installations`). `activeConfig`, `access` and `interfaces` are top-level
93
+ * fields, each present only when the matching {@link InstallationInclude} token
94
+ * was requested.
95
+ *
96
+ * @see https://www.sanity.io/docs/http-reference/applications-api
97
+ * @public
98
+ */
99
+ export type Installation<Include extends InstallationInclude = never> = InstallationBase &
100
+ Included<
101
+ InstallationInclude,
102
+ 'activeConfig',
103
+ Include,
104
+ {activeConfig: InstallationActiveConfig | null}
105
+ > &
106
+ Included<InstallationInclude, 'access', Include, {access: InstallationAccess[]}> &
107
+ Included<InstallationInclude, 'interfaces', Include, {interfaces: InstallationInterface[]}>
108
+
109
+ /**
110
+ * Options for listing an organization's installations.
111
+ *
112
+ * @see https://www.sanity.io/docs/http-reference/applications-api
113
+ * @public
114
+ */
115
+ export interface InstallationsOptions<Include extends InstallationInclude = InstallationInclude> {
116
+ organizationId: string
117
+ include?: Include[]
118
+ /** Page size (1-100, default 50), or `'none'` to disable pagination */
119
+ limit?: number | 'none'
120
+ /** Cursor from a previous response's `nextCursor` */
121
+ cursor?: string
122
+ }
123
+
124
+ /**
125
+ * The cursor-paginated envelope returned by `GET /installations`.
126
+ *
127
+ * @see https://www.sanity.io/docs/http-reference/applications-api
128
+ * @public
129
+ */
130
+ export interface InstallationsResponse<Include extends InstallationInclude = never> {
131
+ nextCursor: string | null
132
+ data: Installation<Include>[]
133
+ }
134
+
135
+ /** The API takes `include` as a comma-separated list; sorted so key and query agree */
136
+ function serializeInclude(include: InstallationInclude[] | undefined): string | undefined {
137
+ return include?.length ? [...include].sort().join(',') : undefined
138
+ }
139
+
140
+ /**
141
+ * Fetcher for an organization's installations (`GET /installations`), on the
142
+ * shared fetcher cache.
143
+ *
144
+ * @see https://www.sanity.io/docs/http-reference/applications-api
145
+ * @internal
146
+ */
147
+ export const installations = defineFetcher<
148
+ [options: InstallationsOptions],
149
+ InstallationsResponse<InstallationInclude>
150
+ >({
151
+ name: 'installations',
152
+ getKey: (_instance, options) =>
153
+ [
154
+ options.organizationId,
155
+ serializeInclude(options.include) ?? '',
156
+ options.limit ?? '',
157
+ options.cursor ?? '',
158
+ ].join(':'),
159
+ fetch: (instance) => (options) =>
160
+ getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
161
+ switchMap((client) =>
162
+ client.observable.request<InstallationsResponse<InstallationInclude>>({
163
+ uri: '/installations',
164
+ query: buildQuery({
165
+ organizationId: options.organizationId,
166
+ include: serializeInclude(options.include),
167
+ limit: options.limit,
168
+ cursor: options.cursor,
169
+ }),
170
+ tag: 'installations.list',
171
+ }),
172
+ ),
173
+ ),
174
+ tags: (data) => [
175
+ {type: 'installation', id: 'LIST'},
176
+ ...data.data.map((installation) => ({type: 'installation', id: installation.id})),
177
+ ],
178
+ })
179
+
180
+ /**
181
+ * Fetcher for a single installation (`GET /installations/:id`), on the shared
182
+ * fetcher cache. No pre-seeding from {@link installations}: its list keys
183
+ * include the organization, which this fetcher's params can't reconstruct.
184
+ *
185
+ * @see https://www.sanity.io/docs/http-reference/applications-api
186
+ * @internal
187
+ */
188
+ export const installation = defineFetcher<
189
+ [installationId: string, options?: {include?: InstallationInclude[]}],
190
+ Installation<InstallationInclude>
191
+ >({
192
+ name: 'installation',
193
+ getKey: (_instance, installationId, options) =>
194
+ `${installationId}:${serializeInclude(options?.include) ?? ''}`,
195
+ fetch: (instance) => (installationId, options) =>
196
+ getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
197
+ switchMap((client) =>
198
+ client.observable.request<Installation<InstallationInclude>>({
199
+ uri: `/installations/${installationId}`,
200
+ query: buildQuery({include: serializeInclude(options?.include)}),
201
+ tag: 'installations.get',
202
+ }),
203
+ ),
204
+ ),
205
+ tags: (data) => [{type: 'installation', id: data.id}],
206
+ })
@@ -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
+ })