@sanity/sdk 2.4.0 → 2.6.0

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 (57) hide show
  1. package/dist/index.d.ts +346 -110
  2. package/dist/index.js +428 -136
  3. package/dist/index.js.map +1 -1
  4. package/package.json +10 -9
  5. package/src/_exports/index.ts +15 -3
  6. package/src/auth/authStore.test.ts +13 -13
  7. package/src/auth/refreshStampedToken.test.ts +16 -16
  8. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +6 -6
  9. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -4
  10. package/src/client/clientStore.test.ts +45 -43
  11. package/src/client/clientStore.ts +23 -9
  12. package/src/comlink/controller/actions/destroyController.test.ts +2 -2
  13. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +6 -6
  14. package/src/comlink/controller/actions/getOrCreateController.test.ts +5 -5
  15. package/src/comlink/controller/actions/getOrCreateController.ts +1 -1
  16. package/src/comlink/controller/actions/releaseChannel.test.ts +3 -2
  17. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -4
  18. package/src/comlink/node/actions/getOrCreateNode.test.ts +7 -7
  19. package/src/comlink/node/actions/releaseNode.test.ts +2 -2
  20. package/src/comlink/node/comlinkNodeStore.test.ts +4 -3
  21. package/src/config/loggingConfig.ts +149 -0
  22. package/src/config/sanityConfig.ts +47 -23
  23. package/src/document/actions.ts +11 -7
  24. package/src/document/applyDocumentActions.test.ts +9 -6
  25. package/src/document/applyDocumentActions.ts +9 -49
  26. package/src/document/documentStore.test.ts +128 -115
  27. package/src/document/documentStore.ts +40 -10
  28. package/src/document/permissions.test.ts +9 -9
  29. package/src/document/permissions.ts +17 -7
  30. package/src/document/processActions.test.ts +248 -0
  31. package/src/document/processActions.ts +173 -0
  32. package/src/document/reducers.ts +13 -6
  33. package/src/presence/presenceStore.ts +13 -7
  34. package/src/preview/previewStore.test.ts +10 -2
  35. package/src/preview/previewStore.ts +2 -1
  36. package/src/preview/subscribeToStateAndFetchBatches.test.ts +8 -5
  37. package/src/preview/subscribeToStateAndFetchBatches.ts +9 -3
  38. package/src/projection/projectionStore.test.ts +18 -2
  39. package/src/projection/projectionStore.ts +2 -1
  40. package/src/projection/subscribeToStateAndFetchBatches.test.ts +6 -5
  41. package/src/projection/subscribeToStateAndFetchBatches.ts +9 -3
  42. package/src/query/queryStore.ts +3 -1
  43. package/src/releases/getPerspectiveState.ts +2 -2
  44. package/src/releases/releasesStore.ts +10 -4
  45. package/src/store/createActionBinder.test.ts +8 -6
  46. package/src/store/createActionBinder.ts +54 -28
  47. package/src/store/createSanityInstance.test.ts +85 -1
  48. package/src/store/createSanityInstance.ts +53 -4
  49. package/src/store/createStateSourceAction.test.ts +12 -11
  50. package/src/store/createStateSourceAction.ts +6 -6
  51. package/src/store/createStoreInstance.test.ts +29 -16
  52. package/src/store/createStoreInstance.ts +6 -5
  53. package/src/store/defineStore.test.ts +1 -1
  54. package/src/store/defineStore.ts +12 -7
  55. package/src/utils/logger-usage-example.md +141 -0
  56. package/src/utils/logger.test.ts +757 -0
  57. package/src/utils/logger.ts +537 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -18,7 +18,8 @@
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "git+ssh://git@github.com/sanity-io/sdk.git"
21
+ "url": "git+https://github.com/sanity-io/sdk.git",
22
+ "directory": "packages/core"
22
23
  },
23
24
  "license": "MIT",
24
25
  "author": "Sanity <developers@sanity.io>",
@@ -48,9 +49,9 @@
48
49
  "@sanity/diff-match-patch": "^3.2.0",
49
50
  "@sanity/diff-patch": "^6.0.0",
50
51
  "@sanity/json-match": "^1.0.5",
51
- "@sanity/message-protocol": "^0.12.0",
52
+ "@sanity/message-protocol": "^0.18.0",
52
53
  "@sanity/mutate": "^0.12.4",
