@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,14 +1,14 @@
1
+ import {DocumentId, getDraftId, getPublishedId} from '@sanity/id-utils'
1
2
  import {type SanityDocument} from '@sanity/types'
2
3
  import {evaluateSync, type ExprNode, parse} from 'groq-js'
3
4
  import {describe, expect, it} from 'vitest'
4
5
 
5
6
  import {createSanityInstance} from '../store/createSanityInstance'
6
- import {getDraftId, getPublishedId} from '../utils/ids'
7
7
  import {type DocumentAction} from './actions'
8
8
  import {calculatePermissions, createGrantsLookup, type DatasetAcl, type Grant} from './permissions'
9
9
  import {type SyncTransactionState} from './reducers'
10
10
 
11
- const instance = createSanityInstance()
11
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
12
12
 
13
13
  afterAll(() => {
14
14
  instance.dispose()
@@ -32,9 +32,11 @@ const alwaysDeny = parse('false')
32
32
  const createState = (
33
33
  docStates: Record<string, {local: unknown}>,
34
34
  grants?: Record<Grant, ExprNode>,
35
+ identity?: string,
35
36
  ): SyncTransactionState => ({
36
37
  documentStates: docStates as SyncTransactionState['documentStates'],
37
38
  grants,
39
+ identity,
38
40
  queued: [],
39
41
  applied: [],
40
42
  })
@@ -52,6 +54,18 @@ describe('createGrantsLookup', () => {
52
54
  expect(evaluateSync(grants[key], {params: {document: dummyDoc}}).data).toBe(true)
53
55
  })
54
56
  })
57
+
58
+ it('should ignore unknown permissions like "manage"', () => {
59
+ const datasetAcl = [
60
+ {filter: '_id != null', permissions: ['history', 'read', 'update', 'create', 'manage']},
61
+ ] as DatasetAcl
62
+ const grants = createGrantsLookup(datasetAcl)
63
+ const dummyDoc = {_id: 'doc1'}
64
+ ;(['read', 'update', 'create', 'history'] as Grant[]).forEach((key) => {
65
+ expect(grants[key]).toBeDefined()
66
+ expect(evaluateSync(grants[key], {params: {document: dummyDoc}}).data).toBe(true)
67
+ })
68
+ })
55
69
  })
56
70
 
