@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
@@ -2,6 +2,7 @@ import {beforeEach, describe, expect, it, vi} from 'vitest'
2
2
 
3
3
  import {type DocumentResource} from '../config/sanityConfig'
4
4
  import {
5
+ bindActionByDataset,
5
6
  bindActionByResource,
6
7
  bindActionByResourceAndPerspective,
7
8
  bindActionGlobally,
@@ -29,7 +30,7 @@ describe('createActionBinder', () => {
29
30
  return context.state.counter
30
31
  })
31
32
  const boundAction = binder(storeDefinition, action)
32
- const instance = createSanityInstance()
33
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
33
34
 
34
35
  // First call creates store instance
35
36
  const result1 = boundAction(instance, 5)
@@ -43,9 +44,9 @@ describe('createActionBinder', () => {
43
44
  })
44
45
 
45
46
  it('should create separate store instances for different composite keys', () => {
46
- const binder = createActionBinder((instance, ..._rest) => {
47
- return {name: instance.instanceId}
48
- })
47
+ const binder = createActionBinder(({config: {projectId, dataset}}, ..._rest) => ({
48
+ name: `${projectId}.${dataset}`,
49
+ }))
49
50
  const storeDefinition = {
50
51
  name: 'TestStore',
51
52
  getInitialState: () => ({counter: 0}),
@@ -55,8 +56,8 @@ describe('createActionBinder', () => {
55
56
  return context.state.counter
56
57
  })
57
58
  const boundAction = binder(storeDefinition, action)
58
- const instanceA = createSanityInstance()
59
- const instanceB = createSanityInstance()
59
+ const instanceA = createSanityInstance({projectId: 'p1', dataset: 'd1'})
60
+ const instanceB = createSanityInstance({projectId: 'p2', dataset: 'd2'})
60
61
 
61
62
  const resultA = boundAction(instanceA, 3)
62
63
  const resultB = boundAction(instanceB, 4)
@@ -74,8 +75,8 @@ describe('createActionBinder', () => {
74
75
  }
75
76
  const action = vi.fn((context) => context.state.counter)
76
77
  const boundAction = binder(storeDefinition, action)
77
- const instance1 = createSanityInstance()
78
- const instance2 = createSanityInstance()
78
+ const instance1 = createSanityInstance({projectId: 'p1', dataset: 'd1'})
79
+ const instance2 = createSanityInstance({projectId: 'p1', dataset: 'd1'})
79
80
 
80
81
  // Call action on both instances
81
82
  boundAction(instance1)
@@ -95,6 +96,34 @@ describe('createActionBinder', () => {
95
96
  })
96
97
  })
97
98
 
99
+ describe('bindActionByDataset', () => {
100
+ it('should work correctly when projectId and dataset are provided', () => {
101
+ const storeDefinition = {
102
+ name: 'DSStore',
103
+ getInitialState: () => ({counter: 0}),
104
+ }
105
+ const action = vi.fn((_context, {value}: {value: string}) => value)
106
+ const boundAction = bindActionByDataset(storeDefinition, action)
107
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
108
+ const result = boundAction(instance, {value: 'hello'})
109
+ expect(result).toBe('hello')
110
+ })
111
+
112
+ it('should throw an error if projectId or dataset is missing', () => {
113
+ const storeDefinition = {
114
+ name: 'DSStore',
115
+ getInitialState: () => ({counter: 0}),
116
+ }
117
+ const action = vi.fn((_context, _?) => 'fail')
118
+ const boundAction = bindActionByDataset(storeDefinition, action)
119
+ // Instance with missing dataset
120
+ const instance = createSanityInstance({projectId: 'proj1', dataset: ''})
121
+ expect(() => boundAction(instance)).toThrow(
122
+ 'This API requires a project ID and dataset configured.',
123
+ )
124
+ })
125
+ })
126
+
98
127
  describe('bindActionGlobally', () => {
99
128
  it('should work correctly ignoring config in key generation', () => {
100
129
  const storeDefinition = {
@@ -105,8 +134,8 @@ describe('bindActionGlobally', () => {
105
134
  const boundAction = bindActionGlobally(storeDefinition, action)
106
135
 
107
136
  // Create instances with different configs
108
- const instance1 = createSanityInstance()
109
- const instance2 = createSanityInstance()
137
+ const instance1 = createSanityInstance({projectId: 'any', dataset: 'any'})
138
+ const instance2 = createSanityInstance({projectId: 'different', dataset: 'config'})
110
139
 
111
140
  // Both instances should use the same store
112
141
  const result1 = boundAction(instance1, 42)
@@ -133,47 +162,42 @@ describe('bindActionGlobally', () => {
133
162
  })
134
163
 
135
164
  describe('bindActionByResource', () => {
136
- it('should resolve from config.defaultResource when no resource provided', () => {
165
+ it('should throw an error when provided an invalid resource', () => {
137
166
  const storeDefinition = {
138
- name: 'ResourceNamedStore',
167
+ name: 'SourceStore',
139
168
  getInitialState: () => ({counter: 0}),
140
169
  }
141
- const action = vi.fn((context) => context.key)
170
+ const action = vi.fn((_context) => 'success')
142
171
  const boundAction = bindActionByResource(storeDefinition, action)
143
- const instance = createSanityInstance({
144
- defaultResource: {projectId: 'named-proj', dataset: 'named-ds'},
145
- })
146
- const result = boundAction(instance, {})
147
- expect(result).toEqual(
148
- expect.objectContaining({
149
- name: 'named-proj.named-ds',
150
- resource: {projectId: 'named-proj', dataset: 'named-ds'},
151
- }),
152
- )
172
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
173
+
174
+ expect(() =>
175
+ boundAction(instance, {resource: {invalid: 'resource'} as unknown as DocumentResource}),
176
+ ).toThrow('Received invalid resource:')
153
177
  })
154
178
 
155
- it('should throw an error when provided an invalid resource', () => {
179
+ it('should throw an error when no resource provided and projectId/dataset are missing', () => {
156
180
  const storeDefinition = {
157
- name: 'ResourceStore',
181
+ name: 'SourceStore',
158
182
  getInitialState: () => ({counter: 0}),
159
183
  }
160
184
  const action = vi.fn((_context) => 'success')
161
185
  const boundAction = bindActionByResource(storeDefinition, action)
162
- const instance = createSanityInstance()
186
+ const instance = createSanityInstance({projectId: '', dataset: ''})
163
187
 
164
- expect(() =>
165
- boundAction(instance, {resource: {invalid: 'resource'} as unknown as DocumentResource}),
166
- ).toThrow('Received invalid resource:')
188
+ expect(() => boundAction(instance, {})).toThrow(
189
+ 'This API requires a project ID and dataset configured.',
190
+ )
167
191
  })
168
192
 
169
193
  it('should work correctly with a valid dataset resource', () => {
170
194
  const storeDefinition = {
171
- name: 'ResourceStore',
195
+ name: 'SourceStore',
172
196
  getInitialState: () => ({counter: 0}),
173
197
  }
174
198
  const action = vi.fn((_context) => 'success')
175
199
  const boundAction = bindActionByResource(storeDefinition, action)
176
- const instance = createSanityInstance()
200
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
177
201
 
178
202
  const result = boundAction(instance, {
179
203
  resource: {projectId: 'proj2', dataset: 'ds2'},
@@ -190,7 +214,7 @@ describe('bindActionByResourceAndPerspective', () => {
190
214
  }
191
215
  const action = vi.fn((_context) => 'success')
192
216
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
193
- const instance = createSanityInstance()
217
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
194
218
 
195
219
  expect(() =>
196
220
  boundAction(instance, {
@@ -200,35 +224,34 @@ describe('bindActionByResourceAndPerspective', () => {
200
224
  ).toThrow('Received invalid resource:')
201
225
  })
202
226
 
203
- it('should work correctly with a valid dataset resource and explicit perspective', () => {
227
+ it('should throw an error when no resource provided and projectId/dataset are missing', () => {
204
228
  const storeDefinition = {
205
229
  name: 'PerspectiveStore',
206
230
  getInitialState: () => ({counter: 0}),
207
231
  }
208
232
  const action = vi.fn((_context) => 'success')
209
233
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
210
- const instance = createSanityInstance()
234
+ const instance = createSanityInstance({projectId: '', dataset: ''})
211
235
 
212
- const result = boundAction(instance, {
213
- resource: {projectId: 'proj2', dataset: 'ds2'},
214
- perspective: 'drafts',
215
- })
216
- expect(result).toBe('success')
236
+ expect(() => boundAction(instance, {perspective: 'drafts'})).toThrow(
237
+ 'This API requires a project ID and dataset configured.',
238
+ )
217
239
  })
218
240
 
219
- it('should throw an error when no resource provided and no default resource configured', () => {
241
+ it('should work correctly with a valid dataset resource and explicit perspective', () => {
220
242
  const storeDefinition = {
221
243
  name: 'PerspectiveStore',
222
244
  getInitialState: () => ({counter: 0}),
223
245
  }
224
246
  const action = vi.fn((_context) => 'success')
225
247
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
226
- const instance = createSanityInstance({})
248
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
227
249
 
228
- // @ts-expect-error - test invalid argument
229
- expect(() => boundAction(instance, {perspective: 'drafts' as const})).toThrow(
230
- 'No resource provided and no default resource configured.',
231
- )
250
+ const result = boundAction(instance, {
251
+ resource: {projectId: 'proj2', dataset: 'ds2'},
252
+ perspective: 'drafts',
253
+ })
254
+ expect(result).toBe('success')
232
255
  })
233
256
 
234
257
  it('should work correctly with valid dataset resource and no perspective (falls back to drafts)', () => {
@@ -238,7 +261,7 @@ describe('bindActionByResourceAndPerspective', () => {
238
261
  }
239
262
  const action = vi.fn((_context) => 'success')
240
263
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
241
- const instance = createSanityInstance()
264
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
242
265
 
243
266
  const result = boundAction(instance, {
244
267
  resource: {projectId: 'proj1', dataset: 'ds1'},
@@ -246,7 +269,7 @@ describe('bindActionByResourceAndPerspective', () => {
246
269
  expect(result).toBe('success')
247
270
  })
248
271
 
249
- it('should fall back to "drafts" when options.perspective is not provided', () => {
272
+ it('should use instance.config.perspective when options.perspective is not provided', () => {
250
273
  const storeDefinition = {
251
274
  name: 'PerspectiveStore',
252
275
  getInitialState: () => ({counter: 0}),
@@ -254,13 +277,15 @@ describe('bindActionByResourceAndPerspective', () => {
254
277
  const action = vi.fn((context) => context.key)
255
278
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
256
279
  const instance = createSanityInstance({
280
+ projectId: 'proj1',
281
+ dataset: 'ds1',
257
282
  perspective: 'published',
258
283
  })
259
284
 
260
- const result = boundAction(instance, {resource: {projectId: 'proj2', dataset: 'ds2'}})
285
+ const result = boundAction(instance, {})
261
286
  expect(result).toEqual(
262
287
  expect.objectContaining({
263
- name: 'proj2.ds2:published',
288
+ name: 'proj1.ds1:published',
264
289
  perspective: 'published',
265
290
  }),
266
291
  )
@@ -277,18 +302,13 @@ describe('bindActionByResourceAndPerspective', () => {
277
302
  })
278
303
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
279
304
  // Use unique project/dataset so we don't reuse stores from other tests
280
- const instance = createSanityInstance()
305
+ const instance = createSanityInstance({
306
+ projectId: 'perspective-isolation',
307
+ dataset: 'ds1',
308
+ })
281
309
 
282
- const resultDrafts = boundAction(
283
- instance,
284
- {resource: {projectId: 'proj3', dataset: 'ds3'}, perspective: 'drafts'},
285
- 3,
286
- )
287
- const resultPublished = boundAction(
288
- instance,
289
- {resource: {projectId: 'proj3', dataset: 'ds3'}, perspective: 'published'},
290
- 4,
291
- )
310
+ const resultDrafts = boundAction(instance, {perspective: 'drafts'}, 3)
311
+ const resultPublished = boundAction(instance, {perspective: 'published'}, 4)
292
312
 
293
313
  expect(resultDrafts).toBe(3)
294
314
  expect(resultPublished).toBe(4)
@@ -303,10 +323,9 @@ describe('bindActionByResourceAndPerspective', () => {
303
323
  }
304
324
  const action = vi.fn((_context) => 'success')
305
325
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
306
- const instance = createSanityInstance()
326
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
307
327
 
308
328
  const result = boundAction(instance, {
309
- resource: {projectId: 'proj1', dataset: 'ds1'},
310
329
  perspective: {releaseName: 'release1'},
311
330
  })
312
331
  expect(result).toBe('success')
@@ -320,23 +339,6 @@ describe('bindActionByResourceAndPerspective', () => {
320
339
  )
321
340
  })
322
341
 
323
- it('should throw when provided a stackable perspective', () => {
324
- const storeDefinition = {
325
- name: 'PerspectiveStore',
326
- getInitialState: () => ({counter: 0}),
327
- }
328
- const action = vi.fn((_context) => 'success')
329
- const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
330
- const instance = createSanityInstance()
331
-
332
- expect(() =>
333
- boundAction(instance, {
334
- resource: {projectId: 'proj1', dataset: 'ds1'},
335
- perspective: ['drafts', 'release1'] as unknown as 'drafts',
336
- }),
337
- ).toThrow('Stackable perspectives are not supported.')
338
- })
339
-
340
342
  it('should reuse same store when same resource and perspective are used', () => {
341
343
  const storeDefinition = {
342
344
  name: 'PerspectiveStore',
@@ -348,18 +350,13 @@ describe('bindActionByResourceAndPerspective', () => {
348
350
  })
349
351
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
350
352
  // Use unique project/dataset so we don't reuse stores from other tests
351
- const instance = createSanityInstance()
353
+ const instance = createSanityInstance({
354
+ projectId: 'perspective-reuse',
355
+ dataset: 'ds1',
356
+ })
352
357
 
353
- const result1 = boundAction(
354
- instance,
355
- {resource: {projectId: 'proj4', dataset: 'ds4'}, perspective: 'drafts'},
356
- 2,
357
- )
358
- const result2 = boundAction(
359
- instance,
360
- {resource: {projectId: 'proj4', dataset: 'ds4'}, perspective: 'drafts'},
361
- 3,
362
- )
358
+ const result1 = boundAction(instance, {perspective: 'drafts'}, 2)
359
+ const result2 = boundAction(instance, {perspective: 'drafts'}, 3)
363
360
 
364
361
  expect(result1).toBe(2)
365
362
  expect(result2).toBe(5)
@@ -1,10 +1,12 @@
1
+ import {type ClientPerspective} from '@sanity/client'
2
+
1
3
  import {
4
+ type DatasetHandle,
2
5
  type DocumentResource,
3
6
  isCanvasResource,
4
7
  isDatasetResource,
5
8
  isMediaLibraryResource,
6
- type PerspectiveHandle,
7
- type ResourceHandle,
9
+ type ReleasePerspective,
8
10
  } from '../config/sanityConfig'
9
11
  import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
10
12
  import {type SanityInstance} from './createSanityInstance'
@@ -17,8 +19,14 @@ export interface BoundResourceKey {
17
19
  resource: DocumentResource
18
20
  }
19
21
  export interface BoundPerspectiveKey extends BoundResourceKey {
20
- perspective: NonNullable<PerspectiveHandle['perspective']>
22
+ perspective: ClientPerspective | ReleasePerspective
23
+ }
24
+ interface BoundDatasetKey {
25
+ name: string
26
+ projectId: string
27
+ dataset: string
21
28
  }
29
+
22
30
  /**
23
31
  * Defines a store action that operates on a specific state type
24
32
  */
@@ -115,29 +123,75 @@ export function createActionBinder<
115
123
  }
116
124
  }
117
125
 
118
- const resourceKeyName = (resource: DocumentResource): string => {
119
- if (isDatasetResource(resource)) return `${resource.projectId}.${resource.dataset}`
120
- if (isMediaLibraryResource(resource)) return `media-library:${resource.mediaLibraryId}`
121
- if (isCanvasResource(resource)) return `canvas:${resource.canvasId}`
122
- throw new Error(`Received invalid resource: ${JSON.stringify(resource)}`)
123
- }
126
+ /**
127
+ * Binds an action to a store that's scoped to a specific project and dataset
128
+ *
129
+ * @remarks
130
+ * This creates actions that operate on state isolated to a specific projectId and dataset.
131
+ * Different project/dataset combinations will have separate states.
132
+ *
133
+ * @throws Error if projectId or dataset is missing from the Sanity instance config
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * // Define a store
138
+ * const documentStore = defineStore<DocumentState>({
139
+ * name: 'Document',
140
+ * getInitialState: () => ({ documents: {} }),
141
+ * // ...
142
+ * })
143
+ *
144
+ * // Create dataset-specific actions
145
+ * export const fetchDocument = bindActionByDataset(
146
+ * documentStore,
147
+ * ({instance, state}, documentId) => {
148
+ * // This state is isolated to the specific project/dataset
149
+ * // ...fetch logic...
150
+ * }
151
+ * )
152
+ *
153
+ * // Usage
154
+ * fetchDocument(sanityInstance, 'doc123')
155
+ * ```
156
+ */
157
+ export const bindActionByDataset = createActionBinder<
158
+ BoundDatasetKey,
159
+ [(object & {projectId?: string; dataset?: string})?, ...unknown[]]
160
+ >((instance, options) => {
161
+ const projectId = options?.projectId ?? instance.config.projectId
162
+ const dataset = options?.dataset ?? instance.config.dataset
163
+ if (!projectId || !dataset) {
164
+ throw new Error('This API requires a project ID and dataset configured.')
165
+ }
166
+ return {name: `${projectId}.${dataset}`, projectId, dataset}
167
+ })
124
168
 
125
169
  const createResourceKey = (
126
170
  instance: SanityInstance,
127
171
  resource?: DocumentResource,
128
172
  ): BoundResourceKey => {
173
+ let name: string | undefined
174
+ let resourceForKey: DocumentResource | undefined
129
175
  if (resource) {
130
- return {name: resourceKeyName(resource), resource}
176
+ resourceForKey = resource
177
+ if (isDatasetResource(resource)) {
178
+ name = `${resource.projectId}.${resource.dataset}`
179
+ } else if (isMediaLibraryResource(resource)) {
180
+ name = `media-library:${resource.mediaLibraryId}`
181
+ } else if (isCanvasResource(resource)) {
182
+ name = `canvas:${resource.canvasId}`
183
+ } else {
184
+ throw new Error(`Received invalid resource: ${JSON.stringify(resource)}`)
185
+ }
186
+ return {name, resource: resourceForKey}
131
187
  }
132
188
 
133
- const defaultResource = instance.config.defaultResource
134
- if (!defaultResource) {
135
- throw new Error(
136
- 'No resource provided and no default resource configured. ' +
137
- 'Provide a "default" resource in your resources map, or pass an explicit resource.',
138
- )
189
+ // TODO: remove reference to instance.config when we get to v3
190
+ const {projectId, dataset} = instance.config
191
+ if (!projectId || !dataset) {
192
+ throw new Error('This API requires a project ID and dataset configured.')
139
193
  }
140
- return {name: resourceKeyName(defaultResource), resource: defaultResource}
194
+ return {name: `${projectId}.${dataset}`, resource: {projectId, dataset}}
141
195
  }
142
196
 
143
197
  /**
@@ -145,6 +199,8 @@ const createResourceKey = (
145
199
  **/
146
200
  export const bindActionByResource = createActionBinder<
147
201
  BoundResourceKey,
202
+ // this implies resources is optional to keep backwards compatibility
203
+ // but in reality, we'll always pass a resource (since we'll defer to the instance until v3)
148
204
  [{resource?: DocumentResource}, ...unknown[]]
149
205
  >((instance, {resource}) => {
150
206
  return createResourceKey(instance, resource)
@@ -161,7 +217,6 @@ export const bindActionByResource = createActionBinder<
161
217
  * can isolate single queries by perspective.
162
218
  *
163
219
  * @throws Error if resource or perspective is missing from the Sanity instance config
164
- * @throws Error if a stackable perspective (array) is provided, as they are not supported
165
220
  *
166
221
  * @example
167
222
  * ```ts
@@ -187,7 +242,7 @@ export const bindActionByResource = createActionBinder<
187
242
  */
188
243
  export const bindActionByResourceAndPerspective = createActionBinder<
189
244
  BoundPerspectiveKey,
190
- [ResourceHandle, ...unknown[]]
245
+ [DatasetHandle, ...unknown[]]
191
246
  >((instance, options): BoundPerspectiveKey => {
192
247
  const {resource, perspective} = options
193
248
  // TODO: remove reference to instance.config.perspective when we get to v3
@@ -198,15 +253,14 @@ export const bindActionByResourceAndPerspective = createActionBinder<
198
253
  } else if (typeof utilizedPerspective === 'string') {
199
254
  perspectiveKey = utilizedPerspective
200
255
  } else {
201
- throw new Error(
202
- `Stackable perspectives are not supported. Received perspective: ${JSON.stringify(utilizedPerspective)}`,
203
- )
256
+ // "StackablePerspective", shouldn't be a common case, but just in case
257
+ perspectiveKey = JSON.stringify(utilizedPerspective)
204
258
  }
205
- const resourceKey = createResourceKey(instance, resource)
259
+ const sourceKey = createResourceKey(instance, resource)
206
260
 
207
261
  return {
208
- name: `${resourceKey.name}:${perspectiveKey}`,
209
- resource: resourceKey.resource,
262
+ name: `${sourceKey.name}:${perspectiveKey}`,
263
+ resource: sourceKey.resource,
210
264
  perspective: utilizedPerspective,
211
265
  }
212
266
  })
@@ -5,14 +5,14 @@ import {createSanityInstance} from './createSanityInstance'
5
5
 
6
6
  describe('createSanityInstance', () => {
7
7
  it('should create an instance with a unique instanceId and given config', () => {
8
- const instance = createSanityInstance()
8
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
9
9
  expect(typeof instance.instanceId).toBe('string')
10
- expect(instance.config).toEqual({})
10
+ expect(instance.config).toEqual({projectId: 'proj1', dataset: 'ds1'})
11
11
  expect(instance.isDisposed()).toBe(false)
12
12
  })
13
13
 
14
14
  it('should dispose an instance and call onDispose callbacks', () => {
15
- const instance = createSanityInstance()
15
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
16
16
  const callback = vi.fn()
17
17
  instance.onDispose(callback)
18
18
  instance.dispose()
@@ -21,7 +21,7 @@ describe('createSanityInstance', () => {
21
21
  })
22
22
 
23
23
  it('should not call onDispose callbacks more than once when disposed multiple times', () => {
24
- const instance = createSanityInstance()
24
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
25
25
  const callback = vi.fn()
26
26
  instance.onDispose(callback)
27
27
  instance.dispose()
@@ -29,6 +29,60 @@ describe('createSanityInstance', () => {
29
29
  expect(callback).toHaveBeenCalledTimes(1)
30
30
  })
31
31
 
32
+ it('should create a child instance with merged config and correct parent', () => {
33
+ const parent = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
34
+ const child = parent.createChild({dataset: 'ds2'})
35
+ expect(child.config).toEqual({projectId: 'proj1', dataset: 'ds2'})
36
+ expect(child.getParent()).toBe(parent)
37
+ })
38
+
39
+ it('should match an instance in the hierarchy using match', () => {
40
+ // three-level hierarchy
41
+ const grandparent = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
42
+ const parent = grandparent.createChild({projectId: 'proj2'})
43
+ const child = parent.createChild({dataset: 'ds2'})
44
+
45
+ expect(child.config).toEqual({projectId: 'proj2', dataset: 'ds2'})
46
+ expect(parent.config).toEqual({projectId: 'proj2', dataset: 'ds1'})
47
+
48
+ expect(child.match({dataset: 'ds2'})).toBe(child)
49
+ expect(child.match({projectId: 'proj2'})).toBe(child)
50
+ expect(child.match({projectId: 'proj1'})).toBe(grandparent)
51
+ expect(parent.match({projectId: 'proj1'})).toBe(grandparent)
52
+ expect(grandparent.match({projectId: 'proj1'})).toBe(grandparent)
53
+ })
54
+
55
+ it('should match `undefined` when the desired resource ID should not be set on an instance', () => {
56
+ const noProjectOrDataset = createSanityInstance()
57
+ const noDataset = noProjectOrDataset.createChild({projectId: 'proj1'})
58
+ const leaf = noDataset.createChild({dataset: 'ds1'})
59
+
60
+ // no keys means anything (in this case, self) will match
61
+ expect(leaf.match({})).toBe(leaf)
62
+
63
+ // `[resourceId]: undefined` means match an instance with no dataset set
64
+ expect(leaf.match({dataset: undefined})).toBe(noDataset)
65
+ expect(noDataset.match({dataset: undefined})).toBe(noDataset)
66
+ expect(leaf.match({projectId: undefined})).toBe(noProjectOrDataset)
67
+ expect(noDataset.match({projectId: undefined})).toBe(noProjectOrDataset)
68
+ expect(noProjectOrDataset.match({projectId: undefined})).toBe(noProjectOrDataset)
69
+ })
70
+
71
+ it('should return undefined when no match is found', () => {
72
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
73
+ expect(instance.match({dataset: 'non-existent'})).toBeUndefined()
74
+ })
75
+
76
+ it('should inherit and merge auth config', () => {
77
+ const parent = createSanityInstance({
78
+ projectId: 'proj1',
79
+ dataset: 'ds1',
80
+ auth: {apiHost: 'api.sanity.work'},
81
+ })
82
+ const child = parent.createChild({auth: {token: 'my-token'}})
83
+ expect(child.config.auth).toEqual({apiHost: 'api.sanity.work', token: 'my-token'})
84
+ })
85
+
32
86
  describe('logging', () => {
33
87
  const mockHandler: LogHandler = {
34
88
  error: vi.fn(),
@@ -52,30 +106,31 @@ describe('createSanityInstance', () => {
52
106
  })
53
107
 
54
108
  it('should log instance creation at info level', () => {
55
- createSanityInstance()
109
+ createSanityInstance({projectId: 'test-proj', dataset: 'test-ds'})
56
110
 
57
111
  expect(mockHandler.info).toHaveBeenCalledWith(
58
112
  expect.stringContaining('[INFO] [sdk]'),
59
113
  expect.objectContaining({
60
- hasAuth: false,
61
- hasPerspective: false,
114
+ hasProjectId: true,
115
+ hasDataset: true,
62
116
  }),
63
117
  )
64
118
  })
65
119
 
66
120
  it('should log configuration details at debug level', () => {
67
- createSanityInstance({defaultResource: {projectId: 'test-proj', dataset: 'test-dataset'}})
121
+ createSanityInstance({projectId: 'test-proj', dataset: 'test-ds'})
68
122
 
69
123
  expect(mockHandler.debug).toHaveBeenCalledWith(
70
124
  expect.stringContaining('[DEBUG] [sdk]'),
71
125
  expect.objectContaining({
72
- hasStudioConfig: false,
126
+ projectId: 'test-proj',
127
+ dataset: 'test-ds',
73
128
  }),
74
129
  )
75
130
  })
76
131
 
77
132
  it('should log instance disposal', () => {
78
- const instance = createSanityInstance()
133
+ const instance = createSanityInstance({projectId: 'test-proj'})
79
134
  vi.clearAllMocks() // Clear creation logs
80
135
 
81
136
  instance.dispose()
@@ -86,11 +141,26 @@ describe('createSanityInstance', () => {
86
141
  )
87
142
  })
88
143
 
89
- it('should include instance id in logs', () => {
90
- createSanityInstance()
144
+ it('should log child instance creation at debug level', () => {
145
+ const parent = createSanityInstance({projectId: 'parent-proj'})
146
+ vi.clearAllMocks() // Clear parent creation logs
147
+
148
+ parent.createChild({dataset: 'child-ds'})
149
+
150
+ expect(mockHandler.debug).toHaveBeenCalledWith(
151
+ expect.stringContaining('Creating child instance'),
152
+ expect.objectContaining({
153
+ overridingDataset: true,
154
+ }),
155
+ )
156
+ })
157
+
158
+ it('should include instance context in logs', () => {
159
+ createSanityInstance({projectId: 'my-project', dataset: 'my-dataset'})
91
160
 
161
+ // Check that logs include the instance context (project and dataset)
92
162
  expect(mockHandler.info).toHaveBeenCalledWith(
93
- expect.stringMatching(/\[instance:/),
163
+ expect.stringMatching(/\[project:my-project\].*\[dataset:my-dataset\]/),
94
164
  expect.anything(),
95
165
  )
96
166
  })