53
- "@sanity/types": "^3.83.0",
54
+ "@sanity/types": "^5.2.0",
54
55
  "groq": "3.88.1-typegen-experimental.0",
55
56
  "groq-js": "^1.19.0",
56
57
  "lodash-es": "^4.17.21",
@@ -60,7 +61,7 @@
60
61
  },
61
62
  "devDependencies": {
62
63
  "@sanity/browserslist-config": "^1.0.5",
63
- "@sanity/pkg-utils": "^7.9.6",
64
+ "@sanity/pkg-utils": "^8.1.29",
64
65
  "@sanity/prettier-config": "^1.0.6",
65
66
  "@types/lodash-es": "^4.17.12",
66
67
  "@vitest/coverage-v8": "3.2.4",
@@ -70,14 +71,14 @@
70
71
  "typescript": "^5.8.3",
71
72
  "vite": "^6.3.4",
72
73
  "vitest": "^3.2.4",
73
- "@repo/config-test": "0.0.1",
74
- "@repo/package.bundle": "3.82.0",
75
74
  "@repo/config-eslint": "0.0.0",
75
+ "@repo/package.bundle": "3.82.0",
76
76
  "@repo/tsconfig": "0.0.1",
77
- "@repo/package.config": "0.0.1"
77
+ "@repo/package.config": "0.0.1",
78
+ "@repo/config-test": "0.0.1"
78
79
  },
79
80
  "engines": {
80
- "node": ">=20.0.0"
81
+ "node": ">=20.19"
81
82
  },
82
83
  "publishConfig": {
83
84
  "access": "public"
@@ -76,13 +76,25 @@ export {
76
76
  createProjectHandle,
77
77
  } from '../config/handles'
78
78
  export {
79
- canvasSource,
79
+ configureLogging,
80
+ type InstanceContext,
81
+ type LogContext,
82
+ type Logger,
83
+ type LoggerConfig,
84
+ type LogLevel,
85
+ type LogNamespace,
86
+ } from '../config/loggingConfig'
87
+ export {
88
+ type CanvasSource,
80
89
  type DatasetHandle,
81
- datasetSource,
90
+ type DatasetSource,
82
91
  type DocumentHandle,
83
92
  type DocumentSource,
84
93
  type DocumentTypeHandle,
85
- mediaLibrarySource,
94
+ isCanvasSource,
95
+ isDatasetSource,
96
+ isMediaLibrarySource,
97
+ type MediaLibrarySource,
86
98
  type PerspectiveHandle,
87
99
  type ProjectHandle,
88
100
  type ReleasePerspective,
@@ -92,7 +92,7 @@ describe('authStore', () => {
92
92
  },
93
93
  })
94
94
 
95
- const {options, dashboardContext} = authStore.getInitialState(instance)
95
+ const {options, dashboardContext} = authStore.getInitialState(instance, null)
96
96
 
97
97
  expect(options.apiHost).toBe(apiHost)
98
98
  expect(options.callbackUrl).toBe(callbackUrl)
@@ -114,7 +114,7 @@ describe('authStore', () => {
114
114
  auth: {initialLocationHref},
115
115
  })
116
116
 
117
- const {dashboardContext, authState} = authStore.getInitialState(instance)
117
+ const {dashboardContext, authState} = authStore.getInitialState(instance, null)
118
118
  expect(dashboardContext).toEqual(context)
119
119
  expect(authState.type).toBe(AuthStateType.LOGGED_OUT)
120
120
  })
@@ -129,7 +129,7 @@ describe('authStore', () => {
129
129
  auth: {initialLocationHref},
130
130
  })
131
131
 
132
- const {dashboardContext, authState} = authStore.getInitialState(instance)
132
+ const {dashboardContext, authState} = authStore.getInitialState(instance, null)
133
133
  expect(dashboardContext).toEqual(expectedContext)
134
134
  expect(authState.type).toBe(AuthStateType.LOGGED_OUT)
135
135
  })
@@ -143,7 +143,7 @@ describe('authStore', () => {
143
143
  auth: {initialLocationHref},
144
144
  })
145
145
 
146
- const {dashboardContext, authState} = authStore.getInitialState(instance)
146
+ const {dashboardContext, authState} = authStore.getInitialState(instance, null)
147
147
  expect(dashboardContext).toStrictEqual({})