57
71
  describe('calculatePermissions', () => {
@@ -66,19 +80,16 @@ describe('calculatePermissions', () => {
66
80
  // For a document.create action, the selector expects both published and draft keys.
67
81
  const state = createState(
68
82
  {
69
- [getPublishedId('doc1')]: {local: createDoc('doc1', 'Original Title')},
70
- [getDraftId('doc1')]: {local: null},
83
+ [getPublishedId(DocumentId('doc1'))]: {
84
+ local: createDoc(DocumentId('doc1'), 'Original Title'),
85
+ },
86
+ [getDraftId(DocumentId('doc1'))]: {local: null},
71
87
  },
72
88
  defaultGrants,
73
89
  )
74
90
 
75
91
  const actions: DocumentAction[] = [
76
- {
77
- documentId: 'doc1',
78
- type: 'document.create',
79
- documentType: 'article',
80
- resource: {projectId: 'p', dataset: 'd'},
81
- },
92
+ {documentId: 'doc1', type: 'document.create', documentType: 'article'},
82
93
  ]
83
94
  const result = calculatePermissions({instance, state}, {actions})
84
95
  expect(result).toEqual({allowed: true})
@@ -88,18 +99,13 @@ describe('calculatePermissions', () => {
88
99
  // Missing the draft key will cause documentsSelector to return undefined.
89
100
  const state = createState(
90
101
  {
91
- [getPublishedId('doc1')]: {local: createDoc('doc1', 'Title')},
102
+ [getPublishedId(DocumentId('doc1'))]: {local: createDoc(DocumentId('doc1'), 'Title')},
92
103
  // Missing getDraftId('doc1')
93
104
  },
94
105
  defaultGrants,
95
106
  )
96
107
  const actions: DocumentAction[] = [
97
- {
98
- documentId: 'doc1',
99
- type: 'document.create',
100
- documentType: 'article',
101
- resource: {projectId: 'p', dataset: 'd'},
102
- },
108
+ {documentId: 'doc1', type: 'document.create', documentType: 'article'},
103
109
  ]
104
110
  expect(calculatePermissions({instance, state}, {actions})).toBeUndefined()
105
111
  })
@@ -109,18 +115,13 @@ describe('calculatePermissions', () => {
109
115
  const deniedGrants = {...defaultGrants, create: alwaysDeny}
110
116
  const state = createState(
111
117
  {
112
- [getPublishedId('doc1')]: {local: createDoc('doc1', 'Title')},
113
- [getDraftId('doc1')]: {local: null},
118
+ [getPublishedId(DocumentId('doc1'))]: {local: createDoc(DocumentId('doc1'), 'Title')},
119
+ [getDraftId(DocumentId('doc1'))]: {local: null},
114
120
  },
115
121
  deniedGrants,
116
122
  )
117
123
  const actions: DocumentAction[] = [
118
- {
119
- documentId: 'doc1',
120
- type: 'document.create',
121
- documentType: 'article',
122
- resource: {projectId: 'p', dataset: 'd'},
123
- },
124
+ {documentId: 'doc1', type: 'document.create', documentType: 'article'},
124
125
  ]
125
126
  const result = calculatePermissions({instance, state}, {actions})
126
127
  expect(result).toBeDefined()
@@ -142,18 +143,13 @@ describe('calculatePermissions', () => {
142
143
  // Both published and draft documents are present as null.
143
144
  const state = createState(
144
145
  {
145
- [getPublishedId('doc1')]: {local: null},
146
- [getDraftId('doc1')]: {local: null},
146
+ [getPublishedId(DocumentId('doc1'))]: {local: null},
147
+ [getDraftId(DocumentId('doc1'))]: {local: null},
147
148
  },
148
149
  defaultGrants,
149
150
  )
150
151
  const actions: DocumentAction[] = [
151
- {
152
- documentId: 'doc1',
153
- documentType: 'book',
154
- type: 'document.edit',
155
- resource: {projectId: 'p', dataset: 'd'},
156
- },
152
+ {documentId: 'doc1', documentType: 'book', type: 'document.edit'},
157
153
  ]
158
154
  const result = calculatePermissions({instance, state}, {actions})
159
155
  expect(result).toBeDefined()
@@ -173,18 +169,13 @@ describe('calculatePermissions', () => {
173
169
  const deniedGrants = {...defaultGrants, update: alwaysDeny}
174
170
  const state = createState(
175
171
  {
176
- [getPublishedId('doc1')]: {local: createDoc('doc1', 'Title')},
177
- [getDraftId('doc1')]: {local: createDoc(getDraftId('doc1'), 'Draft Title')},
172
+ [getPublishedId(DocumentId('doc1'))]: {local: createDoc(DocumentId('doc1'), 'Title')},
173
+ [getDraftId(DocumentId('doc1'))]: {local: createDoc(DocumentId('doc1'), 'Draft Title')},
178
174
  },
179
175
  deniedGrants,
180
176
  )
181
177
  const actions: DocumentAction[] = [
182
- {
183
- documentId: 'doc1',
184
- documentType: 'book',
185
- type: 'document.edit',
186
- resource: {projectId: 'p', dataset: 'd'},
187
- },
178
+ {documentId: 'doc1', documentType: 'book', type: 'document.edit'},
188
179
  ]
189
180
  const result = calculatePermissions({instance, state}, {actions})
190
181
  expect(result).toBeDefined()
@@ -204,16 +195,11 @@ describe('calculatePermissions', () => {
204
195
 
205
196
  it('should return undefined if grants are not provided', () => {
206
197
  const state = createState({
207
- [getPublishedId('doc1')]: {local: createDoc('doc1', 'Title')},
208
- [getDraftId('doc1')]: {local: null},
198
+ [getPublishedId(DocumentId('doc1'))]: {local: createDoc(DocumentId('doc1'), 'Title')},
199
+ [getDraftId(DocumentId('doc1'))]: {local: null},
209
200
  })
210
201
  const actions: DocumentAction[] = [
211
- {
212
- documentId: 'doc1',
213
- type: 'document.create',
214
- documentType: 'article',
215
- resource: {projectId: 'p', dataset: 'd'},
216
- },
202
+ {documentId: 'doc1', type: 'document.create', documentType: 'article'},
217
203
  ]
218
204
  expect(calculatePermissions({instance, state}, {actions})).toBeUndefined()
219
205
  })
@@ -222,18 +208,13 @@ describe('calculatePermissions', () => {
222
208
  // For document.delete, if the published document is missing, processActions throws an ActionError.
223
209
  const state = createState(
224
210
  {
225
- [getPublishedId('doc1')]: {local: null},
226
- [getDraftId('doc1')]: {local: null},
211
+ [getPublishedId(DocumentId('doc1'))]: {local: null},
212
+ [getDraftId(DocumentId('doc1'))]: {local: null},
227
213
  },
228
214
  defaultGrants,
229
215
  )
230
216
  const actions: DocumentAction[] = [
231
- {
232
- documentId: 'doc1',
233
- documentType: 'book',
234
- type: 'document.delete',
235
- resource: {projectId: 'p', dataset: 'd'},
236
- },
217
+ {documentId: 'doc1', documentType: 'book', type: 'document.delete'},
237
218
  ]
238
219
  const result = calculatePermissions({instance, state}, {actions})
239
220
  expect(result).toBeDefined()
@@ -252,8 +233,8 @@ describe('calculatePermissions', () => {
252
233
  it('should memoize the result for identical state and actions inputs', () => {
253
234
  const state = createState(
254
235
  {
255
- [getPublishedId('doc1')]: {local: createDoc('doc1', 'Title')},
256
- [getDraftId('doc1')]: {local: null},
236
+ [getPublishedId(DocumentId('doc1'))]: {local: createDoc(DocumentId('doc1'), 'Title')},
237
+ [getDraftId(DocumentId('doc1'))]: {local: null},
257
238
  },
258
239
  defaultGrants,
259
240
  )
@@ -261,11 +242,77 @@ describe('calculatePermissions', () => {
261
242
  documentId: 'doc1',
262
243
  type: 'document.create',
263
244
  documentType: 'article',
264
- resource: {projectId: 'p', dataset: 'd'},
265
245
  }
266
246
  // notice how the action is a copy
267
247
  const result1 = calculatePermissions({instance, state}, {actions: [{...action}]})
268
248
  const result2 = calculatePermissions({instance, state}, {actions: [{...action}]})
269
249
  expect(result1).toBe(result2)
270
250
  })
251
+
252
+ describe('identity-aware grants', () => {
253
+ // Mirrors the canvas ACL filter shape: only the document's creator may
254
+ // update it. Without an identity passed to groq, `identity()` is null and
255
+ // the expression collapses to null — which used to read as "denied".
256
+ const canvasUpdateAcl: DatasetAcl = [
257
+ {
258
+ filter: '_type == "sanity.canvas.document" && _system.createdBy == identity()',
259
+ permissions: ['read', 'update'],
260
+ },
261
+ ]
262
+
263
+ const canvasDoc = (createdBy: string): SanityDocument => ({
264
+ _id: 'canvas-1',
265
+ _type: 'sanity.canvas.document',
266
+ _createdAt: '2025-01-01T00:00:00.000Z',
267
+ _updatedAt: '2025-01-01T00:00:00.000Z',
268
+ _rev: 'rev-1',
269
+ // Content Lake sets `_system.createdBy` on canvas documents; the public
270
+ // `DocumentSystem` type doesn't declare it, so the literal needs a cast.
271
+ _system: {createdBy} as SanityDocument['_system'],
272
+ })
273
+
274
+ const editAction: DocumentAction = {
275
+ documentId: 'canvas-1',
276
+ documentType: 'sanity.canvas.document',
277
+ type: 'document.edit',
278
+ liveEdit: true,
279
+ }
280
+
281
+ it('allows edits when identity matches the document creator', () => {
282
+ const state = createState(
283
+ {'canvas-1': {local: canvasDoc('user-A')}},
284
+ createGrantsLookup(canvasUpdateAcl),
285
+ 'user-A',
286
+ )
287
+ expect(calculatePermissions({instance, state}, {actions: [editAction]})).toEqual({
288
+ allowed: true,
289
+ })
290
+ })
291
+
292
+ it('denies edits when identity does not match the document creator', () => {
293
+ const state = createState(
294
+ {'canvas-1': {local: canvasDoc('user-A')}},
295
+ createGrantsLookup(canvasUpdateAcl),
296
+ 'user-B',
297
+ )
298
+ const result = calculatePermissions({instance, state}, {actions: [editAction]})
299
+ expect(result?.allowed).toBe(false)
300
+ expect(result?.reasons).toEqual(
301
+ expect.arrayContaining([expect.objectContaining({type: 'access', documentId: 'canvas-1'})]),
302
+ )
303
+ })
304
+
305
+ it('denies edits when identity is not yet loaded', () => {
306
+ const state = createState(
307
+ {'canvas-1': {local: canvasDoc('user-A')}},
308
+ createGrantsLookup(canvasUpdateAcl),
309
+ undefined,
310
+ )
311
+ const result = calculatePermissions({instance, state}, {actions: [editAction]})
312
+ expect(result?.allowed).toBe(false)
313
+ expect(result?.reasons).toEqual(
314
+ expect.arrayContaining([expect.objectContaining({type: 'access', documentId: 'canvas-1'})]),
315
+ )
316
+ })
317
+ })
271
318
  })
@@ -1,12 +1,14 @@
1
+ import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
1
2
  import {type SanityDocument} from '@sanity/types'
2
- import {evaluateSync, type ExprNode, parse} from 'groq-js'
3
+ import {type ExprNode, parse} from 'groq-js'
3
4
  import {createSelector} from 'reselect'
4
5
 
6
+ import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
5
7
  import {type SelectorContext} from '../store/createStateSourceAction'
6
- import {getDraftId, getPublishedId} from '../utils/ids'
7
8
  import {MultiKeyWeakMap} from '../utils/MultiKeyWeakMap'
8
9
  import {type DocumentAction} from './actions'
9
- import {ActionError, PermissionActionError, processActions} from './processActions'
10
+ import {ActionError, PermissionActionError, processActions} from './processActions/processActions'
11
+ import {checkGrant} from './processActions/shared'
10
12
  import {type DocumentSet} from './processMutations'
11
13
  import {type SyncTransactionState} from './reducers'
12
14
 
@@ -28,6 +30,8 @@ export function createGrantsLookup(datasetAcl: DatasetAcl): Record<Grant, ExprNo
28
30
  for (const entry of datasetAcl) {
29
31
  for (const grant of entry.permissions) {
30
32
  const set = filtersByGrant[grant]
33
+ // Ignore permissions we don't model (e.g. "manage")
34
+ if (!set) continue
31
35
  set.add(entry.filter)
32
36
  filtersByGrant[grant] = set
33
37
  }
@@ -71,7 +75,14 @@ const documentsSelector = createSelector(
71
75
  // For liveEdit documents, only fetch the single document
72
76
  if (action.liveEdit) return [action.documentId]
73
77
  // For standard documents, fetch both draft and published
74
- return [getPublishedId(action.documentId), getDraftId(action.documentId)]
78
+ const ids: string[] = [
79
+ getPublishedId(DocumentId(action.documentId)),
80
+ getDraftId(DocumentId(action.documentId)),
81
+ ]
82
+ if (isReleasePerspective(action.perspective)) {
83
+ ids.push(getVersionId(DocumentId(action.documentId), action.perspective.releaseName))
84
+ }
85
+ return ids
75
86
  })
76
87
  .flat(),
77
88
  )
@@ -132,11 +143,6 @@ const memoizedActionsSelector = createSelector(
132
143
  },
133
144
  )
134
145
 
135
- function checkGrant(grantExpr: ExprNode, document: SanityDocument): boolean {
136
- const value = evaluateSync(grantExpr, {params: {document}})
137
- return value.type === 'boolean' && value.data
138
- }
139
-
140
146
  /** @beta */
141
147
  export interface PermissionDeniedReason {
142
148
  type: 'precondition' | 'access'
@@ -164,11 +170,13 @@ export function calculatePermissions(
164
170
  const _calculatePermissions = createSelector(
165
171
  [
166
172
  ({state: {grants}}: SelectorContext<SyncTransactionState>) => grants,
173
+ ({state: {identity}}: SelectorContext<SyncTransactionState>) => identity,
167
174
  documentsSelector,
168
175
  memoizedActionsSelector,
169
176
  ],
170
177
  (
171
178
  grants: Record<Grant, ExprNode> | undefined,
179
+ identity: string | undefined,
172
180
  documents: DocumentSet | undefined,
173
181
  actions: DocumentAction[] | undefined,
174
182
  ): DocumentPermissionsResult | undefined => {
@@ -187,6 +195,7 @@ const _calculatePermissions = createSelector(
187
195
  base: documents,
188
196
  timestamp,
189
197
  grants,
198
+ identity,
190
199
  })
191
200
  } catch (error) {
192
201
  if (error instanceof PermissionActionError) {
@@ -210,17 +219,33 @@ const _calculatePermissions = createSelector(
210
219
  // Check edit actions with no patches
211
220
  if (action.type === 'document.edit' && !action.patches?.length) {
212
221
  const docId = action.documentId
213
- // For liveEdit documents, only check the single document
214
- const doc = action.liveEdit
215
- ? documents[docId]
216
- : (documents[getDraftId(docId)] ?? documents[getPublishedId(docId)])
222
+ let doc: SanityDocument | null | undefined
223
+ if (action.liveEdit) {
224
+ doc = documents[docId]
225
+ }
226
+ // don't allow users to edit version documents that don't exist
227
+ // they should be explicitly created first, as in studio
228
+ else if (isReleasePerspective(action.perspective)) {
229
+ doc = documents[getVersionId(DocumentId(docId), action.perspective.releaseName)]
230
+ } else {
231
+ doc =
232
+ documents[getDraftId(DocumentId(docId))] ?? documents[getPublishedId(DocumentId(docId))]
233
+ }
217
234
  if (!doc) {
218
- reasons.push({
219
- type: 'precondition',
220
- message: `The document with ID "${docId}" could not be found. Please check that it exists before editing.`,
221
- documentId: docId,
222
- })
223
- } else if (!checkGrant(grants.update, doc)) {
235
+ if (isReleasePerspective(action.perspective)) {
236
+ reasons.push({
237
+ type: 'precondition',
238
+ message: `The version document with ID "${docId}" could not be found. Please create it or add it to the release first.`,
239
+ documentId: docId,
240
+ })
241
+ } else {
242
+ reasons.push({
243
+ type: 'precondition',
244
+ message: `The document with ID "${docId}" could not be found. Please check that it exists before editing.`,
245
+ documentId: docId,
246
+ })
247
+ }
248
+ } else if (!checkGrant(grants.update, doc, identity)) {
224
249
  reasons.push({
225
250
  type: 'access',
226
251
  message: `You are not allowed to edit the document with ID "${docId}".`,
@@ -0,0 +1,138 @@
1
+ import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
2
+ import {type Mutation, type SanityDocument} from '@sanity/types'
3
+
4
+ import {isReleasePerspective} from '../../releases/utils/isReleasePerspective'
5
+ import {type CreateDocumentAction} from '../actions'
6
+ import {getId, processMutations} from '../processMutations'
7
+ import {
8
+ ActionError,
9
+ type ActionHandlerContext,
10
+ type ActionHandlerResult,
11
+ checkGrant,
12
+ PermissionActionError,
13
+ } from './shared'
14
+
15
+ export function handleCreate(
16
+ action: CreateDocumentAction,
17
+ ctx: ActionHandlerContext,
18
+ ): ActionHandlerResult {
19
+ const {transactionId, timestamp, grants, identity, outgoingActions, outgoingMutations} = ctx
20
+ let {base, working} = ctx
21
+
22
+ const documentId = getId(action.documentId)
23
+
24
+ if (action.liveEdit) {
25
+ if (working[documentId]) {
26
+ throw new ActionError({
27
+ documentId,
28
+ transactionId,
29
+ message: `This document already exists.`,
30
+ })
31
+ }
32
+
33
+ const newDocBase = {_type: action.documentType, _id: documentId, ...action.initialValue}
34
+ const newDocWorking = {
35
+ _type: action.documentType,
36
+ _id: documentId,
37
+ ...action.initialValue,
38
+ }
39
+ const mutations: Mutation[] = [{create: newDocWorking}]
40
+
41
+ base = processMutations({
42
+ documents: base,
43
+ transactionId,
44
+ mutations: [{create: newDocBase}],
45
+ timestamp,
46
+ })
47
+ working = processMutations({
48
+ documents: working,
49
+ transactionId,
50
+ mutations,
51
+ timestamp,
52
+ })
53
+
54
+ if (!checkGrant(grants.create, working[documentId] as SanityDocument, identity)) {
55
+ throw new PermissionActionError({
56
+ documentId,
57
+ transactionId,
58
+ message: `You do not have permission to create document "${documentId}".`,
59
+ })
60
+ }
61
+
62
+ // liveEdit documents use the mutation endpoint directly -- we don't send actions
63
+ outgoingMutations.push(...mutations)
64
+ return {base, working}
65
+ }
66
+
67
+ // Standard draft/published/version logic
68
+ const versionId = isReleasePerspective(action.perspective)
69
+ ? getVersionId(DocumentId(documentId), action.perspective.releaseName)
70
+ : undefined
71
+ const draftId = getDraftId(DocumentId(documentId))
72
+ const publishedId = getPublishedId(DocumentId(documentId))
73
+
74
+ const alreadyHasVersion = versionId ? working[versionId] : working[draftId]
75
+
76
+ if (alreadyHasVersion) {
77
+ const errorDocType = versionId ? 'release version' : 'draft'
78
+ throw new ActionError({
79
+ documentId,
80
+ transactionId,
81
+ message: `A ${errorDocType} of this document already exists. Please use or discard the existing ${errorDocType} before creating a new one.`,
82
+ })
83
+ }
84
+
85
+ // Spread the (possibly undefined) draft or published version directly.
86
+ // (studio uses the draft version as a base if you are in a release perspective)
87
+ const newDocBase = {
88
+ ...(base[draftId] ?? base[publishedId]),
89
+ _type: action.documentType,
90
+ _id: versionId ?? draftId,
91
+ ...action.initialValue,
92
+ }
93
+ const newDocWorking = {
94
+ ...(working[draftId] ?? working[publishedId]),
95
+ _type: action.documentType,
96
+ _id: versionId ?? draftId,
97
+ ...action.initialValue,
98
+ }
99
+ const mutations: Mutation[] = [{create: newDocWorking}]
100
+
101
+ base = processMutations({
102
+ documents: base,
103
+ transactionId,
104
+ mutations: [{create: newDocBase}],
105
+ timestamp,
106
+ })
107
+ working = processMutations({
108
+ documents: working,
109
+ transactionId,
110
+ mutations,
111
+ timestamp,
112
+ })
113
+
114
+ if (versionId && !checkGrant(grants.create, working[versionId] as SanityDocument, identity)) {
115
+ throw new PermissionActionError({
116
+ documentId,
117
+ transactionId,
118
+ message: `You do not have permission to create a release version for document "${documentId}".`,
119
+ })
120
+ } else if (
121
+ !versionId &&
122
+ !checkGrant(grants.create, working[draftId] as SanityDocument, identity)
123
+ ) {
124
+ throw new PermissionActionError({
125
+ documentId,
126
+ transactionId,
127
+ message: `You do not have permission to create a draft for document "${documentId}".`,
128
+ })
129
+ }
130
+
131
+ outgoingMutations.push(...mutations)
132
+ outgoingActions.push({
133
+ actionType: 'sanity.action.document.version.create',
134
+ publishedId,
135
+ attributes: newDocWorking,
136
+ })
137
+ return {base, working}
138
+ }
@@ -0,0 +1,100 @@
1
+ import {DocumentId, getDraftId, getPublishedId} from '@sanity/id-utils'
2
+ import {type Mutation} from '@sanity/types'
3
+
4
+ import {isReleasePerspective} from '../../releases/utils/isReleasePerspective'
5
+ import {type DeleteDocumentAction} from '../actions'
6
+ import {processMutations} from '../processMutations'
7
+ import {
8
+ ActionError,
9
+ type ActionHandlerContext,
10
+ type ActionHandlerResult,
11
+ checkGrant,
12
+ PermissionActionError,
13
+ } from './shared'
14
+
15
+ export function handleDelete(
16
+ action: DeleteDocumentAction,
17
+ ctx: ActionHandlerContext,
18
+ ): ActionHandlerResult {
19
+ const {transactionId, timestamp, grants, identity, outgoingActions, outgoingMutations} = ctx
20
+ let {base, working} = ctx
21
+
22
+ const documentId = action.documentId
23
+
24
+ if (isReleasePerspective(action.perspective)) {
25
+ throw new ActionError({
26
+ documentId,
27
+ transactionId,
28
+ message: `Cannot delete a version document. You may want to use the "unpublish" or "discard" actions instead.`,
29
+ })
30
+ }
31
+
32
+ if (action.liveEdit) {
33
+ if (!working[documentId]) {
34
+ throw new ActionError({
35
+ documentId,
36
+ transactionId,
37
+ message: 'The document you are trying to delete does not exist.',
38
+ })
39
+ }
40
+
41
+ if (!checkGrant(grants.update, working[documentId], identity)) {
42
+ throw new PermissionActionError({
43
+ documentId,
44
+ transactionId,
45
+ message: `You do not have permission to delete this document.`,
46
+ })
47
+ }
48
+
49
+ const mutations: Mutation[] = [{delete: {id: documentId}}]
50
+
51
+ base = processMutations({documents: base, transactionId, mutations, timestamp})
52
+ working = processMutations({documents: working, transactionId, mutations, timestamp})
53
+
54
+ // although liveEdit documents can use the actions API for deletion,
55
+ // having this be an action while other operations are mutations creates an inconsistency
56
+ // (and a possible race condition in document store where mutations might get skipped)
57
+ outgoingMutations.push(...mutations)
58
+ return {base, working}
59
+ }
60
+
61
+ // Standard draft/published logic
62
+ const draftId = getDraftId(DocumentId(documentId))
63
+ const publishedId = getPublishedId(DocumentId(documentId))
64
+
65
+ if (!working[publishedId]) {
66
+ throw new ActionError({
67
+ documentId,
68
+ transactionId,
69
+ message: working[draftId]
70
+ ? 'Cannot delete a document without a published version.'
71
+ : 'The document you are trying to delete does not exist.',
72
+ })
73
+ }
74
+
75
+ const cantDeleteDraft = working[draftId] && !checkGrant(grants.update, working[draftId], identity)
76
+ const cantDeletePublished =
77
+ working[publishedId] && !checkGrant(grants.update, working[publishedId], identity)
78
+
79
+ if (cantDeleteDraft || cantDeletePublished) {
80
+ throw new PermissionActionError({
81
+ documentId,
82
+ transactionId,
83
+ message: `You do not have permission to delete this document.`,
84
+ })
85
+ }
86
+
87
+ const mutations: Mutation[] = [{delete: {id: publishedId}}, {delete: {id: draftId}}]
88
+ const includeDrafts = working[draftId] ? [draftId] : undefined
89
+
90
+ base = processMutations({documents: base, transactionId, mutations, timestamp})
91
+ working = processMutations({documents: working, transactionId, mutations, timestamp})
92
+
93
+ outgoingMutations.push(...mutations)
94
+ outgoingActions.push({
95
+ actionType: 'sanity.action.document.delete',
96
+ publishedId,
97
+ ...(includeDrafts ? {includeDrafts} : {}),
98
+ })
99
+ return {base, working}
100
+ }