@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
@@ -0,0 +1,199 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {
3
+ type ConsentStatus,
4
+ createBatchedStore,
5
+ type SessionId,
6
+ type TelemetryEvent,
7
+ type TelemetryLogger,
8
+ type TelemetryStore,
9
+ } from '@sanity/telemetry'
10
+
11
+ import {createLogger} from '../utils/logger'
12
+ import {CORE_SDK_VERSION} from '../version'
13
+ import {type TelemetryEnvironment} from './environment'
14
+ import {SDKError, SDKHookMounted, SDKSessionEnded, SDKSessionStarted} from './events'
15
+
16
+ const FLUSH_INTERVAL_MS = 30_000
17
+ const CONSENT_TAG = 'telemetry-consent.sdk'
18
+ const BATCH_TAG = 'telemetry.batch'
19
+
20
+ const log = createLogger('telemetry')
21
+
22
+ /**
23
+ * Manages telemetry for a single SDK instance.
24
+ *
25
+ * Wraps `@sanity/telemetry`'s batched store with SDK-specific concerns:
26
+ * consent caching, session lifecycle events, and hook usage tracking.
27
+ * The `environment` is captured at construction time and recorded in
28
+ * the event context so downstream pipelines can distinguish dev and
29
+ * production sessions.
30
+ *
31
+ * @internal
32
+ */
33
+ export interface TelemetryManager {
34
+ /**
35
+ * Eagerly resolve and cache the user's consent status.
36
+ * Returns true only when the user has explicitly opted in (`granted`).
37
+ * Call this before logging any events to avoid buffering events that
38
+ * will be dropped on the first flush.
39
+ */
40
+ checkConsent(): Promise<boolean>
41
+
42
+ /** Log a "SDK Session Started" event */
43
+ logSessionStarted(data: {projectId: string; perspective: string; authMethod: string}): void
44
+
45
+ /** Log a "SDK Hook Mounted" event (deduplicated per hook name) */
46
+ logHookFirstUsed(hookName: string): void
47
+
48
+ /** Log a "SDK Error" event */
49
+ logError(errorType: string, hookName: string): void
50
+
51
+ /** Log a "SDK Session Ended" event and tear down the store */
52
+ endSession(): void
53
+
54
+ /** Tear down the store without logging a session-end event */
55
+ dispose(): void
56
+
57
+ /** The set of hook names used during this session */
58
+ readonly hooksUsed: ReadonlySet<string>
59
+ }
60
+
61
+ interface TelemetryManagerOptions {
62
+ sessionId: string
63
+ getClient: () => SanityClient
64
+ projectId: string
65
+ environment: TelemetryEnvironment
66
+ }
67
+
68
+ /**
69
+ * Creates a telemetry manager for a single SDK instance session.
70
+ *
71
+ * The manager initializes a `createBatchedStore` from `@sanity/telemetry`,
72
+ * caches the consent check for the lifetime of the session, and provides
73
+ * typed methods for each SDK telemetry event.
74
+ *
75
+ * @internal
76
+ */
77
+ export function createTelemetryManager(options: TelemetryManagerOptions): TelemetryManager {
78
+ const {sessionId, getClient, projectId, environment} = options
79
+ const startedAt = Date.now()
80
+ const emittedHooks = new Set<string>()
81
+
82
+ let cachedConsent: {status: ConsentStatus} | null = null
83
+
84
+ const resolveConsent = async (): Promise<{status: ConsentStatus}> => {
85
+ if (cachedConsent) return cachedConsent
86
+ try {
87
+ const client = getClient()
88
+ const result = await client.request<{status: ConsentStatus}>({
89
+ uri: '/intake/telemetry-status',
90
+ tag: CONSENT_TAG,
91
+ })
92
+ cachedConsent = result
93
+ } catch {
94
+ cachedConsent = {status: 'undetermined'}
95
+ }
96
+ return cachedConsent
97
+ }
98
+
99
+ const enrichBatch = (batch: TelemetryEvent[]) =>
100
+ batch.map((event) => {
101
+ // Trace events (`trace.start` / `trace.log` / `trace.error` /
102
+ // `trace.complete`) arrive with their own caller-provided `context`.
103
+ // Log / userProperties events don't. Narrow via `in` so `event`
104
+ // stays typed as `TelemetryEvent`, then merge so we don't drop the
105
+ // trace context. SDK-owned fields below win on conflict.
106
+ const existing =
107
+ 'context' in event ? (event.context as Record<string, unknown> | undefined) : undefined
108
+ return {
109
+ ...event,
110
+ context: {
111
+ ...existing,
112
+ version: CORE_SDK_VERSION,
113
+ environment,
114
+ origin: typeof window !== 'undefined' ? window.location.origin : 'node',
115
+ },
116
+ }
117
+ })
118
+
119
+ const sendEvents = async (batch: TelemetryEvent[]): Promise<unknown> => {
120
+ const client = getClient()
121
+ log.debug('sending event batch', {batchSize: batch.length, environment})
122
+ return client.request({
123
+ uri: '/intake/batch',
124
+ method: 'POST',
125
+ body: {projectId, batch: enrichBatch(batch)},
126
+ tag: BATCH_TAG,
127
+ })
128
+ }
129
+
130
+ const store: TelemetryStore<Record<string, unknown>> = createBatchedStore(
131
+ sessionId as SessionId,
132
+ {
133
+ flushInterval: FLUSH_INTERVAL_MS,
134
+ resolveConsent,
135
+ sendEvents,
136
+ },
137
+ )
138
+
139
+ const logger: TelemetryLogger<Record<string, unknown>> = store.logger
140
+
141
+ return {
142
+ async checkConsent() {
143
+ const {status} = await resolveConsent()
144
+ return status === 'granted'
145
+ },
146
+
147
+ logSessionStarted(data) {
148
+ log.debug('event: SDK Session Started', {
149
+ projectId: data.projectId,
150
+ perspective: data.perspective,
151
+ authMethod: data.authMethod,
152
+ version: CORE_SDK_VERSION,
153
+ environment,
154
+ })
155
+ logger.log(SDKSessionStarted, {
156
+ version: CORE_SDK_VERSION,
157
+ ...data,
158
+ })
159
+ },
160
+
161
+ logHookFirstUsed(hookName: string) {
162
+ if (emittedHooks.has(hookName)) return
163
+ emittedHooks.add(hookName)
164
+ log.debug('event: SDK Hook Mounted', {hookName})
165
+ logger.log(SDKHookMounted, {hookName})
166
+ },
167
+
168
+ logError(errorType: string, hookName: string) {
169
+ log.debug('event: SDK Error', {errorType, hookName})
170
+ logger.log(SDKError, {errorType, hookName})
171
+ },
172
+
173
+ endSession() {
174
+ const durationSeconds = Math.round((Date.now() - startedAt) / 1000)
175
+ log.debug('event: SDK Session Ended', {
176
+ durationSeconds,
177
+ hooksUsed: [...emittedHooks],
178
+ environment,
179
+ })
180
+ logger.log(SDKSessionEnded, {
181
+ durationSeconds,
182
+ hooksUsed: [...emittedHooks],
183
+ })
184
+
185
+ store.flush().catch(() => {
186
+ // Best-effort flush on dispose; swallow errors
187
+ })
188
+ store.end()
189
+ },
190
+
191
+ dispose() {
192
+ store.end()
193
+ },
194
+
195
+ get hooksUsed(): ReadonlySet<string> {
196
+ return emittedHooks
197
+ },
198
+ }
199
+ }
@@ -19,10 +19,15 @@ describe('Users Reducers', () => {
19
19
  // Mock SanityInstance for testing
20
20
  const mockInstance: SanityInstance = {
21
21
  instanceId: 'test-instance-id',
22
- config: {},
22
+ config: {
23
+ projectId: 'test-project-id',
24
+ },
23
25
  isDisposed: () => false,
24
26
  dispose: () => {},
25
27
  onDispose: () => () => {},
28
+ getParent: () => undefined,
29
+ createChild: (_config) => mockInstance,
30
+ match: () => undefined,
26
31
  }
27
32
 
28
33
  const sampleOptions: GetUsersOptions = {
@@ -1,17 +1,16 @@
1
- import {omit} from 'lodash-es'
2
-
3
1
  import {type SanityInstance} from '../store/createSanityInstance'
2
+ import {omitProperty} from '../utils/object'
4
3
  import {type GetUsersOptions, type SanityUserResponse, type UsersStoreState} from './types'
5
4
  import {DEFAULT_USERS_BATCH_SIZE} from './usersConstants'
6
5
 
7
6
  /** @internal */
8
7
  export const getUsersKey = (
9
- _instance: SanityInstance,
8
+ instance: SanityInstance,
10
9
  {
11
10
  resourceType,
12
11
  organizationId,
13
12
  batchSize = DEFAULT_USERS_BATCH_SIZE,
14
- projectId,
13
+ projectId = instance.config.projectId,
15
14
  userId,
16
15
  }: GetUsersOptions = {},
17
16
  ): string =>
@@ -48,7 +47,7 @@ export const removeSubscription =
48
47
  const group = prev.users[key]
49
48
  if (!group) return prev
50
49
  const subscriptions = group.subscriptions.filter((id) => id !== subscriptionId)
51
- if (!subscriptions.length) return {...prev, users: omit(prev.users, key)}
50
+ if (!subscriptions.length) return {...prev, users: omitProperty(prev.users, key)}
52
51
  return {...prev, users: {...prev.users, [key]: {...group, subscriptions}}}
53
52
  }
54
53
 
@@ -83,7 +82,7 @@ export const cancelRequest =
83
82
  const group = prev.users[key]
84
83
  if (!group) return prev
85
84
  if (group.subscriptions.length) return prev
86
- return {...prev, users: omit(prev.users, key)}
85
+ return {...prev, users: omitProperty(prev.users, key)}
87
86
  }
88
87
 
89
88
  export const initializeRequest =
@@ -1,3 +1,5 @@
1
+ import {type ProjectHandle} from '../config/sanityConfig'
2
+
1
3
  /**
2
4
  * @public
3
5
  */
@@ -40,8 +42,7 @@ export interface UserProfile {
40
42
  /**
41
43
  * @public
42
44
  */
43
- export interface GetUsersOptions {
44
- projectId?: string
45
+ export interface GetUsersOptions extends ProjectHandle {
45
46
  resourceType?: 'organization' | 'project'
46
47
  batchSize?: number
47
48
  organizationId?: string
@@ -87,8 +88,7 @@ export interface ResolveUsersOptions extends GetUsersOptions {
87
88
  /**
88
89
  * @public
89
90
  */
90
- export interface GetUserOptions {
91
- projectId?: string
91
+ export interface GetUserOptions extends ProjectHandle {
92
92
  userId: string
93
93
  resourceType?: 'organization' | 'project'
94
94
  organizationId?: string
@@ -87,7 +87,7 @@ describe('usersStore', () => {
87
87
  })
88
88
 
89
89
  it('initializes users state and cleans up after unsubscribe', async () => {
90
- const instance = createSanityInstance()
90
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
91
91
  const state = getUsersState(instance, {
92
92
  resourceType: 'project',
93
93
  projectId: 'project1',
@@ -122,7 +122,7 @@ describe('usersStore', () => {
122
122
  })
123
123
 
124
124
  it('maintains state when multiple subscribers exist', async () => {
125
- const instance = createSanityInstance()
125
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
126
126
  const state = getUsersState(instance, {
127
127
  resourceType: 'project',
128
128
  projectId: 'project1',
@@ -165,7 +165,7 @@ describe('usersStore', () => {
165
165
  })
166
166
 
167
167
  it('resolveUsers works without affecting subscriber cleanup', async () => {
168
- const instance = createSanityInstance()
168
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
169
169
  const options: GetUsersOptions = {resourceType: 'project', projectId: 'project1'}
170
170
 
171
171
  const state = getUsersState(instance, options)
@@ -198,7 +198,7 @@ describe('usersStore', () => {
198
198
  })
199
199
 
200
200
  it('handles abort signal in resolveUsers', async () => {
201
- const instance = createSanityInstance()
201
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
202
202
  const options: GetUsersOptions = {resourceType: 'project', projectId: 'project1'}
203
203
  const abortController = new AbortController()
204
204
 
@@ -221,7 +221,7 @@ describe('usersStore', () => {
221
221
  })
222
222
 
223
223
  it('loads more users when loadMoreUsers is called', async () => {
224
- const instance = createSanityInstance()
224
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
225
225
  const options: GetUsersOptions = {resourceType: 'project', projectId: 'project1'}
226
226
 
227
227
  // First response has nextCursor
@@ -291,7 +291,7 @@ describe('usersStore', () => {
291
291
  })
292
292
 
293
293
  it('throws error when loadMoreUsers is called without initial data', async () => {
294
- const instance = createSanityInstance()
294
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
295
295
 
296
296
  // Expect loadMoreUsers to throw when no data is loaded
297
297
  await expect(
@@ -302,7 +302,7 @@ describe('usersStore', () => {
302
302
  })
303
303
 
304
304
  it('throws error when loadMoreUsers is called with no more data available', async () => {
305
- const instance = createSanityInstance()
305
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
306
306
  const options: GetUsersOptions = {resourceType: 'project', projectId: 'project1'}
307
307
 
308
308
  // Response with no nextCursor
@@ -325,7 +325,7 @@ describe('usersStore', () => {
325
325
  })
326
326
 
327
327
  it('handles errors in users fetching', async () => {
328
- const instance = createSanityInstance()
328
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
329
329
  const errorMessage = 'Failed to fetch users'
330
330
 
331
331
  // Override request to simulate error
@@ -351,7 +351,7 @@ describe('usersStore', () => {
351
351
  })
352
352
 
353
353
  it('delays users state removal after unsubscribe', async () => {
354
- const instance = createSanityInstance()
354
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
355
355
  const options: GetUsersOptions = {resourceType: 'project', projectId: 'project1'}
356
356
  const state = getUsersState(instance, options)
357
357
  const unsubscribe = state.subscribe()
@@ -370,7 +370,7 @@ describe('usersStore', () => {
370
370
  })
371
371
 
372
372
  it('preserves users state if a new subscriber subscribes before cleanup delay', async () => {
373
- const instance = createSanityInstance()
373
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
374
374
  const state = getUsersState(instance, {
375
375
  resourceType: 'project',
376
376
  projectId: 'project1',
@@ -412,7 +412,7 @@ describe('usersStore', () => {
412
412
  })
413
413
 
414
414
  it('fetches a single user with a project-scoped ID', async () => {
415
- const instance = createSanityInstance()
415
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
416
416
  const projectUserId = 'p12345'
417
417
  const mockProjectUser: PatchedSanityUserFromClient = {
418
418
  id: projectUserId,
@@ -451,6 +451,7 @@ describe('usersStore', () => {
451
451
  expect(specificRequest).toHaveBeenCalledWith({
452
452
  method: 'GET',
453
453
  uri: `/users/${projectUserId}`,
454
+ tag: 'users.get',
454
455
  })
455
456
 
456
457
  const expectedUser: SanityUser = {
@@ -476,7 +477,10 @@ describe('usersStore', () => {
476
477
  })
477
478
 
478
479
  it('fetches a single user with a global-scoped ID', async () => {
479
- const instance = createSanityInstance()
480
+ const instance = createSanityInstance({
481
+ projectId: 'test',
482
+ dataset: 'test',
483
+ })
480
484
  const globalUserId = 'g12345'
481
485
  const mockGlobalUser: SanityUser = {
482
486
  sanityUserId: globalUserId,
@@ -31,6 +31,7 @@ import {createStateSourceAction, type SelectorContext} from '../store/createStat
31
31
  import {type StoreState} from '../store/createStoreState'
32
32
  import {defineStore, type StoreContext} from '../store/defineStore'
33
33
  import {insecureRandomId} from '../utils/ids'
34
+ import {setCleanupTimeout} from '../utils/setCleanupTimeout'
34
35
  import {
35
36
  addSubscription,
36
37
  cancelRequest,
@@ -130,6 +131,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
130
131
  .request<PatchedSanityUserFromClient>({
131
132
  method: 'GET',
132
133
  uri: `/users/${userId}`,
134
+ tag: 'users.get',
133
135
  })
134
136
  .pipe(
135
137
  map((user) => {
@@ -183,6 +185,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
183
185
  .request<SanityUser | SanityUserResponse>({
184
186
  method: 'GET',
185
187
  uri: `access/${resourceType}/${resourceId}/users/${userId}`,
188
+ tag: 'users.get',
186
189
  })
187
190
  .pipe(
188
191
  map((response) => {
@@ -251,6 +254,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
251
254
  client.observable.request<SanityUserResponse>({
252
255
  method: 'GET',
253
256
  uri: `access/${resource.type}/${resource.id}/users`,
257
+ tag: 'users.list',
254
258
  query: cursor
255
259
  ? {nextCursor: cursor, limit: batchSize.toString()}
256
260
  : {limit: batchSize.toString()},
@@ -310,7 +314,7 @@ export const getUsersState = bindActionGlobally(
310
314
  const key = getUsersKey(instance, options)
311
315
  state.set('addSubscription', addSubscription(subscriptionId, key))
312
316
  return () => {
313
- setTimeout(
317
+ setCleanupTimeout(
314
318
  () => state.set('removeSubscription', removeSubscription(subscriptionId, key)),
315
319
  USERS_STATE_CLEAR_DELAY,
316
320
  )
@@ -0,0 +1,14 @@
1
+ import {describe, expect, it} from 'vitest'
2
+
3
+ import {buildQuery} from './buildQuery'
4
+
5
+ describe('buildQuery', () => {
6
+ it('stringifies scalars, drops undefined, and passes string arrays through', () => {
7
+ expect(buildQuery({a: true, b: 0, c: 'x', d: undefined, e: ['p', 'q']})).toEqual({
8
+ a: 'true',
9
+ b: '0',
10
+ c: 'x',
11
+ e: ['p', 'q'],
12
+ })
13
+ })
14
+ })
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Serialises store options into a `@sanity/client` request query object.
3
+ * Scalars are stringified and `undefined` values dropped; string arrays pass
4
+ * through untouched so the client serialises them as repeated keys
5
+ * (`permissions=a&permissions=b`).
6
+ *
7
+ * @internal
8
+ */
9
+ export function buildQuery(
10
+ params: Record<string, string | number | boolean | string[] | undefined>,
11
+ ): Record<string, string | string[]> {
12
+ return Object.fromEntries(
13
+ Object.entries(params)
14
+ .filter(([, value]) => value !== undefined)
15
+ .map(([key, value]) => [key, Array.isArray(value) ? value : String(value)]),
16
+ )
17
+ }
@@ -9,7 +9,7 @@ describe('createFetcherStore', () => {
9
9
  let instance: SanityInstance
10
10
 
11
11
  beforeEach(() => {
12
- instance = createSanityInstance()
12
+ instance = createSanityInstance({projectId: 'p', dataset: 'd'})
13
13
  vi.useRealTimers()
14
14
  })
15
15
 
@@ -127,8 +127,9 @@ describe('createFetcherStore', () => {
127
127
  expect(fetchSpy).toHaveBeenCalledTimes(1)
128
128
 
129
129
  // Second subscription within throttle interval
130
- const sub2 = stateSource.subscribe()
131
- await firstValueFrom(stateSource.observable)
130
+ const stateSource2 = store.getState(instance, 1)
131
+ const sub2 = stateSource2.subscribe()
132
+ await firstValueFrom(stateSource2.observable)
132
133
  expect(fetchSpy).toHaveBeenCalledTimes(1)
133
134
 
134
135
  // Advance past throttle interval
@@ -136,8 +137,9 @@ describe('createFetcherStore', () => {
136
137
  await vi.advanceTimersByTimeAsync(1000)
137
138
 
138
139
  // Third subscription after throttle interval
139
- const sub3 = stateSource.subscribe()
140
- await firstValueFrom(stateSource.observable)
140
+ const stateSource3 = store.getState(instance, 1)
141
+ const sub3 = stateSource3.subscribe()
142
+ await firstValueFrom(stateSource3.observable)
141
143
  expect(fetchSpy).toHaveBeenCalledTimes(2)
142
144
 
143
145
  sub1()
@@ -1,4 +1,3 @@
1
- import {omit} from 'lodash-es'
2
1
  import {asapScheduler, EMPTY, firstValueFrom, from, Observable} from 'rxjs'
3
2
  import {
4
3
  catchError,
@@ -23,6 +22,8 @@ import {
23
22
  } from '../store/createStateSourceAction'
24
23
  import {defineStore, type StoreContext} from '../store/defineStore'
25
24
  import {insecureRandomId} from '../utils/ids'
25
+ import {omitProperty} from '../utils/object'
26
+ import {setCleanupTimeout} from './setCleanupTimeout'
26
27
 
27
28
  interface CreateFetcherStoreOptions<TParams extends unknown[], TData> {
28
29
  /**
@@ -182,8 +183,10 @@ export function createFetcherStore<TParams extends unknown[], TData>({
182
183
  stateByParams: {
183
184
  ...prev.stateByParams,
184
185
  [entry.key]: {
185
- ...omit(entry, 'error'),
186
- ...omit(prev.stateByParams[entry.key], 'error'),
186
+ ...omitProperty(entry, 'error'),
187
+ ...(prev.stateByParams[entry.key]
188
+ ? omitProperty(prev.stateByParams[entry.key], 'error')
189
+ : {}),
187
190
  data,
188
191
  },
189
192
  },
@@ -247,14 +250,14 @@ export function createFetcherStore<TParams extends unknown[], TData>({
247
250
  }))
248
251
 
249
252
  return () => {
250
- setTimeout(() => {
253
+ setCleanupTimeout(() => {
251
254
  state.set('removeSubscription', (prev: FetcherStoreState<TParams, TData>) => {
252
255
  const entry = prev.stateByParams[key]
253
256
  if (!entry) return prev
254
257
 
255
258
  const newSubs = (entry.subscriptions || []).filter((id) => id !== subscriptionId)
256
259
  if (newSubs.length === 0) {
257
- return {stateByParams: omit(prev.stateByParams, key)}
260
+ return {stateByParams: omitProperty(prev.stateByParams, key)}
258
261
  }
259
262
 
260
263
  return {
@@ -0,0 +1,21 @@
1
+ import {describe, expect, it, vi} from 'vitest'
2
+
3
+ import {getStagingApiHost} from './getStagingApiHost'
4
+
5
+ describe('getStagingApiHost', () => {
6
+ it('returns staging host when __SANITY_STAGING__ is true', () => {
7
+ vi.stubGlobal('__SANITY_STAGING__', true)
8
+ expect(getStagingApiHost()).toBe('https://api.sanity.work')
9
+ vi.unstubAllGlobals()
10
+ })
11
+
12
+ it('returns undefined when __SANITY_STAGING__ is false', () => {
13
+ vi.stubGlobal('__SANITY_STAGING__', false)
14
+ expect(getStagingApiHost()).toBeUndefined()
15
+ vi.unstubAllGlobals()
16
+ })
17
+
18
+ it('returns undefined when __SANITY_STAGING__ is not defined', () => {
19
+ expect(getStagingApiHost()).toBeUndefined()
20
+ })
21
+ })
@@ -0,0 +1,14 @@
1
+ declare const __SANITY_STAGING__: boolean | undefined
2
+
3
+ /**
4
+ * Returns the staging API host if the `__SANITY_STAGING__` build-time flag is
5
+ * set to `true` (mirroring how Sanity Studio detects staging builds).
6
+ *
7
+ * @internal
8
+ */
9
+ export function getStagingApiHost(): string | undefined {
10
+ if (typeof __SANITY_STAGING__ !== 'undefined' && __SANITY_STAGING__ === true) {
11
+ return 'https://api.sanity.work'
12
+ }
13
+ return undefined
14
+ }
@@ -1,34 +1,6 @@
1
1
  import {describe, expect, it} from 'vitest'
2
2
 
3
- import {getDraftId, getPublishedId, insecureRandomId} from './ids'
4
-
5
- describe('getDraftId', () => {
6
- it('should add drafts prefix to non-draft ids', () => {
7
- expect(getDraftId('abc123')).toBe('drafts.abc123')
8
- })
9
-
10
- it('should not modify ids that already have drafts prefix', () => {
11
- expect(getDraftId('drafts.abc123')).toBe('drafts.abc123')
12
- })
13
-
14
- it('should handle empty string', () => {
15
- expect(getDraftId('')).toBe('drafts.')
16
- })
17
- })
18
-
19
- describe('getPublishedId', () => {
20
- it('should remove drafts prefix from draft ids', () => {
21
- expect(getPublishedId('drafts.abc123')).toBe('abc123')
22
- })
23
-
24
- it('should not modify ids that dont have drafts prefix', () => {
25
- expect(getPublishedId('abc123')).toBe('abc123')
26
- })
27
-
28
- it('should handle empty string', () => {
29
- expect(getPublishedId('')).toBe('')
30
- })
31
- })
3
+ import {insecureRandomId} from './ids'
32
4
 
33
5
  describe('insecureRandomId', () => {
34
6
  it('should generate 16-character string', () => {
package/src/utils/ids.ts CHANGED
@@ -1,13 +1,3 @@
1
- export function getPublishedId(id: string): string {
2
- const draftsPrefix = 'drafts.'
3
- return id.startsWith(draftsPrefix) ? id.slice(draftsPrefix.length) : id
4
- }
5
-
6
- export function getDraftId(id: string): string {
7
- const draftsPrefix = 'drafts.'
8
- return id.startsWith(draftsPrefix) ? id : `${draftsPrefix}${id}`
9
- }
10
-
11
1
  export function insecureRandomId(): string {
12
2
  return Array.from({length: 16}, () => Math.floor(Math.random() * 16).toString(16)).join('')
13
3
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * A response field that the API embeds only when its `include` token was
3
+ * requested. Given the full set of tokens `All`, the token `Key` this field
4
+ * maps to, and the tokens actually `Requested`, it resolves to:
5
+ *
6
+ * - `unknown` (field absent) when nothing — or nothing matching `Key` — was requested
7
+ * - `Field` (present, as declared) when a literal request list contains `Key`
8
+ * - `Partial<Field>` (optional) when `Requested` is the wide `All` union, i.e.
9
+ * an `include` array whose members aren't known at the type level
10
+ *
11
+ * Absent arms return `unknown` so they vanish under intersection. Mirrors the
12
+ * `boolean extends Flag` trick the organization/project types use, but for an
13
+ * include-token union rather than a boolean flag.
14
+ *
15
+ * @internal
16
+ */
17
+ export type Included<All extends string, Key extends All, Requested extends All, Field> = [
18
+ All,
19
+ ] extends [Requested]
20
+ ? Partial<Field>
21
+ : [Key] extends [Requested]
22
+ ? Field
23
+ : unknown
24
+
25
+ /**
26
+ * Whether the requested include tokens intersect a given set — used to decide
27
+ * whether a parent field (e.g. an application's `activeDeployment`) is present,
28
+ * when requesting any of several tokens forces it in.
29
+ *
30
+ * @internal
31
+ */
32
+ export type IncludesAny<Requested extends string, Set extends string> = [Requested & Set] extends [
33
+ never,
34
+ ]
35
+ ? false
36
+ : true