@sanity/sdk 2.11.1 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks-dts/utils.d.ts +175 -19
- package/dist/_chunks-es/_internal.js +41 -26
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +15 -4
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/telemetryManager.js +25 -19
- package/dist/_chunks-es/telemetryManager.js.map +1 -1
- package/dist/_chunks-es/version.js +1 -1
- package/dist/_exports/_internal.d.ts +27 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +465 -131
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/_exports/index.ts +23 -2
- package/src/config/sanityConfig.ts +12 -0
- package/src/document/actions.test.ts +112 -1
- package/src/document/actions.ts +148 -1
- package/src/document/applyDocumentActions.test.ts +24 -0
- package/src/document/applyDocumentActions.ts +17 -5
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.test.ts +69 -0
- package/src/document/documentStore.ts +42 -10
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +43 -24
- package/src/document/listen.ts +1 -1
- package/src/document/permissions.test.ts +79 -0
- package/src/document/permissions.ts +8 -7
- package/src/document/processActions/create.ts +7 -4
- package/src/document/processActions/delete.ts +4 -4
- package/src/document/processActions/discard.ts +2 -2
- package/src/document/processActions/edit.ts +13 -47
- package/src/document/processActions/processActions.ts +53 -3
- package/src/document/processActions/publish.ts +4 -4
- 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 +108 -4
- package/src/document/processActions/unpublish.ts +3 -3
- package/src/document/processActions.test.ts +423 -1
- package/src/document/reducers.ts +44 -8
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/src/releases/getPerspectiveState.test.ts +1 -1
- package/src/releases/releasesStore.test.ts +50 -1
- package/src/releases/releasesStore.ts +41 -18
- package/src/releases/utils/sortReleases.test.ts +2 -2
- package/src/releases/utils/sortReleases.ts +1 -1
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/{__telemetry__/sdk.telemetry.ts → events.ts} +9 -9
- package/src/telemetry/initTelemetry.test.ts +240 -16
- package/src/telemetry/initTelemetry.ts +39 -16
- package/src/telemetry/telemetryManager.test.ts +129 -65
- package/src/telemetry/telemetryManager.ts +41 -29
- package/src/telemetry/devMode.test.ts +0 -60
- package/src/telemetry/devMode.ts +0 -41
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {type Mutation} from '@sanity/types'
|
|
2
2
|
import {type ExprNode} from 'groq-js'
|
|
3
3
|
|
|
4
|
-
import {type DocumentAction} from '../actions'
|
|
4
|
+
import {type Action, type DocumentAction} from '../actions'
|
|
5
5
|
import {type Grant} from '../permissions'
|
|
6
6
|
import {type DocumentSet} from '../processMutations'
|
|
7
7
|
import {type HttpAction} from '../reducers'
|
|
@@ -10,6 +10,13 @@ import {handleDelete} from './delete'
|
|
|
10
10
|
import {handleDiscard} from './discard'
|
|
11
11
|
import {handleEdit} from './edit'
|
|
12
12
|
import {handlePublish} from './publish'
|
|
13
|
+
import {handleReleaseArchive, handleReleaseUnarchive} from './releaseArchive'
|
|
14
|
+
import {handleReleaseCreate} from './releaseCreate'
|
|
15
|
+
import {handleReleaseDelete} from './releaseDelete'
|
|
16
|
+
import {handleReleaseEdit} from './releaseEdit'
|
|
17
|
+
import {handleReleasePublish} from './releasePublish'
|
|
18
|
+
import {handleReleaseSchedule, handleReleaseUnschedule} from './releaseSchedule'
|
|
19
|
+
import {isReleaseAction} from './releaseUtil'
|
|
13
20
|
import {
|
|
14
21
|
ActionError,
|
|
15
22
|
type ActionHandlerContext,
|
|
@@ -30,7 +37,7 @@ interface ProcessActionsOptions {
|
|
|
30
37
|
/**
|
|
31
38
|
* The actions to apply to the given documents
|
|
32
39
|
*/
|
|
33
|
-
actions:
|
|
40
|
+
actions: Action[]
|
|
34
41
|
|
|
35
42
|
/**
|
|
36
43
|
* The set of documents these actions were intended to be applied to. These
|
|
@@ -58,6 +65,13 @@ interface ProcessActionsOptions {
|
|
|
58
65
|
*/
|
|
59
66
|
grants: Record<Grant, ExprNode>
|
|
60
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The current user's ID, passed to GROQ as the value of `identity()` when
|
|
70
|
+
* evaluating ACL filters. Optional because the user may not have loaded yet;
|
|
71
|
+
* filters that reference `identity()` will evaluate to null in that case.
|
|
72
|
+
*/
|
|
73
|
+
identity?: string
|
|
74
|
+
|
|
61
75
|
// // TODO: implement initial values from the schema?
|
|
62
76
|
// initialValues?: {[TDocumentType in string]?: {_type: string}}
|
|
63
77
|
}
|
|
@@ -109,6 +123,7 @@ export function processActions({
|
|
|
109
123
|
base: initialBase,
|
|
110
124
|
timestamp,
|
|
111
125
|
grants,
|
|
126
|
+
identity,
|
|
112
127
|
}: ProcessActionsOptions): ProcessActionsResult {
|
|
113
128
|
let base: DocumentSet = {...initialBase}
|
|
114
129
|
let working: DocumentSet = {...initialWorking}
|
|
@@ -116,6 +131,24 @@ export function processActions({
|
|
|
116
131
|
const outgoingActions: HttpAction[] = []
|
|
117
132
|
const outgoingMutations: Mutation[] = []
|
|
118
133
|
|
|
134
|
+
// liveEdit document actions go to the mutations API, since the actions API
|
|
135
|
+
// requires a draft+published pair. Mixing them with anything else in the same
|
|
136
|
+
// transaction would silently lose atomicity for the non-liveEdit operations,
|
|
137
|
+
// so require users to split the transaction.
|
|
138
|
+
// (Note that the reducers already does this for us -- you'd have to try hard to mix them.)
|
|
139
|
+
const liveEditAction = actions.find((action) => !isReleaseAction(action) && action.liveEdit) as
|
|
140
|
+
| DocumentAction
|
|
141
|
+
| undefined
|
|
142
|
+
const otherAction = actions.find((action) => isReleaseAction(action) || !action.liveEdit)
|
|
143
|
+
if (liveEditAction && otherAction) {
|
|
144
|
+
throw new ActionError({
|
|
145
|
+
documentId: liveEditAction.documentId!,
|
|
146
|
+
transactionId,
|
|
147
|
+
message:
|
|
148
|
+
'Cannot combine liveEdit document actions with other actions in the same transaction. Submit them as separate transactions.',
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
119
152
|
for (const action of actions) {
|
|
120
153
|
const result = dispatch(action, {
|
|
121
154
|
base,
|
|
@@ -123,6 +156,7 @@ export function processActions({
|
|
|
123
156
|
transactionId,
|
|
124
157
|
timestamp,
|
|
125
158
|
grants,
|
|
159
|
+
identity,
|
|
126
160
|
outgoingActions,
|
|
127
161
|
outgoingMutations,
|
|
128
162
|
})
|
|
@@ -143,7 +177,7 @@ export function processActions({
|
|
|
143
177
|
}
|
|
144
178
|
}
|
|
145
179
|
|
|
146
|
-
function dispatch(action:
|
|
180
|
+
function dispatch(action: Action, ctx: ActionHandlerContext): ActionHandlerResult {
|
|
147
181
|
switch (action.type) {
|
|
148
182
|
case 'document.create':
|
|
149
183
|
return handleCreate(action, ctx)
|
|
@@ -157,6 +191,22 @@ function dispatch(action: DocumentAction, ctx: ActionHandlerContext): ActionHand
|
|
|
157
191
|
return handlePublish(action, ctx)
|
|
158
192
|
case 'document.unpublish':
|
|
159
193
|
return handleUnpublish(action, ctx)
|
|
194
|
+
case 'release.create':
|
|
195
|
+
return handleReleaseCreate(action, ctx)
|
|
196
|
+
case 'release.edit':
|
|
197
|
+
return handleReleaseEdit(action, ctx)
|
|
198
|
+
case 'release.publish':
|
|
199
|
+
return handleReleasePublish(action, ctx)
|
|
200
|
+
case 'release.schedule':
|
|
201
|
+
return handleReleaseSchedule(action, ctx)
|
|
202
|
+
case 'release.unschedule':
|
|
203
|
+
return handleReleaseUnschedule(action, ctx)
|
|
204
|
+
case 'release.archive':
|
|
205
|
+
return handleReleaseArchive(action, ctx)
|
|
206
|
+
case 'release.unarchive':
|
|
207
|
+
return handleReleaseUnarchive(action, ctx)
|
|
208
|
+
case 'release.delete':
|
|
209
|
+
return handleReleaseDelete(action, ctx)
|
|
160
210
|
default:
|
|
161
211
|
throw new Error(
|
|
162
212
|
`Unknown action type: "${
|
|
@@ -17,7 +17,7 @@ export function handlePublish(
|
|
|
17
17
|
action: PublishDocumentAction,
|
|
18
18
|
ctx: ActionHandlerContext,
|
|
19
19
|
): ActionHandlerResult {
|
|
20
|
-
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations} = ctx
|
|
20
|
+
const {transactionId, timestamp, grants, identity, outgoingActions, outgoingMutations} = ctx
|
|
21
21
|
let {base, working} = ctx
|
|
22
22
|
|
|
23
23
|
const documentId = getId(action.documentId)
|
|
@@ -58,7 +58,7 @@ export function handlePublish(
|
|
|
58
58
|
|
|
59
59
|
const mutations: Mutation[] = [{delete: {id: draftId}}, {createOrReplace: newPublishedFromDraft}]
|
|
60
60
|
|
|
61
|
-
if (working[draftId] && !checkGrant(grants.update, working[draftId])) {
|
|
61
|
+
if (working[draftId] && !checkGrant(grants.update, working[draftId], identity)) {
|
|
62
62
|
throw new PermissionActionError({
|
|
63
63
|
documentId,
|
|
64
64
|
transactionId,
|
|
@@ -66,13 +66,13 @@ export function handlePublish(
|
|
|
66
66
|
})
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
if (working[publishedId] && !checkGrant(grants.update, newPublishedFromDraft)) {
|
|
69
|
+
if (working[publishedId] && !checkGrant(grants.update, newPublishedFromDraft, identity)) {
|
|
70
70
|
throw new PermissionActionError({
|
|
71
71
|
documentId,
|
|
72
72
|
transactionId,
|
|
73
73
|
message: `Publish failed: You do not have permission to update the published version of "${documentId}".`,
|
|
74
74
|
})
|
|
75
|
-
} else if (!working[publishedId] && !checkGrant(grants.create, newPublishedFromDraft)) {
|
|
75
|
+
} else if (!working[publishedId] && !checkGrant(grants.create, newPublishedFromDraft, identity)) {
|
|
76
76
|
throw new PermissionActionError({
|
|
77
77
|
documentId,
|
|
78
78
|
transactionId,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {type ArchiveReleaseAction, type UnarchiveReleaseAction} from '../actions'
|
|
2
|
+
import {getReleaseDocumentId} from './releaseUtil'
|
|
3
|
+
import {
|
|
4
|
+
ActionError,
|
|
5
|
+
type ActionHandlerContext,
|
|
6
|
+
type ActionHandlerResult,
|
|
7
|
+
checkGrant,
|
|
8
|
+
PermissionActionError,
|
|
9
|
+
} from './shared'
|
|
10
|
+
|
|
11
|
+
export function handleReleaseArchive(
|
|
12
|
+
action: ArchiveReleaseAction,
|
|
13
|
+
ctx: ActionHandlerContext,
|
|
14
|
+
): ActionHandlerResult {
|
|
15
|
+
const {base, working, grants, outgoingActions, transactionId, identity} = ctx
|
|
16
|
+
|
|
17
|
+
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
18
|
+
const existing = working[releaseDocumentId] ?? base[releaseDocumentId]
|
|
19
|
+
if (!existing) {
|
|
20
|
+
throw new ActionError({
|
|
21
|
+
documentId: releaseDocumentId,
|
|
22
|
+
transactionId,
|
|
23
|
+
message: `Cannot archive release "${action.releaseId}" because it does not exist.`,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
28
|
+
throw new PermissionActionError({
|
|
29
|
+
documentId: releaseDocumentId,
|
|
30
|
+
transactionId,
|
|
31
|
+
message: `You do not have permission to archive release "${action.releaseId}".`,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Archiving deletes every version document in the release server-side.
|
|
36
|
+
// Although technically we could search through the document store for all related
|
|
37
|
+
// version documents, for now it's simpler to just let the server handle it.
|
|
38
|
+
// (Those local documents will be eventually updated by the listener.)
|
|
39
|
+
outgoingActions.push({
|
|
40
|
+
actionType: 'sanity.action.release.archive',
|
|
41
|
+
releaseId: action.releaseId,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return {base, working}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function handleReleaseUnarchive(
|
|
48
|
+
action: UnarchiveReleaseAction,
|
|
49
|
+
ctx: ActionHandlerContext,
|
|
50
|
+
): ActionHandlerResult {
|
|
51
|
+
const {base, working, grants, outgoingActions, transactionId, identity} = ctx
|
|
52
|
+
|
|
53
|
+
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
54
|
+
const existing = working[releaseDocumentId] ?? base[releaseDocumentId]
|
|
55
|
+
if (!existing) {
|
|
56
|
+
throw new ActionError({
|
|
57
|
+
documentId: releaseDocumentId,
|
|
58
|
+
transactionId,
|
|
59
|
+
message: `Cannot unarchive release "${action.releaseId}" because it does not exist.`,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
64
|
+
throw new PermissionActionError({
|
|
65
|
+
documentId: releaseDocumentId,
|
|
66
|
+
transactionId,
|
|
67
|
+
message: `You do not have permission to unarchive release "${action.releaseId}".`,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
outgoingActions.push({
|
|
72
|
+
actionType: 'sanity.action.release.unarchive',
|
|
73
|
+
releaseId: action.releaseId,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return {base, working}
|
|
77
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {type Mutation, type SanityDocument} from '@sanity/types'
|
|
2
|
+
|
|
3
|
+
import {type CreateReleaseAction} from '../actions'
|
|
4
|
+
import {processMutations} from '../processMutations'
|
|
5
|
+
import {getReleaseDocumentId} from './releaseUtil'
|
|
6
|
+
import {
|
|
7
|
+
ActionError,
|
|
8
|
+
type ActionHandlerContext,
|
|
9
|
+
type ActionHandlerResult,
|
|
10
|
+
checkGrant,
|
|
11
|
+
PermissionActionError,
|
|
12
|
+
} from './shared'
|
|
13
|
+
|
|
14
|
+
export function handleReleaseCreate(
|
|
15
|
+
action: CreateReleaseAction,
|
|
16
|
+
ctx: ActionHandlerContext,
|
|
17
|
+
): ActionHandlerResult {
|
|
18
|
+
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations, identity} = ctx
|
|
19
|
+
let {base, working} = ctx
|
|
20
|
+
|
|
21
|
+
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
22
|
+
|
|
23
|
+
if (working[releaseDocumentId] || base[releaseDocumentId]) {
|
|
24
|
+
throw new ActionError({
|
|
25
|
+
documentId: releaseDocumentId,
|
|
26
|
+
transactionId,
|
|
27
|
+
message: `A release with id "${action.releaseId}" already exists.`,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
// Optimistic local release doc
|
|
31
|
+
const releaseDoc = {
|
|
32
|
+
_id: releaseDocumentId,
|
|
33
|
+
_type: 'system.release',
|
|
34
|
+
name: action.releaseId,
|
|
35
|
+
state: 'active',
|
|
36
|
+
metadata: action.metadata,
|
|
37
|
+
}
|
|
38
|
+
const mutations: Mutation[] = [{create: releaseDoc}]
|
|
39
|
+
|
|
40
|
+
base = processMutations({documents: base, transactionId, mutations, timestamp})
|
|
41
|
+
working = processMutations({documents: working, transactionId, mutations, timestamp})
|
|
42
|
+
|
|
43
|
+
if (!checkGrant(grants.create, working[releaseDocumentId] as SanityDocument, identity)) {
|
|
44
|
+
throw new PermissionActionError({
|
|
45
|
+
documentId: releaseDocumentId,
|
|
46
|
+
transactionId,
|
|
47
|
+
message: `You do not have permission to create release "${action.releaseId}".`,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
outgoingMutations.push(...mutations)
|
|
52
|
+
outgoingActions.push({
|
|
53
|
+
actionType: 'sanity.action.release.create',
|
|
54
|
+
releaseId: action.releaseId,
|
|
55
|
+
metadata: action.metadata,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return {base, working}
|
|
59
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {type Mutation} from '@sanity/types'
|
|
2
|
+
|
|
3
|
+
import {type DeleteReleaseAction} from '../actions'
|
|
4
|
+
import {processMutations} from '../processMutations'
|
|
5
|
+
import {getReleaseDocumentId} from './releaseUtil'
|
|
6
|
+
import {
|
|
7
|
+
ActionError,
|
|
8
|
+
type ActionHandlerContext,
|
|
9
|
+
type ActionHandlerResult,
|
|
10
|
+
checkGrant,
|
|
11
|
+
PermissionActionError,
|
|
12
|
+
} from './shared'
|
|
13
|
+
|
|
14
|
+
// you can only delete archived or published releases
|
|
15
|
+
// https://www.sanity.io/docs/content-lake/dispatch-actions#k22ab37420f3c
|
|
16
|
+
const DELETABLE_STATES = new Set(['archived', 'published'])
|
|
17
|
+
|
|
18
|
+
export function handleReleaseDelete(
|
|
19
|
+
action: DeleteReleaseAction,
|
|
20
|
+
ctx: ActionHandlerContext,
|
|
21
|
+
): ActionHandlerResult {
|
|
22
|
+
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations, identity} = ctx
|
|
23
|
+
let {base, working} = ctx
|
|
24
|
+
|
|
25
|
+
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
26
|
+
const existing = working[releaseDocumentId] ?? base[releaseDocumentId]
|
|
27
|
+
|
|
28
|
+
if (!existing) {
|
|
29
|
+
throw new ActionError({
|
|
30
|
+
documentId: releaseDocumentId,
|
|
31
|
+
transactionId,
|
|
32
|
+
message: `Cannot delete release "${action.releaseId}" because it does not exist.`,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const state = existing['state']
|
|
37
|
+
if (state && typeof state === 'string' && !DELETABLE_STATES.has(state)) {
|
|
38
|
+
throw new ActionError({
|
|
39
|
+
documentId: releaseDocumentId,
|
|
40
|
+
transactionId,
|
|
41
|
+
message: `Cannot delete release "${action.releaseId}" while it is "${state}". Archive it first.`,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
46
|
+
throw new PermissionActionError({
|
|
47
|
+
documentId: releaseDocumentId,
|
|
48
|
+
transactionId,
|
|
49
|
+
message: `You do not have permission to delete release "${action.releaseId}".`,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const mutations: Mutation[] = [{delete: {id: releaseDocumentId}}]
|
|
54
|
+
|
|
55
|
+
base = processMutations({documents: base, transactionId, mutations, timestamp})
|
|
56
|
+
working = processMutations({documents: working, transactionId, mutations, timestamp})
|
|
57
|
+
|
|
58
|
+
outgoingMutations.push(...mutations)
|
|
59
|
+
outgoingActions.push({
|
|
60
|
+
actionType: 'sanity.action.release.delete',
|
|
61
|
+
releaseId: action.releaseId,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return {base, working}
|
|
65
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {type EditReleaseAction} from '../actions'
|
|
2
|
+
import {getReleaseDocumentId} from './releaseUtil'
|
|
3
|
+
import {type ActionHandlerContext, type ActionHandlerResult, applySingleDocPatch} from './shared'
|
|
4
|
+
|
|
5
|
+
export function handleReleaseEdit(
|
|
6
|
+
action: EditReleaseAction,
|
|
7
|
+
ctx: ActionHandlerContext,
|
|
8
|
+
): ActionHandlerResult {
|
|
9
|
+
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations, identity} = ctx
|
|
10
|
+
const {base, working} = ctx
|
|
11
|
+
|
|
12
|
+
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
13
|
+
|
|
14
|
+
const result = applySingleDocPatch({
|
|
15
|
+
base,
|
|
16
|
+
working,
|
|
17
|
+
documentId: releaseDocumentId,
|
|
18
|
+
patches: [action.patch],
|
|
19
|
+
transactionId,
|
|
20
|
+
timestamp,
|
|
21
|
+
grants,
|
|
22
|
+
identity,
|
|
23
|
+
notFoundMessage: `Cannot edit release "${action.releaseId}" because it does not exist.`,
|
|
24
|
+
permissionMessage: `You do not have permission to edit release "${action.releaseId}".`,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
outgoingMutations.push(...result.workingMutations)
|
|
28
|
+
outgoingActions.push(
|
|
29
|
+
...result.diffedPatches.map((patch) => ({
|
|
30
|
+
actionType: 'sanity.action.release.edit' as const,
|
|
31
|
+
releaseId: action.releaseId,
|
|
32
|
+
patch,
|
|
33
|
+
})),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return {base: result.base, working: result.working}
|
|
37
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {type PublishReleaseAction} from '../actions'
|
|
2
|
+
import {getReleaseDocumentId} from './releaseUtil'
|
|
3
|
+
import {
|
|
4
|
+
ActionError,
|
|
5
|
+
type ActionHandlerContext,
|
|
6
|
+
type ActionHandlerResult,
|
|
7
|
+
checkGrant,
|
|
8
|
+
PermissionActionError,
|
|
9
|
+
} from './shared'
|
|
10
|
+
|
|
11
|
+
export function handleReleasePublish(
|
|
12
|
+
action: PublishReleaseAction,
|
|
13
|
+
ctx: ActionHandlerContext,
|
|
14
|
+
): ActionHandlerResult {
|
|
15
|
+
const {base, working, grants, outgoingActions, transactionId, identity} = ctx
|
|
16
|
+
|
|
17
|
+
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
18
|
+
const existing = working[releaseDocumentId] ?? base[releaseDocumentId]
|
|
19
|
+
if (!existing) {
|
|
20
|
+
throw new ActionError({
|
|
21
|
+
documentId: releaseDocumentId,
|
|
22
|
+
transactionId,
|
|
23
|
+
message: `Cannot publish release "${action.releaseId}" because it does not exist.`,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
28
|
+
throw new PermissionActionError({
|
|
29
|
+
documentId: releaseDocumentId,
|
|
30
|
+
transactionId,
|
|
31
|
+
message: `You do not have permission to publish release "${action.releaseId}".`,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// a release publish cascades to every version document in the release
|
|
36
|
+
// although technically we could search through the document store for all related
|
|
37
|
+
// version documents, for now it's simpler to just let the server handle it.
|
|
38
|
+
// (Those local documents will be eventually updated by the listener.)
|
|
39
|
+
outgoingActions.push({
|
|
40
|
+
actionType: 'sanity.action.release.publish',
|
|
41
|
+
releaseId: action.releaseId,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return {base, working}
|
|
45
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {type ScheduleReleaseAction, type UnscheduleReleaseAction} from '../actions'
|
|
2
|
+
import {getReleaseDocumentId} from './releaseUtil'
|
|
3
|
+
import {
|
|
4
|
+
ActionError,
|
|
5
|
+
type ActionHandlerContext,
|
|
6
|
+
type ActionHandlerResult,
|
|
7
|
+
checkGrant,
|
|
8
|
+
PermissionActionError,
|
|
9
|
+
} from './shared'
|
|
10
|
+
|
|
11
|
+
export function handleReleaseSchedule(
|
|
12
|
+
action: ScheduleReleaseAction,
|
|
13
|
+
ctx: ActionHandlerContext,
|
|
14
|
+
): ActionHandlerResult {
|
|
15
|
+
const {base, working, grants, outgoingActions, transactionId, identity} = ctx
|
|
16
|
+
|
|
17
|
+
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
18
|
+
|
|
19
|
+
if (Number.isNaN(Date.parse(action.publishAt))) {
|
|
20
|
+
throw new ActionError({
|
|
21
|
+
documentId: releaseDocumentId,
|
|
22
|
+
transactionId,
|
|
23
|
+
message: `Cannot schedule release "${action.releaseId}": "publishAt" must be a valid ISO 8601 timestamp (received "${action.publishAt}").`,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const existing = working[releaseDocumentId] ?? base[releaseDocumentId]
|
|
28
|
+
if (!existing) {
|
|
29
|
+
throw new ActionError({
|
|
30
|
+
documentId: releaseDocumentId,
|
|
31
|
+
transactionId,
|
|
32
|
+
message: `Cannot schedule release "${action.releaseId}" because it does not exist.`,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
37
|
+
throw new PermissionActionError({
|
|
38
|
+
documentId: releaseDocumentId,
|
|
39
|
+
transactionId,
|
|
40
|
+
message: `You do not have permission to schedule release "${action.releaseId}".`,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Scheduling flips `state` to 'scheduled' and locks version documents
|
|
45
|
+
// server-side. We don't model the lock locally yet — local edits to those
|
|
46
|
+
// version docs will be rejected at submit time. The listener will sync the
|
|
47
|
+
// updated release state.
|
|
48
|
+
outgoingActions.push({
|
|
49
|
+
actionType: 'sanity.action.release.schedule',
|
|
50
|
+
releaseId: action.releaseId,
|
|
51
|
+
publishAt: action.publishAt,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return {base, working}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function handleReleaseUnschedule(
|
|
58
|
+
action: UnscheduleReleaseAction,
|
|
59
|
+
ctx: ActionHandlerContext,
|
|
60
|
+
): ActionHandlerResult {
|
|
61
|
+
const {base, working, grants, outgoingActions, transactionId, identity} = ctx
|
|
62
|
+
|
|
63
|
+
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
64
|
+
const existing = working[releaseDocumentId] ?? base[releaseDocumentId]
|
|
65
|
+
if (!existing) {
|
|
66
|
+
throw new ActionError({
|
|
67
|
+
documentId: releaseDocumentId,
|
|
68
|
+
transactionId,
|
|
69
|
+
message: `Cannot unschedule release "${action.releaseId}" because it does not exist.`,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
74
|
+
throw new PermissionActionError({
|
|
75
|
+
documentId: releaseDocumentId,
|
|
76
|
+
transactionId,
|
|
77
|
+
message: `You do not have permission to unschedule release "${action.releaseId}".`,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
outgoingActions.push({
|
|
82
|
+
actionType: 'sanity.action.release.unschedule',
|
|
83
|
+
releaseId: action.releaseId,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return {base, working}
|
|
87
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {type Action, type ReleaseAction} from '../actions'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Path prefix shared by every release document. Matches studio's
|
|
5
|
+
* `RELEASE_DOCUMENTS_PATH` constant.
|
|
6
|
+
*/
|
|
7
|
+
const RELEASE_DOCUMENTS_PATH = '_.releases'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns the full release document ID for the given release name.
|
|
11
|
+
* e.g. `getReleaseDocumentId('my-release') === '_.releases.my-release'`
|
|
12
|
+
* @beta
|
|
13
|
+
*/
|
|
14
|
+
export function getReleaseDocumentId(releaseId: string): string {
|
|
15
|
+
return `${RELEASE_DOCUMENTS_PATH}.${releaseId}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const RELEASE_ACTION_TYPES = new Set([
|
|
19
|
+
'release.create',
|
|
20
|
+
'release.edit',
|
|
21
|
+
'release.publish',
|
|
22
|
+
'release.schedule',
|
|
23
|
+
'release.unschedule',
|
|
24
|
+
'release.archive',
|
|
25
|
+
'release.unarchive',
|
|
26
|
+
'release.delete',
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
export function isReleaseAction(action: Action): action is ReleaseAction {
|
|
30
|
+
return RELEASE_ACTION_TYPES.has(action.type)
|
|
31
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {diffValue} from '@sanity/diff-patch'
|
|
2
|
+
import {type Mutation, type PatchOperations, type SanityDocument} from '@sanity/types'
|
|
2
3
|
import {evaluateSync, type ExprNode} from 'groq-js'
|
|
3
4
|
|
|
4
5
|
import {type Grant} from '../permissions'
|
|
5
|
-
import {type DocumentSet} from '../processMutations'
|
|
6
|
+
import {type DocumentSet, processMutations} from '../processMutations'
|
|
6
7
|
import {type HttpAction} from '../reducers'
|
|
7
8
|
|
|
8
9
|
export interface ActionHandlerContext {
|
|
@@ -11,6 +12,12 @@ export interface ActionHandlerContext {
|
|
|
11
12
|
transactionId: string
|
|
12
13
|
timestamp: string
|
|
13
14
|
grants: Record<Grant, ExprNode>
|
|
15
|
+
/**
|
|
16
|
+
* The current user's ID, used by GROQ's `identity()` when evaluating ACL
|
|
17
|
+
* filters. May be `undefined` before the user has loaded; in that case
|
|
18
|
+
* `identity()` evaluates to null.
|
|
19
|
+
*/
|
|
20
|
+
identity: string | undefined
|
|
14
21
|
outgoingActions: HttpAction[]
|
|
15
22
|
outgoingMutations: Mutation[]
|
|
16
23
|
}
|
|
@@ -20,8 +27,12 @@ export interface ActionHandlerResult {
|
|
|
20
27
|
working: DocumentSet
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
export function checkGrant(
|
|
24
|
-
|
|
30
|
+
export function checkGrant(
|
|
31
|
+
grantExpr: ExprNode,
|
|
32
|
+
document: SanityDocument,
|
|
33
|
+
identity: string | undefined,
|
|
34
|
+
): boolean {
|
|
35
|
+
const value = evaluateSync(grantExpr, {params: {document}, identity})
|
|
25
36
|
return value.type === 'boolean' && value.data
|
|
26
37
|
}
|
|
27
38
|
|
|
@@ -45,3 +56,96 @@ export class ActionError extends Error implements ActionErrorOptions {
|
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
export class PermissionActionError extends ActionError {}
|
|
59
|
+
|
|
60
|
+
interface ApplySingleDocPatchOptions {
|
|
61
|
+
base: DocumentSet
|
|
62
|
+
working: DocumentSet
|
|
63
|
+
documentId: string
|
|
64
|
+
patches: PatchOperations[] | undefined
|
|
65
|
+
transactionId: string
|
|
66
|
+
timestamp: string
|
|
67
|
+
grants: Record<Grant, ExprNode>
|
|
68
|
+
identity: string | undefined
|
|
69
|
+
/**
|
|
70
|
+
* Error message thrown when the target document does not exist in either
|
|
71
|
+
* the base or working set.
|
|
72
|
+
*/
|
|
73
|
+
notFoundMessage?: string
|
|
74
|
+
/**
|
|
75
|
+
* Error message thrown when the working document fails the `update` grant.
|
|
76
|
+
*/
|
|
77
|
+
permissionMessage?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ApplySingleDocPatchResult {
|
|
81
|
+
base: DocumentSet
|
|
82
|
+
working: DocumentSet
|
|
83
|
+
/**
|
|
84
|
+
* Patch operations representing the minimal diff between base before and
|
|
85
|
+
* after the user's patches were applied. These are the patches that should
|
|
86
|
+
* be sent to the server (or applied to the working set as mutations).
|
|
87
|
+
*/
|
|
88
|
+
diffedPatches: PatchOperations[]
|
|
89
|
+
/**
|
|
90
|
+
* Mutation envelopes for `diffedPatches` already keyed to `documentId`.
|
|
91
|
+
* Useful for callers that want to push them to `outgoingMutations`.
|
|
92
|
+
*/
|
|
93
|
+
workingMutations: Mutation[]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Shared logic for applying user-provided patches to a single document that
|
|
98
|
+
* is identified by an exact ID (no draft/published wrapping). Used by the
|
|
99
|
+
* liveEdit branch of `document.edit` and by `release.edit`.
|
|
100
|
+
*
|
|
101
|
+
* Returns the updated base + working sets, plus the diffed patches in both
|
|
102
|
+
* raw and mutation form so the caller can decide what to send to the server.
|
|
103
|
+
*/
|
|
104
|
+
export function applySingleDocPatch({
|
|
105
|
+
base: initialBase,
|
|
106
|
+
working: initialWorking,
|
|
107
|
+
documentId,
|
|
108
|
+
patches,
|
|
109
|
+
transactionId,
|
|
110
|
+
timestamp,
|
|
111
|
+
grants,
|
|
112
|
+
identity,
|
|
113
|
+
notFoundMessage = 'Cannot edit document because it does not exist.',
|
|
114
|
+
permissionMessage = `You do not have permission to edit document "${documentId}".`,
|
|
115
|
+
}: ApplySingleDocPatchOptions): ApplySingleDocPatchResult {
|
|
116
|
+
let base = initialBase
|
|
117
|
+
let working = initialWorking
|
|
118
|
+
|
|
119
|
+
const userPatches = patches?.map((patch) => ({patch: {id: documentId, ...patch}}))
|
|
120
|
+
|
|
121
|
+
if (!userPatches?.length) {
|
|
122
|
+
return {base, working, diffedPatches: [], workingMutations: []}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!working[documentId] || !base[documentId]) {
|
|
126
|
+
throw new ActionError({documentId, transactionId, message: notFoundMessage})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const baseBefore = base[documentId]
|
|
130
|
+
base = processMutations({documents: base, transactionId, mutations: userPatches, timestamp})
|
|
131
|
+
const baseAfter = base[documentId]
|
|
132
|
+
const diffedPatches = diffValue(baseBefore, baseAfter) as PatchOperations[]
|
|
133
|
+
|
|
134
|
+
const workingBefore = working[documentId] as SanityDocument
|
|
135
|
+
if (!checkGrant(grants.update, workingBefore, identity)) {
|
|
136
|
+
throw new PermissionActionError({documentId, transactionId, message: permissionMessage})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const workingMutations: Mutation[] = diffedPatches.map((patch) => ({
|
|
140
|
+
patch: {id: documentId, ...patch},
|
|
141
|
+
}))
|
|
142
|
+
|
|
143
|
+
working = processMutations({
|
|
144
|
+
documents: working,
|
|
145
|
+
transactionId,
|
|
146
|
+
mutations: workingMutations,
|
|
147
|
+
timestamp,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return {base, working, diffedPatches, workingMutations}
|
|
151
|
+
}
|