148
148
  expect(authState.type).toBe(AuthStateType.LOGGED_OUT)
149
149
  expect(errorSpy).toHaveBeenCalledWith(
@@ -166,7 +166,7 @@ describe('authStore', () => {
166
166
  },
167
167
  })
168
168
 
169
- const {authState, dashboardContext} = authStore.getInitialState(instance)
169
+ const {authState, dashboardContext} = authStore.getInitialState(instance, null)
170
170
  expect(authState).toMatchObject({type: AuthStateType.LOGGED_IN, token})
171
171
  expect(dashboardContext).toEqual(context)
172
172
  })
@@ -182,7 +182,7 @@ describe('authStore', () => {
182
182
 
183
183
  vi.mocked(getAuthCode).mockReturnValue('auth-code')
184
184
 
185
- const {authState, dashboardContext} = authStore.getInitialState(instance)
185
+ const {authState, dashboardContext} = authStore.getInitialState(instance, null)
186
186
  expect(authState).toMatchObject({type: AuthStateType.LOGGING_IN})
187
187
  expect(dashboardContext).toEqual(context)
188
188
  })
@@ -197,7 +197,7 @@ describe('authStore', () => {
197
197
  vi.mocked(getAuthCode).mockReturnValue(null)
198
198
  vi.mocked(getTokenFromStorage).mockReturnValue(storageToken)
199
199
 
200
- const {authState, dashboardContext} = authStore.getInitialState(instance)
200
+ const {authState, dashboardContext} = authStore.getInitialState(instance, null)
201
201
  expect(authState).toMatchObject({type: AuthStateType.LOGGED_IN, token: storageToken})
202
202
  expect(dashboardContext).toStrictEqual({})
203
203
  })
@@ -215,7 +215,7 @@ describe('authStore', () => {
215
215
  vi.mocked(getAuthCode).mockReturnValue(null)
216
216
  vi.mocked(getTokenFromStorage).mockReturnValue(storageToken)
217
217
 
218
- const {authState, dashboardContext} = authStore.getInitialState(instance)
218
+ const {authState, dashboardContext} = authStore.getInitialState(instance, null)
219
219
  expect(authState).toMatchObject({type: AuthStateType.LOGGED_OUT})
220
220
  expect(dashboardContext).toEqual(context)
221
221
  })
@@ -229,7 +229,7 @@ describe('authStore', () => {
229
229
  vi.mocked(getAuthCode).mockReturnValue(null)
230
230
  vi.mocked(getTokenFromStorage).mockReturnValue(null)
231
231
 
232
- const {authState, dashboardContext} = authStore.getInitialState(instance)
232
+ const {authState, dashboardContext} = authStore.getInitialState(instance, null)
233
233
  expect(authState).toMatchObject({type: AuthStateType.LOGGED_OUT})
234
234
  expect(dashboardContext).toStrictEqual({})
235
235
  })
@@ -252,7 +252,7 @@ describe('authStore', () => {
252
252
  auth: {storageArea: mockStorage}, // Provide mock storage
253
253
  })
254
254
 
255
- const {authState, options} = authStore.getInitialState(instance)
255
+ const {authState, options} = authStore.getInitialState(instance, null)
256
256
  expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, studioStorageKey)
257
257
  expect(authState).toMatchObject({type: AuthStateType.LOGGED_IN, token: studioToken})
258
258
  expect(options.authMethod).toBe('localstorage')
@@ -277,7 +277,7 @@ describe('authStore', () => {
277
277
  })
278
278
 
279
279
  // Verify initial state without async cookie probe
280
- const {authState: initialAuthState} = authStore.getInitialState(instance)
280
+ const {authState: initialAuthState} = authStore.getInitialState(instance, null)
281
281
  expect(initialAuthState.type).toBe(AuthStateType.LOGGED_OUT)
282
282
  expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, studioStorageKey)
283
283
 
@@ -296,7 +296,7 @@ describe('authStore', () => {
296
296
  dataset: 'd',
297
297
  })
298
298
 
299
- const {authState, options} = authStore.getInitialState(instance)
299
+ const {authState, options} = authStore.getInitialState(instance, null)
300
300
  expect(getStudioTokenFromLocalStorage).not.toHaveBeenCalled()
301
301
  expect(checkForCookieAuth).not.toHaveBeenCalled()
