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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/README.md +12 -0
  2. package/dist/_exports/_internal.d.ts +163 -0
  3. package/dist/_exports/_internal.d.ts.map +1 -0
  4. package/dist/_exports/_internal.js +332 -0
  5. package/dist/_exports/_internal.js.map +1 -0
  6. package/dist/_exports/agent.d.ts +65 -0
  7. package/dist/_exports/agent.d.ts.map +1 -0
  8. package/dist/_exports/agent.js +71 -0
  9. package/dist/_exports/agent.js.map +1 -0
  10. package/dist/_exports/comlink.d.ts +118 -0
  11. package/dist/_exports/comlink.d.ts.map +1 -0
  12. package/dist/_exports/comlink.js +69 -0
  13. package/dist/_exports/comlink.js.map +1 -0
  14. package/dist/authStore-CtE9bLHW.js +754 -0
  15. package/dist/authStore-CtE9bLHW.js.map +1 -0
  16. package/dist/authStore-DKFX8fn7.d.ts +3179 -0
  17. package/dist/authStore-DKFX8fn7.d.ts.map +1 -0
  18. package/dist/clientStore-BtE2-q2i.js +90 -0
  19. package/dist/clientStore-BtE2-q2i.js.map +1 -0
  20. package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
  21. package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
  22. package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
  23. package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
  24. package/dist/getEnv-D5RGkQLu.js +8 -0
  25. package/dist/getEnv-D5RGkQLu.js.map +1 -0
  26. package/dist/getNodeState-CR3__B2D.js +67 -0
  27. package/dist/getNodeState-CR3__B2D.js.map +1 -0
  28. package/dist/index.d.ts +3 -2332
  29. package/dist/index.js +4171 -4332
  30. package/dist/index.js.map +1 -1
  31. package/dist/logger-DAtJb5RQ.js +173 -0
  32. package/dist/logger-DAtJb5RQ.js.map +1 -0
  33. package/dist/object-CxXlSVg7.js +83 -0
  34. package/dist/object-CxXlSVg7.js.map +1 -0
  35. package/dist/queryStore-DPf9sN3V.js +1346 -0
  36. package/dist/queryStore-DPf9sN3V.js.map +1 -0
  37. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  38. package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
  39. package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
  40. package/dist/telemetryManager-Dq_qCkeI.js +127 -0
  41. package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
  42. package/dist/version-BChjsi4u.js +9 -0
  43. package/dist/version-BChjsi4u.js.map +1 -0
  44. package/package.json +47 -52
  45. package/src/_exports/_internal.ts +27 -0
  46. package/src/_exports/agent.ts +19 -0
  47. package/src/_exports/comlink.ts +16 -0
  48. package/src/_exports/index.ts +125 -64
  49. package/src/access/checkPermissions.test.ts +55 -0
  50. package/src/access/checkPermissions.ts +54 -0
  51. package/src/agent/agentActions.test.ts +16 -60
  52. package/src/agent/agentActions.ts +25 -38
  53. package/src/applications/applications.test-d.ts +88 -0
  54. package/src/applications/applications.test.ts +130 -0
  55. package/src/applications/applications.ts +367 -0
  56. package/src/auth/authLogger.ts +30 -0
  57. package/src/auth/authStore.test.ts +194 -13
  58. package/src/auth/authStore.ts +58 -26
  59. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  60. package/src/auth/getOrganizationVerificationState.ts +4 -4
  61. package/src/auth/handleAuthCallback.test.ts +35 -1
  62. package/src/auth/handleAuthCallback.ts +25 -6
  63. package/src/auth/logout.test.ts +75 -2
  64. package/src/auth/logout.ts +23 -4
  65. package/src/auth/refreshStampedToken.test.ts +219 -469
  66. package/src/auth/refreshStampedToken.ts +79 -195
  67. package/src/auth/studioAuth.ts +1 -1
  68. package/src/auth/studioModeAuth.ts +2 -1
  69. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  70. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  71. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  72. package/src/client/clientStore.test.ts +62 -37
  73. package/src/client/clientStore.ts +45 -75
  74. package/src/client/liveEvents.test.ts +151 -0
  75. package/src/client/liveEvents.ts +107 -0
  76. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  77. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  78. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  79. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  80. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  81. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  82. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  83. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  84. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  85. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  86. package/src/comlink/node/getNodeState.test.ts +1 -1
  87. package/src/comlink/node/getNodeState.ts +2 -1
  88. package/src/comments/addonDatasetStore.test.ts +215 -0
  89. package/src/comments/addonDatasetStore.ts +249 -0
  90. package/src/comments/buildCommentThreads.test.ts +83 -0
  91. package/src/comments/buildCommentThreads.ts +54 -0
  92. package/src/comments/commentActions.test.ts +540 -0
  93. package/src/comments/commentActions.ts +555 -0
  94. package/src/comments/commentFieldPath.test.ts +40 -0
  95. package/src/comments/commentFieldPath.ts +44 -0
  96. package/src/comments/commentsConstants.ts +80 -0
  97. package/src/comments/commentsStore.test.ts +419 -0
  98. package/src/comments/commentsStore.ts +483 -0
  99. package/src/comments/normalizeComment.test.ts +116 -0
  100. package/src/comments/normalizeComment.ts +45 -0
  101. package/src/comments/observeComments.test.ts +193 -0
  102. package/src/comments/observeComments.ts +113 -0
  103. package/src/comments/reducers.test.ts +277 -0
  104. package/src/comments/reducers.ts +316 -0
  105. package/src/comments/types.ts +249 -0
  106. package/src/comments/weakenReferences.test.ts +47 -0
  107. package/src/comments/weakenReferences.ts +29 -0
  108. package/src/config/__tests__/handles.test.ts +18 -12
  109. package/src/config/handles.ts +25 -7
  110. package/src/config/sanityConfig.ts +40 -82
  111. package/src/datasets/datasets.test.ts +2 -2
  112. package/src/datasets/datasets.ts +10 -4
  113. package/src/document/actions.test.ts +115 -33
  114. package/src/document/actions.ts +212 -15
  115. package/src/document/applyDocumentActions.test.ts +35 -13
  116. package/src/document/applyDocumentActions.ts +30 -9
  117. package/src/document/documentConstants.ts +15 -0
  118. package/src/document/documentStore.concurrency.test.ts +1306 -0
  119. package/src/document/documentStore.test.ts +753 -273
  120. package/src/document/documentStore.ts +197 -75
  121. package/src/document/events.test.ts +57 -2
  122. package/src/document/events.ts +82 -26
  123. package/src/document/listen.test.ts +56 -0
  124. package/src/document/listen.ts +74 -21
  125. package/src/document/listenerEventOperators.test.ts +311 -0
  126. package/src/document/listenerEventOperators.ts +217 -0
  127. package/src/document/patchOperations.test.ts +76 -2
  128. package/src/document/patchOperations.ts +60 -1
  129. package/src/document/permissions.test.ts +107 -60
  130. package/src/document/permissions.ts +44 -19
  131. package/src/document/processActions/create.ts +138 -0
  132. package/src/document/processActions/delete.ts +100 -0
  133. package/src/document/processActions/discard.ts +63 -0
  134. package/src/document/processActions/edit.ts +154 -0
  135. package/src/document/processActions/processActions.ts +218 -0
  136. package/src/document/processActions/publish.ts +120 -0
  137. package/src/document/processActions/releaseArchive.ts +77 -0
  138. package/src/document/processActions/releaseCreate.ts +59 -0
  139. package/src/document/processActions/releaseDelete.ts +65 -0
  140. package/src/document/processActions/releaseEdit.ts +37 -0
  141. package/src/document/processActions/releasePublish.ts +45 -0
  142. package/src/document/processActions/releaseSchedule.ts +87 -0
  143. package/src/document/processActions/releaseUtil.ts +31 -0
  144. package/src/document/processActions/shared.ts +308 -0
  145. package/src/document/processActions/unpublish.ts +85 -0
  146. package/src/document/processActions.test.ts +1129 -121
  147. package/src/document/reducers.test.ts +772 -64
  148. package/src/document/reducers.ts +205 -51
  149. package/src/document/resourceRules.test.ts +178 -0
  150. package/src/document/resourceRules.ts +117 -0
  151. package/src/document/sharedListener.test.ts +16 -14
  152. package/src/document/sharedListener.ts +24 -8
  153. package/src/document/util.test.ts +46 -0
  154. package/src/document/util.ts +50 -0
  155. package/src/favorites/favorites.test.ts +5 -6
  156. package/src/installations/installations.test-d.ts +73 -0
  157. package/src/installations/installations.test.ts +62 -0
  158. package/src/installations/installations.ts +206 -0
  159. package/src/organization/organization.test-d.ts +36 -0
  160. package/src/organization/organization.test.ts +138 -0
  161. package/src/organization/organization.ts +138 -0
  162. package/src/organizations/organizations.test-d.ts +56 -0
  163. package/src/organizations/organizations.test.ts +150 -0
  164. package/src/organizations/organizations.ts +107 -0
  165. package/src/presence/bifurTransport.test.ts +201 -9
  166. package/src/presence/bifurTransport.ts +98 -14
  167. package/src/presence/paths.test.ts +70 -0
  168. package/src/presence/paths.ts +41 -0
  169. package/src/presence/presenceStore.test.ts +915 -24
  170. package/src/presence/presenceStore.ts +576 -104
  171. package/src/presence/types.ts +175 -3
  172. package/src/preview/previewProjectionUtils.test.ts +9 -22
  173. package/src/preview/previewProjectionUtils.ts +7 -3
  174. package/src/preview/types.ts +0 -15
  175. package/src/project/project.test-d.ts +37 -0
  176. package/src/project/project.test.ts +157 -12
  177. package/src/project/project.ts +138 -25
  178. package/src/projection/getProjectionState.test.ts +18 -20
  179. package/src/projection/getProjectionState.ts +23 -3
  180. package/src/projection/projectionQuery.ts +5 -5
  181. package/src/projection/projectionStore.test.ts +1 -9
  182. package/src/projection/resolveProjection.test.ts +1 -2
  183. package/src/projection/resolveProjection.ts +16 -0
  184. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  185. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  186. package/src/projects/projects.test-d.ts +32 -0
  187. package/src/projects/projects.test.ts +108 -42
  188. package/src/projects/projects.ts +63 -27
  189. package/src/query/queryStore.test.ts +259 -123
  190. package/src/query/queryStore.ts +85 -69
  191. package/src/query/reducers.test.ts +0 -42
  192. package/src/query/reducers.ts +2 -18
  193. package/src/releases/getPerspectiveState.test.ts +17 -23
  194. package/src/releases/getPerspectiveState.ts +8 -20
  195. package/src/releases/observeReleases.test.ts +127 -0
  196. package/src/releases/observeReleases.ts +102 -0
  197. package/src/releases/releasesStore.test.ts +100 -53
  198. package/src/releases/releasesStore.ts +69 -44
  199. package/src/releases/utils/sortReleases.test.ts +38 -2
  200. package/src/releases/utils/sortReleases.ts +15 -10
  201. package/src/store/createActionBinder.test.ts +109 -90
  202. package/src/store/createActionBinder.ts +36 -28
  203. package/src/store/createSanityInstance.test.ts +15 -13
  204. package/src/store/createSanityInstance.ts +14 -2
  205. package/src/store/createStateSourceAction.test.ts +64 -2
  206. package/src/store/createStateSourceAction.ts +38 -43
  207. package/src/store/createStoreInstance.test.ts +4 -2
  208. package/src/store/fetcherStore.test-d.ts +121 -0
  209. package/src/store/fetcherStore.test.ts +299 -0
  210. package/src/store/fetcherStore.ts +801 -0
  211. package/src/telemetry/environment.test.ts +119 -0
  212. package/src/telemetry/environment.ts +92 -0
  213. package/src/telemetry/events.ts +42 -0
  214. package/src/telemetry/initTelemetry.test.ts +432 -0
  215. package/src/telemetry/initTelemetry.ts +218 -0
  216. package/src/telemetry/telemetryManager.test.ts +327 -0
  217. package/src/telemetry/telemetryManager.ts +199 -0
  218. package/src/users/reducers.test.ts +3 -1
  219. package/src/users/reducers.ts +5 -6
  220. package/src/users/types.ts +4 -4
  221. package/src/users/usersStore.test.ts +16 -12
  222. package/src/users/usersStore.ts +5 -1
  223. package/src/utils/buildQuery.test.ts +14 -0
  224. package/src/utils/buildQuery.ts +17 -0
  225. package/src/utils/createFetcherStore.test.ts +7 -5
  226. package/src/utils/createFetcherStore.ts +8 -5
  227. package/src/utils/getStagingApiHost.test.ts +21 -0
  228. package/src/utils/getStagingApiHost.ts +14 -0
  229. package/src/utils/ids.test.ts +1 -29
  230. package/src/utils/ids.ts +0 -10
  231. package/src/utils/includeShape.ts +36 -0
  232. package/src/utils/isImportError.test.ts +72 -0
  233. package/src/utils/isImportError.ts +34 -0
  234. package/src/utils/logger.test.ts +12 -0
  235. package/src/utils/logger.ts +8 -3
  236. package/src/utils/object.test.ts +95 -0
  237. package/src/utils/object.ts +142 -0
  238. package/src/utils/setCleanupTimeout.ts +37 -0
  239. package/src/document/processActions.ts +0 -707
  240. package/src/utils/defineIntent.test.ts +0 -477
  241. package/src/utils/defineIntent.ts +0 -244
