@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts +1147 -0
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
- package/dist/_chunks-es/_internal.js +134 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +93 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_exports/_internal.d.ts +95 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +24 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +1079 -1076
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1967 -2302
- package/dist/index.js.map +1 -1
- package/package.json +48 -47
- package/src/_exports/_internal.ts +26 -0
- package/src/_exports/index.ts +120 -11
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- package/src/agent/agentActions.test.ts +16 -60
- package/src/agent/agentActions.ts +25 -38
- package/src/applications/applications.test-d.ts +83 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +364 -0
- package/src/applications/userApplications.test.ts +126 -0
- package/src/applications/userApplications.ts +261 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authMode.test.ts +25 -0
- package/src/auth/authMode.ts +6 -3
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +59 -26
- package/src/auth/getOrganizationVerificationState.test.ts +18 -17
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +35 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +75 -2
- package/src/auth/logout.ts +23 -4
- package/src/auth/refreshStampedToken.test.ts +219 -469
- package/src/auth/refreshStampedToken.ts +79 -195
- package/src/auth/studioAuth.ts +1 -1
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
- package/src/client/clientStore.test.ts +62 -37
- package/src/client/clientStore.ts +45 -75
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/comlink/controller/actions/destroyController.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
- package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +7 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +109 -79
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +10 -4
- package/src/document/actions.test.ts +115 -33
- package/src/document/actions.ts +212 -15
- package/src/document/applyDocumentActions.test.ts +38 -12
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +673 -278
- package/src/document/documentStore.ts +162 -71
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +82 -26
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +74 -21
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +49 -2
- package/src/document/patchOperations.ts +50 -0
- package/src/document/permissions.test.ts +105 -60
- package/src/document/permissions.ts +44 -19
- package/src/document/processActions/create.ts +138 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +154 -0
- package/src/document/processActions/processActions.ts +218 -0
- package/src/document/processActions/publish.ts +120 -0
- package/src/document/processActions/releaseArchive.ts +77 -0
- package/src/document/processActions/releaseCreate.ts +59 -0
- package/src/document/processActions/releaseDelete.ts +65 -0
- package/src/document/processActions/releaseEdit.ts +37 -0
- package/src/document/processActions/releasePublish.ts +45 -0
- package/src/document/processActions/releaseSchedule.ts +87 -0
- package/src/document/processActions/releaseUtil.ts +31 -0
- package/src/document/processActions/shared.ts +308 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +1129 -121
- package/src/document/reducers.test.ts +639 -64
- package/src/document/reducers.ts +187 -48
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/src/document/sharedListener.test.ts +16 -14
- package/src/document/sharedListener.ts +24 -8
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +68 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +202 -0
- package/src/organization/organization.test-d.ts +36 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +138 -0
- package/src/organizations/organizations.test-d.ts +56 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +107 -0
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +94 -23
- package/src/presence/presenceStore.ts +85 -29
- package/src/preview/getPreviewState.test.ts +137 -0
- package/src/preview/getPreviewState.ts +69 -0
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/resolvePreview.test.ts +64 -0
- package/src/preview/resolvePreview.ts +43 -0
- package/src/preview/types.ts +10 -0
- package/src/project/project.test-d.ts +37 -0
- package/src/project/project.test.ts +157 -12
- package/src/project/project.ts +138 -25
- package/src/projection/getProjectionState.test.ts +18 -20
- package/src/projection/getProjectionState.ts +23 -3
- package/src/projection/projectionQuery.ts +5 -5
- package/src/projection/projectionStore.test.ts +1 -1
- package/src/projection/resolveProjection.test.ts +1 -2
- package/src/projection/resolveProjection.ts +16 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
- package/src/projection/types.ts +8 -0
- package/src/projects/projects.test-d.ts +32 -0
- package/src/projects/projects.test.ts +108 -42
- package/src/projects/projects.ts +63 -27
- package/src/query/queryStore.test.ts +259 -123
- package/src/query/queryStore.ts +78 -63
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +2 -18
- package/src/releases/getPerspectiveState.test.ts +17 -23
- package/src/releases/getPerspectiveState.ts +8 -20
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +100 -53
- package/src/releases/releasesStore.ts +69 -44
- package/src/releases/utils/sortReleases.test.ts +38 -2
- package/src/releases/utils/sortReleases.ts +15 -10
- package/src/store/createActionBinder.test.ts +87 -90
- package/src/store/createActionBinder.ts +79 -25
- package/src/store/createSanityInstance.test.ts +83 -13
- package/src/store/createSanityInstance.ts +77 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +39 -44
- package/src/store/createStoreInstance.test.ts +4 -2
- package/src/store/fetcherStore.test-d.ts +121 -0
- package/src/store/fetcherStore.test.ts +299 -0
- package/src/store/fetcherStore.ts +801 -0
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/events.ts +42 -0
- package/src/telemetry/initTelemetry.test.ts +432 -0
- package/src/telemetry/initTelemetry.ts +218 -0
- package/src/telemetry/telemetryManager.test.ts +327 -0
- package/src/telemetry/telemetryManager.ts +199 -0
- package/src/users/reducers.test.ts +6 -1
- package/src/users/reducers.ts +5 -6
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +16 -12
- package/src/users/usersStore.ts +5 -1
- package/src/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/createFetcherStore.test.ts +7 -5
- package/src/utils/createFetcherStore.ts +8 -5
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/includeShape.ts +36 -0
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/logger.test.ts +12 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +37 -0
- package/src/document/processActions.ts +0 -707
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
|
@@ -274,10 +274,51 @@ describe('set', () => {
|
|
|
274
274
|
expect(output).toEqual({a: 1, nonexistent: {path: 999}})
|
|
275
275
|
})
|
|
276
276
|
|
|
277
|
-
|
|
277
|
+
// Content Lake never creates array items when setting: a keyed or indexed
|
|
278
|
+
// segment that does not resolve is a silent no-op server-side. The applier
|
|
279
|
+
// used to append a degenerate item (without the key, even), which
|
|
280
|
+
// fabricated array items locally that would never exist on the server.
|
|
281
|
+
it('does not create an item from a key constraint if the key is not present', () => {
|
|
278
282
|
const input = {items: [{_key: 'item1'}]}
|
|
279
283
|
const output = set(input, {'items[_key=="item2"]': {_key: 'item2'}})
|
|
280
|
-
expect(output).toEqual({items: [{_key: 'item1'}
|
|
284
|
+
expect(output).toEqual({items: [{_key: 'item1'}]})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('does not create an item when setting a property under an unresolvable key constraint', () => {
|
|
288
|
+
const input = {
|
|
289
|
+
blocks: [
|
|
290
|
+
{
|
|
291
|
+
_key: 'b1',
|
|
292
|
+
children: [{_key: 's1', _type: 'span', text: 'hello', marks: []}],
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
}
|
|
296
|
+
const output = set(input, {'blocks[_key=="b1"].children[_key=="missing"].marks': ['mark1']})
|
|
297
|
+
expect(output).toEqual(input)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('does not create an item when setting through an out-of-bounds index', () => {
|
|
301
|
+
const input = {items: [{_key: 'item1', value: 1}]}
|
|
302
|
+
const output = set(input, {'items[3].value': 99})
|
|
303
|
+
expect(output).toEqual(input)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('still sets a missing property on an existing keyed item', () => {
|
|
307
|
+
const input = {items: [{_key: 'item1'}]}
|
|
308
|
+
const output = set(input, {'items[_key=="item1"].value': 42})
|
|
309
|
+
expect(output).toEqual({items: [{_key: 'item1', value: 42}]})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('does not create anything when an array-addressing segment targets a non-array', () => {
|
|
313
|
+
const input = {items: {nested: true}}
|
|
314
|
+
const output = set(input, {'items[_key=="item1"].value': 42})
|
|
315
|
+
expect(output).toEqual(input)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('does not create an item when setting through a negative out-of-bounds index', () => {
|
|
319
|
+
const input = {items: [{_key: 'item1', value: 1}]}
|
|
320
|
+
const output = set(input, {'items[-5].value': 99})
|
|
321
|
+
expect(output).toEqual(input)
|
|
281
322
|
})
|
|
282
323
|
})
|
|
283
324
|
|
|
@@ -299,6 +340,12 @@ describe('setIfMissing', () => {
|
|
|
299
340
|
const output = setIfMissing(input, {'a.b': 42})
|
|
300
341
|
expect(output).toEqual({a: {b: 1}})
|
|
301
342
|
})
|
|
343
|
+
|
|
344
|
+
it('does not create an item under an unresolvable key constraint', () => {
|
|
345
|
+
const input = {items: [{_key: 'item1'}]}
|
|
346
|
+
const output = setIfMissing(input, {'items[_key=="missing"].value': 1})
|
|
347
|
+
expect(output).toEqual(input)
|
|
348
|
+
})
|
|
302
349
|
})
|
|
303
350
|
|
|
304
351
|
describe('unset', () => {
|
|
@@ -175,6 +175,54 @@ export const ensureArrayKeysDeep = memoize(<R>(input: R): R => {
|
|
|
175
175
|
return Object.fromEntries(entries) as R
|
|
176
176
|
})
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Whether every array-addressing segment (keyed `[_key=="…"]` or numeric
|
|
180
|
+
* index) in the given match path resolves to an existing array item.
|
|
181
|
+
*
|
|
182
|
+
* This mirrors Content Lake, which auto-creates missing *object* properties
|
|
183
|
+
* when setting but never creates *array items*:
|
|
184
|
+
*
|
|
185
|
+
* ```js
|
|
186
|
+
* // object properties are created, even nested ones:
|
|
187
|
+
* set({}, {'meta.author.name': 'Ada'})
|
|
188
|
+
* // -> {meta: {author: {name: 'Ada'}}} (applies)
|
|
189
|
+
*
|
|
190
|
+
* // array items are not:
|
|
191
|
+
* set({items: [{_key: 'a'}]}, {'items[_key=="b"].title': 'x'})
|
|
192
|
+
* // -> {items: [{_key: 'a'}]} (no-op)
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* Without this filter, the second example fabricated a degenerate item
|
|
196
|
+
* (`{title: 'x'}`, no `_key`) that would never exist on the server, which
|
|
197
|
+
* is how concurrent Portable Text edits grew phantom span-less children.
|
|
198
|
+
*/
|
|
199
|
+
function resolvesArraySegments(input: unknown, path: SingleValuePath): boolean {
|
|
200
|
+
let current: unknown = input
|
|
201
|
+
for (const segment of path) {
|
|
202
|
+
if (typeof segment === 'string') {
|
|
203
|
+
// missing object properties (nested ones included) are created
|
|
204
|
+
// server-side, so a string segment may descend into undefined
|
|
205
|
+
current =
|
|
206
|
+
typeof current === 'object' && current !== null && !Array.isArray(current)
|
|
207
|
+
? (current as Record<string, unknown>)[segment]
|
|
208
|
+
: undefined
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
// keyed and numeric segments address array items, which are never created
|
|
212
|
+
if (!Array.isArray(current)) return false
|
|
213
|
+
if (isKeySegment(segment)) {
|
|
214
|
+
const index = getIndexForKey(current, segment._key)
|
|
215
|
+
if (index === undefined) return false
|
|
216
|
+
current = current[index]
|
|
217
|
+
} else {
|
|
218
|
+
const index = segment < 0 ? current.length + segment : segment
|
|
219
|
+
if (!(index in current)) return false
|
|
220
|
+
current = current[index]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
|
|
178
226
|
/**
|
|
179
227
|
* Given an input object and a record of path expressions to values, this
|
|
180
228
|
* function will set each match with the given value.
|
|
@@ -198,6 +246,7 @@ export function set(input: unknown, pathExpressionValues: Record<string, unknown
|
|
|
198
246
|
replacementValue,
|
|
199
247
|
})),
|
|
200
248
|
)
|
|
249
|
+
.filter(({path}) => resolvesArraySegments(input, path))
|
|
201
250
|
.reduce((acc, {path, replacementValue}) => setDeep(acc, path, replacementValue), input)
|
|
202
251
|
|
|
203
252
|
return ensureArrayKeysDeep(result)
|
|
@@ -231,6 +280,7 @@ export function setIfMissing(
|
|
|
231
280
|
}))
|
|
232
281
|
})
|
|
233
282
|
.filter((matchEntry) => matchEntry.value === null || matchEntry.value === undefined)
|
|
283
|
+
.filter(({path}) => resolvesArraySegments(input, path))
|
|
234
284
|
.reduce((acc, {path, replacementValue}) => setDeep(acc, path, replacementValue), input)
|
|
235
285
|
|
|
236
286
|
return ensureArrayKeysDeep(result)
|
|
@@ -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')]: {
|
|
70
|
-
|
|
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(
|
|
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,75 @@ 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
|
+
_system: {createdBy},
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const editAction: DocumentAction = {
|
|
273
|
+
documentId: 'canvas-1',
|
|
274
|
+
documentType: 'sanity.canvas.document',
|
|
275
|
+
type: 'document.edit',
|
|
276
|
+
liveEdit: true,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
it('allows edits when identity matches the document creator', () => {
|
|
280
|
+
const state = createState(
|
|
281
|
+
{'canvas-1': {local: canvasDoc('user-A')}},
|
|
282
|
+
createGrantsLookup(canvasUpdateAcl),
|
|
283
|
+
'user-A',
|
|
284
|
+
)
|
|
285
|
+
expect(calculatePermissions({instance, state}, {actions: [editAction]})).toEqual({
|
|
286
|
+
allowed: true,
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('denies edits when identity does not match the document creator', () => {
|
|
291
|
+
const state = createState(
|
|
292
|
+
{'canvas-1': {local: canvasDoc('user-A')}},
|
|
293
|
+
createGrantsLookup(canvasUpdateAcl),
|
|
294
|
+
'user-B',
|
|
295
|
+
)
|
|
296
|
+
const result = calculatePermissions({instance, state}, {actions: [editAction]})
|
|
297
|
+
expect(result?.allowed).toBe(false)
|
|
298
|
+
expect(result?.reasons).toEqual(
|
|
299
|
+
expect.arrayContaining([expect.objectContaining({type: 'access', documentId: 'canvas-1'})]),
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('denies edits when identity is not yet loaded', () => {
|
|
304
|
+
const state = createState(
|
|
305
|
+
{'canvas-1': {local: canvasDoc('user-A')}},
|
|
306
|
+
createGrantsLookup(canvasUpdateAcl),
|
|
307
|
+
undefined,
|
|
308
|
+
)
|
|
309
|
+
const result = calculatePermissions({instance, state}, {actions: [editAction]})
|
|
310
|
+
expect(result?.allowed).toBe(false)
|
|
311
|
+
expect(result?.reasons).toEqual(
|
|
312
|
+
expect.arrayContaining([expect.objectContaining({type: 'access', documentId: 'canvas-1'})]),
|
|
313
|
+
)
|
|
314
|
+
})
|
|
315
|
+
})
|
|
271
316
|
})
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
|
|
1
2
|
import {type SanityDocument} from '@sanity/types'
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
}
|