302
302
  expect(getTokenFromStorage).toHaveBeenCalled()
@@ -314,7 +314,7 @@ describe('authStore', () => {
314
314
  vi.mocked(getAuthCode).mockReturnValue(null)
315
315
  vi.mocked(getTokenFromLocation).mockReturnValue('hash-token')
316
316
 
317
- const {authState} = authStore.getInitialState(instance)
317
+ const {authState} = authStore.getInitialState(instance, null)
318
318
  expect(authState).toMatchObject({
319
319
  type: AuthStateType.LOGGING_IN,
320
320
  isExchangingToken: false,
@@ -122,7 +122,7 @@ describe('refreshStampedToken', () => {
122
122
  dataset: 'd',
123
123
  auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
124
124
  })
125
- const initialState = authStore.getInitialState(instance)
125
+ const initialState = authStore.getInitialState(instance, null)
126
126
  initialState.authState = {
127
127
  type: AuthStateType.LOGGED_IN,
128
128
  token: 'sk-initial-token-st123',
@@ -131,7 +131,7 @@ describe('refreshStampedToken', () => {
131
131
  initialState.dashboardContext = {mode: 'test'}
132
132
  const state = createStoreState(initialState)
133
133
 
134
- const subscription = refreshStampedToken({state, instance})
134
+ const subscription = refreshStampedToken({state, instance, key: null})
135
135
  subscriptions.push(subscription)
136
136
 
137
137
  await vi.advanceTimersToNextTimerAsync()
@@ -168,7 +168,7 @@ describe('refreshStampedToken', () => {
168
168
  dataset: 'd',
169
169
  auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
170
170
  })
171
- const initialState = authStore.getInitialState(instance)
171
+ const initialState = authStore.getInitialState(instance, null)
172
172
  initialState.authState = {
173
173
  type: AuthStateType.LOGGED_IN,
174
174
  token: 'sk-initial-token-st123',
@@ -177,7 +177,7 @@ describe('refreshStampedToken', () => {
177
177
  initialState.dashboardContext = {mode: 'test'}
178
178
  const state = createStoreState(initialState)
179
179
 
180
- const subscription = refreshStampedToken({state, instance})
180
+ const subscription = refreshStampedToken({state, instance, key: null})
181
181
  subscriptions.push(subscription)
182
182
 
183
183
  await vi.advanceTimersToNextTimerAsync()
@@ -202,7 +202,7 @@ describe('refreshStampedToken', () => {
202
202
  dataset: 'd',
203
203
  auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
204
204
  })
205
- const initialState = authStore.getInitialState(instance)
205
+ const initialState = authStore.getInitialState(instance, null)
206
206
  initialState.authState = {
207
207
  type: AuthStateType.LOGGED_IN,
208
208
  token: 'sk-initial-token-st123',
@@ -213,7 +213,7 @@ describe('refreshStampedToken', () => {
213
213
  let subscription: Subscription | undefined
214
214
  // We expect this NOT to throw, but accept we can't easily test the lock call or outcome
215
215
  expect(() => {
216
- subscription = refreshStampedToken({state, instance})
216
+ subscription = refreshStampedToken({state, instance, key: null})
217
217
  subscriptions.push(subscription!)
218
218
  }).not.toThrow()
219
219
 
@@ -253,7 +253,7 @@ describe('refreshStampedToken', () => {
253
253
  dataset: 'd',
254
254
  auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
255
255
  })
256
- const initialState = authStore.getInitialState(instance)
256
+ const initialState = authStore.getInitialState(instance, null)
257
257
  initialState.authState = {
258
258
  type: AuthStateType.LOGGED_IN,
259
259
  token: 'sk-initial-token-st123',
@@ -261,7 +261,7 @@ describe('refreshStampedToken', () => {
261
261
  }
262
262
  const state = createStoreState(initialState)
263
263
 
264
- const subscription = refreshStampedToken({state, instance})
264
+ const subscription = refreshStampedToken({state, instance, key: null})
265
265
  subscriptions.push(subscription)
266
266
 
267
267
  // DO NOT advance timers or yield here - focus on immediate observable logic
@@ -303,7 +303,7 @@ describe('refreshStampedToken', () => {
303
303
  dataset: 'd',
304
304
  auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
305
305
  })
306
- const initialState = authStore.getInitialState(instance)
306
+ const initialState = authStore.getInitialState(instance, null)
307
307
  initialState.authState = {
308
308
  type: AuthStateType.LOGGED_IN,
309
309
  token: 'sk-initial-token-st123',
@@ -311,7 +311,7 @@ describe('refreshStampedToken', () => {
311
311
  }
312
312
  const state = createStoreState(initialState)
313
313
 
314
- const subscription = refreshStampedToken({state, instance})
314
+ const subscription = refreshStampedToken({state, instance, key: null})
315
315
  subscriptions.push(subscription)
316
316
 
317
317
  // Advance timers to allow the async `performRefresh` to execute
@@ -349,7 +349,7 @@ describe('refreshStampedToken', () => {
349
349
  dataset: 'd',
350
350
  auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
351
351
  })
352
- const initialState = authStore.getInitialState(instance)
352
+ const initialState = authStore.getInitialState(instance, null)
353
353
  initialState.authState = {
354
354
  type: AuthStateType.LOGGED_IN,
355
355
  token: 'sk-initial-token-st123',
@@ -358,7 +358,7 @@ describe('refreshStampedToken', () => {
358
358
  initialState.dashboardContext = {mode: 'test'}
359
359
  const state = createStoreState(initialState)
360
360
 
361
- const subscription = refreshStampedToken({state, instance})
361
+ const subscription = refreshStampedToken({state, instance, key: null})
362
362
  subscriptions.push(subscription)
363
363
 
364
364
  await vi.advanceTimersToNextTimerAsync()
@@ -378,14 +378,14 @@ describe('refreshStampedToken', () => {
378
378
  dataset: 'd',
379
379
  auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
380
380
  })
381
- const initialState = authStore.getInitialState(instance)
381
+ const initialState = authStore.getInitialState(instance, null)
382
382
  initialState.authState = {
383
383
  type: AuthStateType.LOGGED_OUT,
384
384
  isDestroyingSession: false,
385
385
  } as AuthState
386
386
  const state = createStoreState(initialState)
387
387
 
388
- const subscription = refreshStampedToken({state, instance})
388
+ const subscription = refreshStampedToken({state, instance, key: null})
389
389
  subscriptions.push(subscription)
390
390
 
391
391
  await vi.advanceTimersByTimeAsync(0)
@@ -404,7 +404,7 @@ describe('refreshStampedToken', () => {
404
404
  dataset: 'd',
405
405
  auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
406
406
  })
407
- const initialState = authStore.getInitialState(instance)
407
+ const initialState = authStore.getInitialState(instance, null)
408
408
  initialState.authState = {
409
409
  type: AuthStateType.LOGGED_IN,
410
410
  token: 'sk-nonstamped-token',
@@ -412,7 +412,7 @@ describe('refreshStampedToken', () => {
412
412
  }
413
413
  const state = createStoreState(initialState)
414
414
 
415
- const subscription = refreshStampedToken({state, instance})
415
+ const subscription = refreshStampedToken({state, instance, key: null})
416
416
  subscriptions.push(subscription)
417
417
 
418
418
  await vi.advanceTimersByTimeAsync(0)
@@ -20,8 +20,8 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
20
20
  const clientFactory = vi.fn().mockReturnValue(mockClient)
21
21
  const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
22
22
 
23
- const state = createStoreState(authStore.getInitialState(instance))
24
- const subscription = subscribeToStateAndFetchCurrentUser({state, instance})
23
+ const state = createStoreState(authStore.getInitialState(instance, null))
24
+ const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
25
25
 
26
26
  expect(state.get()).toMatchObject({authState: {type: AuthStateType.LOGGED_OUT}})
27
27
 
@@ -52,8 +52,8 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
52
52
  const clientFactory = vi.fn().mockReturnValue(mockClient)
53
53
  const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
54
54
 
55
- const state = createStoreState(authStore.getInitialState(instance))
56
- const subscription = subscribeToStateAndFetchCurrentUser({state, instance})
55
+ const state = createStoreState(authStore.getInitialState(instance, null))
56
+ const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
57
57
 
58
58
  expect(state.get()).toMatchObject({authState: {type: AuthStateType.LOGGED_OUT}})
59
59
 
@@ -88,8 +88,8 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
88
88
  const clientFactory = vi.fn().mockReturnValue(mockClient)
89
89
  const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
90
90
 
91
- const state = createStoreState(authStore.getInitialState(instance))
92
- const subscription = subscribeToStateAndFetchCurrentUser({state, instance})
91
+ const state = createStoreState(authStore.getInitialState(instance, null))
92
+ const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
93
93
 
94
94
  expect(state.get()).toMatchObject({authState: {type: AuthStateType.LOGGED_OUT}})
95
95
 
@@ -34,9 +34,9 @@ describe('subscribeToStorageEventsAndSetToken', () => {
34
34
  })
35
35
 
36
36
  it('sets the state to logged in when a matching storage event returns a token', () => {
37
- const state = createStoreState(authStore.getInitialState(instance))
37
+ const state = createStoreState(authStore.getInitialState(instance, null))
38
38
  const {storageKey} = state.get().options
39
- const subscription = subscribeToStorageEventsAndSetToken({state, instance})
39
+ const subscription = subscribeToStorageEventsAndSetToken({state, instance, key: null})
40
40
 
41
41
  expect(state.get()).toMatchObject({
42
42
  authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
@@ -54,10 +54,10 @@ describe('subscribeToStorageEventsAndSetToken', () => {
54
54
 
55
55
  it('sets the state to logged in when a matching storage event returns null', () => {
56
56
  vi.mocked(getTokenFromStorage).mockReturnValue('existing-token')
57
- const state = createStoreState(authStore.getInitialState(instance))
57
+ const state = createStoreState(authStore.getInitialState(instance, null))
58
58
  const {storageKey} = state.get().options
59
59
 
60
- const subscription = subscribeToStorageEventsAndSetToken({state, instance})
60
+ const subscription = subscribeToStorageEventsAndSetToken({state, instance, key: null})
61
61
 
62
62
  expect(state.get()).toMatchObject({
63
63
  authState: {type: AuthStateType.LOGGED_IN, token: 'existing-token', currentUser: null},
@@ -3,7 +3,6 @@ import {Subject} from 'rxjs'
3
3
  import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
 
5
5
  import {getAuthMethodState, getTokenState} from '../auth/authStore'
6
- import {canvasSource, datasetSource, mediaLibrarySource} from '../config/sanityConfig'
7
6
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
8
7
  import {getClient, getClientState} from './clientStore'
9
8
 
@@ -34,7 +33,10 @@ beforeEach(() => {
34
33
  vi.mocked(createClient).mockImplementation(
35
34
  (clientConfig) => ({config: () => clientConfig}) as SanityClient,
36
35
  )
37
- instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
36
+ instance = createSanityInstance({
37
+ projectId: 'test-project',
38
+ dataset: 'test-dataset',
39
+ })
38
40
  })
39
41
 
40
42
  afterEach(() => {
@@ -176,18 +178,15 @@ describe('clientStore', () => {
176
178
 
177
179
  describe('source handling', () => {
178
180
  it('should create client when source is provided', () => {
179
- const source = datasetSource('source-project', 'source-dataset')
180
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
181
+ const client = getClient(instance, {
182
+ apiVersion: '2024-11-12',
183
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
184
+ })
181
185
 
182
186
  expect(vi.mocked(createClient)).toHaveBeenCalledWith(
183
187
  expect.objectContaining({
184
- apiVersion: '2024-11-12',
185
- source: expect.objectContaining({
186
- __sanity_internal_sourceId: {
187
- projectId: 'source-project',
188
- dataset: 'source-dataset',
189
- },
190
- }),
188
+ 'apiVersion': '2024-11-12',
189
+ '~experimental_resource': {type: 'dataset', id: 'source-project.source-dataset'},
191
190
  }),
192
191
  )
193
192
  // Client should be projectless - no projectId/dataset in config
@@ -195,19 +194,16 @@ describe('clientStore', () => {
195
194
  expect(client.config()).not.toHaveProperty('dataset')
196
195
  expect(client.config()).toEqual(
197
196
  expect.objectContaining({
198
- source: expect.objectContaining({
199
- __sanity_internal_sourceId: {
200
- projectId: 'source-project',
201
- dataset: 'source-dataset',
202
- },
203
- }),
197
+ '~experimental_resource': {type: 'dataset', id: 'source-project.source-dataset'},
204
198
  }),
205
199
  )
206
200
  })
207
201
 
208
202
  it('should create resource when source has array sourceId and be projectless', () => {
209
- const source = mediaLibrarySource('media-lib-123')
210
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
203
+ const client = getClient(instance, {
204
+ apiVersion: '2024-11-12',
205
+ source: {mediaLibraryId: 'media-lib-123'},
206
+ })
211
207
 
212
208
  expect(vi.mocked(createClient)).toHaveBeenCalledWith(
213
209
  expect.objectContaining({
@@ -226,8 +222,10 @@ describe('clientStore', () => {
226
222
  })
227
223
 
228
224
  it('should create resource when canvas source is provided and be projectless', () => {
229
- const source = canvasSource('canvas-123')
230
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
225
+ const client = getClient(instance, {
226
+ apiVersion: '2024-11-12',
227
+ source: {canvasId: 'canvas-123'},
228
+ })
231
229
 
232
230
  expect(vi.mocked(createClient)).toHaveBeenCalledWith(
233
231
  expect.objectContaining({
@@ -246,30 +244,26 @@ describe('clientStore', () => {
246
244
  })
247
245
 
248
246
  it('should create projectless client when source is provided, ignoring instance config', () => {
249
- const source = datasetSource('source-project', 'source-dataset')
250
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
247
+ const client = getClient(instance, {
248
+ apiVersion: '2024-11-12',
249
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
250
+ })
251
251
 
252
252
  // Client should be projectless - source takes precedence, instance config is ignored
253
253
  expect(client.config()).not.toHaveProperty('projectId')
254
254
  expect(client.config()).not.toHaveProperty('dataset')
255
255
  expect(client.config()).toEqual(
256
256
  expect.objectContaining({
257
- source: expect.objectContaining({
258
- __sanity_internal_sourceId: {
259
- projectId: 'source-project',
260
- dataset: 'source-dataset',
261
- },
262
- }),
257
+ '~experimental_resource': {type: 'dataset', id: 'source-project.source-dataset'},
263
258
  }),
264
259
  )
265
260
  })
266
261
 
267
262
  it('should warn when both source and explicit projectId/dataset are provided', () => {
268
263
  const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
269
- const source = datasetSource('source-project', 'source-dataset')
270
264
  const client = getClient(instance, {
271
265
  apiVersion: '2024-11-12',
272
- source,
266
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
273
267
  projectId: 'explicit-project',
274
268
  dataset: 'explicit-dataset',
275
269
  })
@@ -284,13 +278,18 @@ describe('clientStore', () => {
284
278
  })
285
279
 
286
280
  it('should create different clients for different sources', () => {
287
- const source1 = datasetSource('project-1', 'dataset-1')
288
- const source2 = datasetSource('project-2', 'dataset-2')
289
- const source3 = mediaLibrarySource('media-lib-1')
290
-
291
- const client1 = getClient(instance, {apiVersion: '2024-11-12', source: source1})
292
- const client2 = getClient(instance, {apiVersion: '2024-11-12', source: source2})
293
- const client3 = getClient(instance, {apiVersion: '2024-11-12', source: source3})
281
+ const client1 = getClient(instance, {
282
+ apiVersion: '2024-11-12',
283
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
284
+ })
285
+ const client2 = getClient(instance, {
286
+ apiVersion: '2024-11-12',
287
+ source: {mediaLibraryId: 'media-lib-123'},
288
+ })
289
+ const client3 = getClient(instance, {
290
+ apiVersion: '2024-11-12',
291
+ source: {canvasId: 'canvas-123'},
292
+ })
294
293
 
295
294
  expect(client1).not.toBe(client2)
296
295
  expect(client2).not.toBe(client3)
@@ -299,11 +298,14 @@ describe('clientStore', () => {
299
298
  })
300
299
 
301
300
  it('should reuse clients with identical source configurations', () => {
302
- const source = datasetSource('same-project', 'same-dataset')
303
- const options = {apiVersion: '2024-11-12', source}
304
-
305
- const client1 = getClient(instance, options)
306
- const client2 = getClient(instance, options)
301
+ const client1 = getClient(instance, {
302
+ apiVersion: '2024-11-12',
303
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
304
+ })
305
+ const client2 = getClient(instance, {
306
+ apiVersion: '2024-11-12',
307
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
308
+ })
307
309
 
308
310
  expect(client1).toBe(client2)
309
311
  expect(vi.mocked(createClient)).toHaveBeenCalledTimes(1)
@@ -2,7 +2,12 @@ import {type ClientConfig, createClient, type SanityClient} from '@sanity/client
2
2
  import {pick} from 'lodash-es'
3
3
 
4
4
  import {getAuthMethodState, getTokenState} from '../auth/authStore'
5
- import {type DocumentSource, SOURCE_ID} from '../config/sanityConfig'
5
+ import {
6
+ type DocumentSource,
7
+ isCanvasSource,
8
+ isDatasetSource,
9
+ isMediaLibrarySource,
10
+ } from '../config/sanityConfig'
6
11
  import {bindActionGlobally} from '../store/createActionBinder'
7
12
  import {createStateSourceAction} from '../store/createStateSourceAction'
8
13
  import {defineStore, type StoreContext} from '../store/defineStore'
@@ -61,6 +66,11 @@ export interface ClientStoreState {
61
66
  authMethod?: 'localstorage' | 'cookie'
62
67
  }
63
68
 
69
+ interface ClientResource {
70
+ type: 'dataset' | 'media-library' | 'canvas'
71
+ id: string
72
+ }
73
+
64
74
  /**
65
75
  * Options used when retrieving a client instance from the client store.
66
76
  *
@@ -170,13 +180,17 @@ export const getClient = bindActionGlobally(
170
180
 
171
181
  const tokenFromState = state.get().token
172
182
  const {clients, authMethod} = state.get()
173
- const hasSource = !!options.source
174
- let sourceId = options.source?.[SOURCE_ID]
175
183
 
176
- let resource
177
- if (Array.isArray(sourceId)) {
178
- resource = {type: sourceId[0], id: sourceId[1]}
179
- sourceId = undefined
184
+ let resource: ClientResource | undefined
185
+
186
+ if (options.source) {
187
+ if (isDatasetSource(options.source)) {
188
+ resource = {type: 'dataset', id: `${options.source.projectId}.${options.source.dataset}`}
189
+ } else if (isMediaLibrarySource(options.source)) {
190
+ resource = {type: 'media-library', id: options.source.mediaLibraryId}
191
+ } else if (isCanvasSource(options.source)) {
192
+ resource = {type: 'canvas', id: options.source.canvasId}
193
+ }
180
194
  }
181
195
 
182
196
  const projectId = options.projectId ?? instance.config.projectId
@@ -185,7 +199,7 @@ export const getClient = bindActionGlobally(
185
199
 
186
200
  const effectiveOptions: ClientOptions = {
187
201
  ...DEFAULT_CLIENT_CONFIG,
188
- ...((options.scope === 'global' || !projectId || hasSource) && {useProjectHostname: false}),
202
+ ...((options.scope === 'global' || !projectId || resource) && {useProjectHostname: false}),
189
203
  token: authMethod === 'cookie' ? undefined : (tokenFromState ?? undefined),
190
204
  ...options,
191
205
  ...(projectId && {projectId}),
@@ -197,7 +211,7 @@ export const getClient = bindActionGlobally(
197
211
  // When a source is provided, don't use projectId/dataset - the client should be "projectless"
198
212
  // The client code itself will ignore the non-source config, so we do this to prevent confusing the user.
199
213
  // (ref: https://github.com/sanity-io/client/blob/5c23f81f5ab93a53f5b22b39845c867988508d84/src/data/dataMethods.ts#L691)
200
- if (hasSource) {
214
+ if (resource) {
201
215
  if (options.projectId || options.dataset) {
202
216
  // eslint-disable-next-line no-console
203
217
  console.warn(
@@ -39,7 +39,7 @@ describe('destroyController', () => {
39
39
  })
40
40
 
41
41
  // Execute action
42
- destroyController({state, instance})
42
+ destroyController({state, instance, key: null})
43
43
 
44
44
  // Verify controller was destroyed and state was cleared
45
45
  expect(mockController.destroy).toHaveBeenCalled()
@@ -49,7 +49,7 @@ describe('destroyController', () => {
49
49
 
50
50
  it('should do nothing if no controller exists', () => {
51
51
  // State already has null controller, so just execute action
52
- expect(() => destroyController({state, instance})).not.toThrow()
52
+ expect(() => destroyController({state, instance, key: null})).not.toThrow()
53
53
 
54
54
  // State should remain unchanged
55
55
  expect(state.get().controller).toBeNull()