@@ -1,4 +1,4 @@
1
- import {type ReleaseDocument} from '../releasesStore'
1
+ import {type ReleaseDocument} from '@sanity/client'
2
2
 
3
3
  // mirrors the order of the releases in the releases list in Studio
4
4
  // https://github.com/sanity-io/sanity/blob/main/packages/sanity/src/core/releases/hooks/utils.ts
@@ -6,25 +6,30 @@ export function sortReleases(releases: ReleaseDocument[] = []): ReleaseDocument[
6
6
  // The order should always be:
7
7
  // [undecided (sortByCreatedAt), scheduled(sortBy publishAt || metadata.intendedPublishAt), asap(sortByCreatedAt)]
8
8
  return [...releases].sort((a, b) => {
9
+ // metadata is optional on a release document; guard against releases that
10
+ // have no metadata at all so sorting can't throw and wipe out the list.
11
+ const aType = a.metadata?.releaseType
12
+ const bType = b.metadata?.releaseType
13
+
9
14
  // undecided are always first, then by createdAt descending
10
- if (a.metadata.releaseType === 'undecided' && b.metadata.releaseType !== 'undecided') {
15
+ if (aType === 'undecided' && bType !== 'undecided') {
11
16
  return -1
12
17
  }
13
- if (a.metadata.releaseType !== 'undecided' && b.metadata.releaseType === 'undecided') {
18
+ if (aType !== 'undecided' && bType === 'undecided') {
14
19
  return 1
15
20
  }
16
- if (a.metadata.releaseType === 'undecided' && b.metadata.releaseType === 'undecided') {
21
+ if (aType === 'undecided' && bType === 'undecided') {
17
22
  // Sort by createdAt
18
23
  return new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime()
19
24
  }
20
25
 
21
26
  // Scheduled are always at the middle, then by publishAt descending
22
- if (a.metadata.releaseType === 'scheduled' && b.metadata.releaseType === 'scheduled') {
23
- const aPublishAt = a['publishAt'] || a.metadata['intendedPublishAt']
27
+ if (aType === 'scheduled' && bType === 'scheduled') {
28
+ const aPublishAt = a['publishAt'] || a.metadata?.['intendedPublishAt']
24
29
  if (!aPublishAt) {
25
30
  return 1
26
31
  }
27
- const bPublishAt = b['publishAt'] || b.metadata['intendedPublishAt']
32
+ const bPublishAt = b['publishAt'] || b.metadata?.['intendedPublishAt']
28
33
  if (!bPublishAt) {
29
34
  return -1
30
35
  }
@@ -32,13 +37,13 @@ export function sortReleases(releases: ReleaseDocument[] = []): ReleaseDocument[
32
37
  }
33
38
 
34
39
  // ASAP are always last, then by createdAt descending
35
- if (a.metadata.releaseType === 'asap' && b.metadata.releaseType !== 'asap') {
40
+ if (aType === 'asap' && bType !== 'asap') {
36
41
  return 1
37
42
  }
38
- if (a.metadata.releaseType !== 'asap' && b.metadata.releaseType === 'asap') {
43
+ if (aType !== 'asap' && bType === 'asap') {
39
44
  return -1
40
45
  }
41
- if (a.metadata.releaseType === 'asap' && b.metadata.releaseType === 'asap') {
46
+ if (aType === 'asap' && bType === 'asap') {
42
47
  // Sort by createdAt
43
48
  return new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime()
44
49
  }
@@ -29,7 +29,7 @@ describe('createActionBinder', () => {
29
29
  return context.state.counter
30
30
  })
31
31
  const boundAction = binder(storeDefinition, action)
32
- const instance = createSanityInstance()
32
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
33
33
 
34
34
  // First call creates store instance
35
35
  const result1 = boundAction(instance, 5)
@@ -43,9 +43,9 @@ describe('createActionBinder', () => {
43
43
  })
44
44
 
45
45
  it('should create separate store instances for different composite keys', () => {
46
- const binder = createActionBinder((instance, ..._rest) => {
47
- return {name: instance.instanceId}
48
- })
46
+ const binder = createActionBinder(({config: {projectId, dataset}}, ..._rest) => ({
47
+ name: `${projectId}.${dataset}`,
48
+ }))
49
49
  const storeDefinition = {
50
50
  name: 'TestStore',
51
51
  getInitialState: () => ({counter: 0}),
@@ -55,8 +55,8 @@ describe('createActionBinder', () => {
55
55
  return context.state.counter
56
56
  })
57
57
  const boundAction = binder(storeDefinition, action)
58
- const instanceA = createSanityInstance()
59
- const instanceB = createSanityInstance()
58
+ const instanceA = createSanityInstance({projectId: 'p1', dataset: 'd1'})
59
+ const instanceB = createSanityInstance({projectId: 'p2', dataset: 'd2'})
60
60
 
61
61
  const resultA = boundAction(instanceA, 3)
62
62
  const resultB = boundAction(instanceB, 4)
@@ -74,8 +74,8 @@ describe('createActionBinder', () => {
74
74
  }
75
75
  const action = vi.fn((context) => context.state.counter)
76
76
  const boundAction = binder(storeDefinition, action)
77
- const instance1 = createSanityInstance()
78
- const instance2 = createSanityInstance()
77
+ const instance1 = createSanityInstance({projectId: 'p1', dataset: 'd1'})
78
+ const instance2 = createSanityInstance({projectId: 'p1', dataset: 'd1'})
79
79
 
80
80
  // Call action on both instances
81
81
  boundAction(instance1)
@@ -105,8 +105,8 @@ describe('bindActionGlobally', () => {
105
105
  const boundAction = bindActionGlobally(storeDefinition, action)
106
106
 
107
107
  // Create instances with different configs
108
- const instance1 = createSanityInstance()
109
- const instance2 = createSanityInstance()
108
+ const instance1 = createSanityInstance({projectId: 'any', dataset: 'any'})
109
+ const instance2 = createSanityInstance({projectId: 'different', dataset: 'config'})
110
110
 
111
111
  // Both instances should use the same store
112
112
  const result1 = boundAction(instance1, 42)
@@ -133,47 +133,42 @@ describe('bindActionGlobally', () => {
133
133
  })
134
134
 
135
135
  describe('bindActionByResource', () => {
136
- it('should resolve from config.defaultResource when no resource provided', () => {
136
+ it('should throw an error when provided an invalid resource', () => {
137
137
  const storeDefinition = {
138
- name: 'ResourceNamedStore',
138
+ name: 'SourceStore',
139
139
  getInitialState: () => ({counter: 0}),
140
140
  }
141
- const action = vi.fn((context) => context.key)
141
+ const action = vi.fn((_context) => 'success')
142
142
  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
- )
143
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
144
+
145
+ expect(() =>
146
+ boundAction(instance, {resource: {invalid: 'resource'} as unknown as DocumentResource}),
147
+ ).toThrow('Received invalid resource:')
153
148
  })
154
149
 
155
- it('should throw an error when provided an invalid resource', () => {
150
+ it('should throw an error when no resource provided and projectId/dataset are missing', () => {
156
151
  const storeDefinition = {
157
- name: 'ResourceStore',
152
+ name: 'SourceStore',
158
153
  getInitialState: () => ({counter: 0}),
159
154
  }
160
155
  const action = vi.fn((_context) => 'success')
161
156
  const boundAction = bindActionByResource(storeDefinition, action)
162
- const instance = createSanityInstance()
157
+ const instance = createSanityInstance({projectId: '', dataset: ''})
163
158
 
164
- expect(() =>
165
- boundAction(instance, {resource: {invalid: 'resource'} as unknown as DocumentResource}),
166
- ).toThrow('Received invalid resource:')
159
+ expect(() => boundAction(instance, {})).toThrow(
160
+ 'This API requires a project ID and dataset configured.',
161
+ )
167
162
  })
168
163
 
169
164
  it('should work correctly with a valid dataset resource', () => {
170
165
  const storeDefinition = {
171
- name: 'ResourceStore',
166
+ name: 'SourceStore',
172
167
  getInitialState: () => ({counter: 0}),
173
168
  }
174
169
  const action = vi.fn((_context) => 'success')
175
170
  const boundAction = bindActionByResource(storeDefinition, action)
176
- const instance = createSanityInstance()
171
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
177
172
 
178
173
  const result = boundAction(instance, {
179
174
  resource: {projectId: 'proj2', dataset: 'ds2'},
@@ -182,6 +177,57 @@ describe('bindActionByResource', () => {
182
177
  })
183
178
  })
184
179
 
180
+ describe('instance default resource', () => {
181
+ const storeDefinition = {
182
+ name: 'DefaultResourceStore',
183
+ getInitialState: () => ({counter: 0}),
184
+ }
185
+ const whichResource = bindActionByResource(storeDefinition, ({key}) => key)
186
+
187
+ it('uses config.resource when no resource is passed', () => {
188
+ const instance = createSanityInstance({resource: {projectId: 'p', dataset: 'd'}})
189
+ expect(whichResource(instance, {})).toMatchObject({
190
+ name: 'p.d',
191
+ resource: {projectId: 'p', dataset: 'd'},
192
+ })
193
+ instance.dispose()
194
+ })
195
+
196
+ it('supports a media library as the instance default', () => {
197
+ const instance = createSanityInstance({resource: {mediaLibraryId: 'ml-123'}})
198
+ expect(whichResource(instance, {})).toMatchObject({
199
+ name: 'media-library:ml-123',
200
+ resource: {mediaLibraryId: 'ml-123'},
201
+ })
202
+ instance.dispose()
203
+ })
204
+
205
+ it('supports a canvas as the instance default', () => {
206
+ const instance = createSanityInstance({resource: {canvasId: 'c-1'}})
207
+ expect(whichResource(instance, {})).toMatchObject({
208
+ name: 'canvas:c-1',
209
+ resource: {canvasId: 'c-1'},
210
+ })
211
+ instance.dispose()
212
+ })
213
+
214
+ it('lets a per-call resource override the instance default', () => {
215
+ const instance = createSanityInstance({resource: {mediaLibraryId: 'ml-123'}})
216
+ expect(whichResource(instance, {resource: {projectId: 'x', dataset: 'y'}})).toMatchObject({
217
+ resource: {projectId: 'x', dataset: 'y'},
218
+ })
219
+ instance.dispose()
220
+ })
221
+
222
+ it('still accepts the projectId/dataset shorthand', () => {
223
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
224
+ expect(whichResource(instance, {})).toMatchObject({
225
+ resource: {projectId: 'p', dataset: 'd'},
226
+ })
227
+ instance.dispose()
228
+ })
229
+ })
230
+
185
231
  describe('bindActionByResourceAndPerspective', () => {
186
232
  it('should throw an error when provided an invalid resource', () => {
187
233
  const storeDefinition = {
@@ -190,7 +236,7 @@ describe('bindActionByResourceAndPerspective', () => {
190
236
  }
191
237
  const action = vi.fn((_context) => 'success')
192
238
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
193
- const instance = createSanityInstance()
239
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
194
240
 
195
241
  expect(() =>
196
242
  boundAction(instance, {
@@ -200,35 +246,34 @@ describe('bindActionByResourceAndPerspective', () => {
200
246
  ).toThrow('Received invalid resource:')
201
247
  })
202
248
 
203
- it('should work correctly with a valid dataset resource and explicit perspective', () => {
249
+ it('should throw an error when no resource provided and projectId/dataset are missing', () => {
204
250
  const storeDefinition = {
205
251
  name: 'PerspectiveStore',
206
252
  getInitialState: () => ({counter: 0}),
207
253
  }
208
254
  const action = vi.fn((_context) => 'success')
209
255
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
210
- const instance = createSanityInstance()
256
+ const instance = createSanityInstance({projectId: '', dataset: ''})
211
257
 
212
- const result = boundAction(instance, {
213
- resource: {projectId: 'proj2', dataset: 'ds2'},
214
- perspective: 'drafts',
215
- })
216
- expect(result).toBe('success')
258
+ expect(() => boundAction(instance, {perspective: 'drafts'})).toThrow(
259
+ 'This API requires a project ID and dataset configured.',
260
+ )
217
261
  })
218
262
 
219
- it('should throw an error when no resource provided and no default resource configured', () => {
263
+ it('should work correctly with a valid dataset resource and explicit perspective', () => {
220
264
  const storeDefinition = {
221
265
  name: 'PerspectiveStore',
222
266
  getInitialState: () => ({counter: 0}),
223
267
  }
224
268
  const action = vi.fn((_context) => 'success')
225
269
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
226
- const instance = createSanityInstance({})
270
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
227
271
 
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
- )
272
+ const result = boundAction(instance, {
273
+ resource: {projectId: 'proj2', dataset: 'ds2'},
274
+ perspective: 'drafts',
275
+ })
276
+ expect(result).toBe('success')
232
277
  })
233
278
 
234
279
  it('should work correctly with valid dataset resource and no perspective (falls back to drafts)', () => {
@@ -238,7 +283,7 @@ describe('bindActionByResourceAndPerspective', () => {
238
283
  }
239
284
  const action = vi.fn((_context) => 'success')
240
285
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
241
- const instance = createSanityInstance()
286
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
242
287
 
243
288
  const result = boundAction(instance, {
244
289
  resource: {projectId: 'proj1', dataset: 'ds1'},
@@ -246,7 +291,7 @@ describe('bindActionByResourceAndPerspective', () => {
246
291
  expect(result).toBe('success')
247
292
  })
248
293
 
249
- it('should fall back to "drafts" when options.perspective is not provided', () => {
294
+ it('should use instance.config.perspective when options.perspective is not provided', () => {
250
295
  const storeDefinition = {
251
296
  name: 'PerspectiveStore',
252
297
  getInitialState: () => ({counter: 0}),
@@ -254,13 +299,15 @@ describe('bindActionByResourceAndPerspective', () => {
254
299
  const action = vi.fn((context) => context.key)
255
300
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
256
301
  const instance = createSanityInstance({
302
+ projectId: 'proj1',
303
+ dataset: 'ds1',
257
304
  perspective: 'published',
258
305
  })
259
306
 
260
- const result = boundAction(instance, {resource: {projectId: 'proj2', dataset: 'ds2'}})
307
+ const result = boundAction(instance, {})
261
308
  expect(result).toEqual(
262
309
  expect.objectContaining({
263
- name: 'proj2.ds2:published',
310
+ name: 'proj1.ds1:published',
264
311
  perspective: 'published',
265
312
  }),
266
313
  )
@@ -277,18 +324,13 @@ describe('bindActionByResourceAndPerspective', () => {
277
324
  })
278
325
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
279
326
  // Use unique project/dataset so we don't reuse stores from other tests
280
- const instance = createSanityInstance()
327
+ const instance = createSanityInstance({
328
+ projectId: 'perspective-isolation',
329
+ dataset: 'ds1',
330
+ })
281
331
 
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
- )
332
+ const resultDrafts = boundAction(instance, {perspective: 'drafts'}, 3)
333
+ const resultPublished = boundAction(instance, {perspective: 'published'}, 4)
292
334
 
293
335
  expect(resultDrafts).toBe(3)
294
336
  expect(resultPublished).toBe(4)
@@ -303,10 +345,9 @@ describe('bindActionByResourceAndPerspective', () => {
303
345
  }
304
346
  const action = vi.fn((_context) => 'success')
305
347
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
306
- const instance = createSanityInstance()
348
+ const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
307
349
 
308
350
  const result = boundAction(instance, {
309
- resource: {projectId: 'proj1', dataset: 'ds1'},
310
351
  perspective: {releaseName: 'release1'},
311
352
  })
312
353
  expect(result).toBe('success')
@@ -320,23 +361,6 @@ describe('bindActionByResourceAndPerspective', () => {
320
361
  )
321
362
  })
322
363
 
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
364
  it('should reuse same store when same resource and perspective are used', () => {
341
365
  const storeDefinition = {
342
366
  name: 'PerspectiveStore',
@@ -348,18 +372,13 @@ describe('bindActionByResourceAndPerspective', () => {
348
372
  })
349
373
  const boundAction = bindActionByResourceAndPerspective(storeDefinition, action)
350
374
  // Use unique project/dataset so we don't reuse stores from other tests
351
- const instance = createSanityInstance()
375
+ const instance = createSanityInstance({
376
+ projectId: 'perspective-reuse',
377
+ dataset: 'ds1',
378
+ })
352
379
 
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
- )
380
+ const result1 = boundAction(instance, {perspective: 'drafts'}, 2)
381
+ const result2 = boundAction(instance, {perspective: 'drafts'}, 3)
363
382
 
364
383
  expect(result1).toBe(2)
365
384
  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,7 +19,7 @@ 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
21
23
  }
22
24
  /**
23
25
  * Defines a store action that operates on a specific state type
@@ -115,29 +117,34 @@ export function createActionBinder<
115
117
  }
116
118
  }
117
119
 
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
- }
124
-
125
120
  const createResourceKey = (
126
121
  instance: SanityInstance,
127
122
  resource?: DocumentResource,
128
123
  ): BoundResourceKey => {
129
- if (resource) {
130
- return {name: resourceKeyName(resource), resource}
124
+ let name: string | undefined
125
+ let resourceForKey: DocumentResource | undefined
126
+ // A per-call resource wins; otherwise use the instance's default resource.
127
+ const effectiveResource = resource ?? instance.config.resource
128
+ if (effectiveResource) {
129
+ resourceForKey = effectiveResource
130
+ if (isDatasetResource(effectiveResource)) {
131
+ name = `${effectiveResource.projectId}.${effectiveResource.dataset}`
132
+ } else if (isMediaLibraryResource(effectiveResource)) {
133
+ name = `media-library:${effectiveResource.mediaLibraryId}`
134
+ } else if (isCanvasResource(effectiveResource)) {
135
+ name = `canvas:${effectiveResource.canvasId}`
136
+ } else {
137
+ throw new Error(`Received invalid resource: ${JSON.stringify(effectiveResource)}`)
138
+ }
139
+ return {name, resource: resourceForKey}
131
140
  }
132
141
 
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
- )
142
+ // `projectId`/`dataset` on the config are shorthand for a dataset resource.
143
+ const {projectId, dataset} = instance.config
144
+ if (!projectId || !dataset) {
145
+ throw new Error('This API requires a project ID and dataset configured.')
139
146
  }
140
- return {name: resourceKeyName(defaultResource), resource: defaultResource}
147
+ return {name: `${projectId}.${dataset}`, resource: {projectId, dataset}}
141
148
  }
142
149
 
143
150
  /**
@@ -145,6 +152,8 @@ const createResourceKey = (
145
152
  **/
146
153
  export const bindActionByResource = createActionBinder<
147
154
  BoundResourceKey,
155
+ // `resource` is optional because it falls back to the instance config in
156
+ // `createResourceKey` — see the note there on why that fallback still exists.
148
157
  [{resource?: DocumentResource}, ...unknown[]]
149
158
  >((instance, {resource}) => {
150
159
  return createResourceKey(instance, resource)
@@ -161,7 +170,6 @@ export const bindActionByResource = createActionBinder<
161
170
  * can isolate single queries by perspective.
162
171
  *
163
172
  * @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
173
  *
166
174
  * @example
167
175
  * ```ts
@@ -187,10 +195,11 @@ export const bindActionByResource = createActionBinder<
187
195
  */
188
196
  export const bindActionByResourceAndPerspective = createActionBinder<
189
197
  BoundPerspectiveKey,
190
- [ResourceHandle, ...unknown[]]
198
+ [DatasetHandle, ...unknown[]]
191
199
  >((instance, options): BoundPerspectiveKey => {
192
200
  const {resource, perspective} = options
193
- // TODO: remove reference to instance.config.perspective when we get to v3
201
+ // Same trade-off as the resource fallback in `createResourceKey`: configured once on the
202
+ // instance rather than passed to every call. Kept for the same reason.
194
203
  const utilizedPerspective = perspective ?? instance.config.perspective ?? 'drafts'
195
204
  let perspectiveKey: string
196
205
  if (isReleasePerspective(utilizedPerspective)) {
@@ -198,15 +207,14 @@ export const bindActionByResourceAndPerspective = createActionBinder<
198
207
  } else if (typeof utilizedPerspective === 'string') {
199
208
  perspectiveKey = utilizedPerspective
200
209
  } else {
201
- throw new Error(
202
- `Stackable perspectives are not supported. Received perspective: ${JSON.stringify(utilizedPerspective)}`,
203
- )
210
+ // "StackablePerspective", shouldn't be a common case, but just in case
211
+ perspectiveKey = JSON.stringify(utilizedPerspective)
204
212
  }
205
- const resourceKey = createResourceKey(instance, resource)
213
+ const sourceKey = createResourceKey(instance, resource)
206
214
 
207
215
  return {
208
- name: `${resourceKey.name}:${perspectiveKey}`,
209
- resource: resourceKey.resource,
216
+ name: `${sourceKey.name}:${perspectiveKey}`,
217
+ resource: sourceKey.resource,
210
218
  perspective: utilizedPerspective,
211
219
  }
212
220
  })
@@ -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()
@@ -52,30 +52,31 @@ describe('createSanityInstance', () => {
52
52
  })
53
53
 
54
54
  it('should log instance creation at info level', () => {
55
- createSanityInstance()
55
+ createSanityInstance({projectId: 'test-proj', dataset: 'test-ds'})
56
56
 
57
57
  expect(mockHandler.info).toHaveBeenCalledWith(
58
58
  expect.stringContaining('[INFO] [sdk]'),
59
59
  expect.objectContaining({
60
- hasAuth: false,
61
- hasPerspective: false,
60
+ hasProjectId: true,
61
+ hasDataset: true,
62
62
  }),
63
63
  )
64
64
  })
65
65
 
66
66
  it('should log configuration details at debug level', () => {
67
- createSanityInstance({defaultResource: {projectId: 'test-proj', dataset: 'test-dataset'}})
67
+ createSanityInstance({projectId: 'test-proj', dataset: 'test-ds'})
68
68
 
69
69
  expect(mockHandler.debug).toHaveBeenCalledWith(
70
70
  expect.stringContaining('[DEBUG] [sdk]'),
71
71
  expect.objectContaining({
72
- hasStudioConfig: false,
72
+ projectId: 'test-proj',
73
+ dataset: 'test-ds',
73
74
  }),
74
75
  )
75
76
  })
76
77
 
77
78
  it('should log instance disposal', () => {
78
- const instance = createSanityInstance()
79
+ const instance = createSanityInstance({projectId: 'test-proj'})
79
80
  vi.clearAllMocks() // Clear creation logs
80
81
 
81
82
  instance.dispose()
@@ -86,11 +87,12 @@ describe('createSanityInstance', () => {
86
87
  )
87
88
  })
88
89
 
89
- it('should include instance id in logs', () => {
90
- createSanityInstance()
90
+ it('should include instance context in logs', () => {
91
+ createSanityInstance({projectId: 'my-project', dataset: 'my-dataset'})
91
92
 
93
+ // Check that logs include the instance context (project and dataset)
92
94
  expect(mockHandler.info).toHaveBeenCalledWith(
93
- expect.stringMatching(/\[instance:/),
95
+ expect.stringMatching(/\[project:my-project\].*\[dataset:my-dataset\]/),
94
96
  expect.anything(),
95
97
  )
96
98
  })
@@ -3,7 +3,7 @@ import {insecureRandomId} from '../utils/ids'
3
3
  import {createLogger, type InstanceContext} from '../utils/logger'
4
4
 
5
5
  /**
6
- * Represents a Sanity.io resource instance with its own configuration and lifecycle.
6
+ * Represents a Sanity.io resource instance with its own configuration and lifecycle
7
7
  *
8
8
  * @public
9
9
  */
@@ -14,7 +14,9 @@ export interface SanityInstance {
14
14
  */
15
15
  readonly instanceId: string
16
16
 
17
- /** Resolved configuration for this instance */
17
+ /**
18
+ * Resolved configuration for this instance
19
+ */
18
20
  readonly config: SanityConfig
19
21
 
20
22
  /**
@@ -49,18 +51,28 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
49
51
  const disposeListeners = new Map<string, () => void>()
50
52
  const disposed = {current: false}
51
53
 
54
+ // Create instance context for logging
52
55
  const instanceContext: InstanceContext = {
53
56
  instanceId,
57
+ projectId: config.projectId,
58
+ dataset: config.dataset,
54
59
  }
55
60
 
61
+ // Create logger with instance context
56
62
  const logger = createLogger('sdk', {instanceContext})
57
63
 
64
+ // Log instance creation
58
65
  logger.info('Sanity instance created', {
66
+ hasProjectId: !!config.projectId,
67
+ hasDataset: !!config.dataset,
59
68
  hasAuth: !!config.auth,
60
69
  hasPerspective: !!config.perspective,
61
70
  })
62
71
 
72
+ // Log configuration details at debug level
63
73
  logger.debug('Instance configuration', {
74
+ projectId: config.projectId,
75
+ dataset: config.dataset,
64
76
  perspective: config.perspective,
65
77
  hasStudioConfig: !!config.studio,
66
78
  hasStudioTokenSource: !!config.studio?.auth?.token,