@sanity/sdk 2.11.0 → 2.12.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 +171 -19
- package/dist/_chunks-es/_internal.js +41 -26
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +25 -9
- 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 +723 -418
- package/dist/index.js.map +1 -1
- package/package.json +16 -16
- package/src/_exports/index.ts +23 -2
- package/src/auth/refreshStampedToken.test.ts +2 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +116 -0
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +27 -9
- 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.ts +4 -3
- package/src/document/documentStore.ts +7 -6
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +43 -24
- package/src/document/permissions.ts +1 -1
- package/src/document/processActions/create.ts +135 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +141 -0
- package/src/document/processActions/processActions.ts +209 -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 +36 -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 +139 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +424 -2
- package/src/document/reducers.ts +41 -6
- 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/document/processActions.ts +0 -735
- package/src/telemetry/devMode.test.ts +0 -60
- package/src/telemetry/devMode.ts +0 -41
|
@@ -59,7 +59,8 @@ import {
|
|
|
59
59
|
type DocumentPermissionsResult,
|
|
60
60
|
type Grant,
|
|
61
61
|
} from './permissions'
|
|
62
|
-
import {ActionError} from './processActions'
|
|
62
|
+
import {ActionError} from './processActions/processActions'
|
|
63
|
+
import {isReleaseAction} from './processActions/releaseUtil'
|
|
63
64
|
import {
|
|
64
65
|
type AppliedTransaction,
|
|
65
66
|
applyFirstQueuedTransaction,
|
|
@@ -414,10 +415,11 @@ const subscribeToAppliedAndSubmitNextTransaction = ({
|
|
|
414
415
|
outgoing,
|
|
415
416
|
}))
|
|
416
417
|
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
|
|
418
|
+
// liveEdit transactions route to the mutations API; everything else routes
|
|
419
|
+
// to the actions API. processActions rejects transactions that mix the two,
|
|
420
|
+
// and reducers won't batch across that boundary, so a batch is always
|
|
421
|
+
// entirely liveEdit or entirely not.
|
|
422
|
+
if (outgoing.actions.some((action) => !isReleaseAction(action) && action.liveEdit)) {
|
|
421
423
|
return client.observable
|
|
422
424
|
.mutate(outgoing.outgoingMutations as Mutation[], {
|
|
423
425
|
transactionId: outgoing.transactionId,
|
|
@@ -430,7 +432,6 @@ const subscribeToAppliedAndSubmitNextTransaction = ({
|
|
|
430
432
|
.pipe(revertOnError, toResult)
|
|
431
433
|
}
|
|
432
434
|
|
|
433
|
-
// Pure non-liveEdit transactions use the actions API.
|
|
434
435
|
return client.observable
|
|
435
436
|
.action(outgoing.outgoingActions as Action[], {
|
|
436
437
|
transactionId: outgoing.transactionId,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {describe, expect, it} from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {type DocumentAction} from '../document/actions'
|
|
3
|
+
import {type Action, type DocumentAction} from '../document/actions'
|
|
4
4
|
import {type DocumentEvent, getDocumentEvents} from '../document/events'
|
|
5
5
|
import {type OutgoingTransaction} from '../document/reducers'
|
|
6
6
|
|
|
@@ -41,11 +41,66 @@ describe('getDocumentEvents', () => {
|
|
|
41
41
|
expect(events).toHaveLength(outgoing.actions.length)
|
|
42
42
|
events.forEach((event) => {
|
|
43
43
|
const action = outgoing.actions.find(
|
|
44
|
-
(a) => 'documentId' in event && event.documentId === a.documentId,
|
|
44
|
+
(a) => 'documentId' in a && 'documentId' in event && event.documentId === a.documentId,
|
|
45
45
|
)
|
|
46
46
|
expect(action).toBeDefined()
|
|
47
47
|
expect(event.type).toEqual(expectedMap[action!.type])
|
|
48
48
|
expect((event as Extract<DocumentEvent, {outgoing: unknown}>).outgoing).toBe(outgoing)
|
|
49
49
|
})
|
|
50
50
|
})
|
|
51
|
+
|
|
52
|
+
it('emits created/edited/deleted events for release.create/edit/delete with release doc IDs', () => {
|
|
53
|
+
const outgoing: OutgoingTransaction = {
|
|
54
|
+
transactionId: 'txn-release',
|
|
55
|
+
actions: [
|
|
56
|
+
{type: 'release.create', releaseId: 'r1'} as Action,
|
|
57
|
+
{type: 'release.edit', releaseId: 'r2', patch: {set: {}}} as Action,
|
|
58
|
+
{type: 'release.delete', releaseId: 'r3'} as Action,
|
|
59
|
+
],
|
|
60
|
+
disableBatching: false,
|
|
61
|
+
batchedTransactionIds: [],
|
|
62
|
+
outgoingActions: [],
|
|
63
|
+
outgoingMutations: [],
|
|
64
|
+
base: {},
|
|
65
|
+
working: {},
|
|
66
|
+
previous: {},
|
|
67
|
+
previousRevs: {},
|
|
68
|
+
timestamp: '2025-02-06T00:00:00.000Z',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const events = getDocumentEvents(outgoing)
|
|
72
|
+
expect(events).toEqual([
|
|
73
|
+
{type: 'created', documentId: '_.releases.r1', outgoing},
|
|
74
|
+
{type: 'edited', documentId: '_.releases.r2', outgoing},
|
|
75
|
+
{type: 'deleted', documentId: '_.releases.r3', outgoing},
|
|
76
|
+
])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('skips release actions that have no local mutation (publish/schedule/archive/etc.)', () => {
|
|
80
|
+
const outgoing: OutgoingTransaction = {
|
|
81
|
+
transactionId: 'txn-release-noop',
|
|
82
|
+
actions: [
|
|
83
|
+
{type: 'release.publish', releaseId: 'r1'} as Action,
|
|
84
|
+
{
|
|
85
|
+
type: 'release.schedule',
|
|
86
|
+
releaseId: 'r2',
|
|
87
|
+
publishAt: '2026-01-01T00:00:00.000Z',
|
|
88
|
+
} as Action,
|
|
89
|
+
{type: 'release.unschedule', releaseId: 'r3'} as Action,
|
|
90
|
+
{type: 'release.archive', releaseId: 'r4'} as Action,
|
|
91
|
+
{type: 'release.unarchive', releaseId: 'r5'} as Action,
|
|
92
|
+
],
|
|
93
|
+
disableBatching: false,
|
|
94
|
+
batchedTransactionIds: [],
|
|
95
|
+
outgoingActions: [],
|
|
96
|
+
outgoingMutations: [],
|
|
97
|
+
base: {},
|
|
98
|
+
working: {},
|
|
99
|
+
previous: {},
|
|
100
|
+
previousRevs: {},
|
|
101
|
+
timestamp: '2025-02-06T00:00:00.000Z',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
expect(getDocumentEvents(outgoing)).toEqual([])
|
|
105
|
+
})
|
|
51
106
|
})
|
package/src/document/events.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {type MultipleMutationResult, type SanityClient} from '@sanity/client'
|
|
2
2
|
|
|
3
|
-
import {type DocumentAction} from './actions'
|
|
3
|
+
import {type DocumentAction, type ReleaseAction} from './actions'
|
|
4
|
+
import {getReleaseDocumentId, isReleaseAction} from './processActions/releaseUtil'
|
|
4
5
|
import {type OutgoingTransaction} from './reducers'
|
|
5
6
|
|
|
6
7
|
/** @beta Response body from submitting an outgoing transaction (actions or mutations API). */
|
|
@@ -118,31 +119,49 @@ export interface DocumentDiscardedEvent {
|
|
|
118
119
|
outgoing: OutgoingTransaction
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
122
|
+
// Release actions that write a mutation to the local release doc map onto
|
|
123
|
+
// the regular per-document events with `documentId = '_.releases.<releaseId>'`.
|
|
124
|
+
// The other release actions (publish/schedule/unschedule/archive/unarchive)
|
|
125
|
+
// don't mutate local state, so they aren't in the map and get skipped — they
|
|
126
|
+
// surface through the transaction-level `accepted`/`reverted` events instead.
|
|
127
|
+
const actionMap = {
|
|
128
|
+
'document.create': 'created',
|
|
129
|
+
'document.delete': 'deleted',
|
|
130
|
+
'document.discard': 'discarded',
|
|
131
|
+
'document.edit': 'edited',
|
|
132
|
+
'document.publish': 'published',
|
|
133
|
+
'document.unpublish': 'unpublished',
|
|
134
|
+
'release.create': 'created',
|
|
135
|
+
'release.edit': 'edited',
|
|
136
|
+
'release.delete': 'deleted',
|
|
137
|
+
} satisfies Partial<Record<DocumentAction['type'] | ReleaseAction['type'], DocumentEvent['type']>>
|
|
138
|
+
|
|
139
|
+
type MappedActionType = keyof typeof actionMap
|
|
133
140
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
export function getDocumentEvents(outgoing: OutgoingTransaction): DocumentEvent[] {
|
|
142
|
+
const documentIdsByAction = outgoing.actions.reduce(
|
|
143
|
+
(acc, action) => {
|
|
144
|
+
if (!(action.type in actionMap)) return acc
|
|
145
|
+
const documentId = isReleaseAction(action)
|
|
146
|
+
? getReleaseDocumentId(action.releaseId)
|
|
147
|
+
: action.documentId
|
|
148
|
+
if (!documentId) return acc
|
|
149
|
+
const type = action.type as MappedActionType
|
|
150
|
+
const ids = acc[type] ?? new Set<string>()
|
|
151
|
+
ids.add(documentId)
|
|
152
|
+
acc[type] = ids
|
|
153
|
+
return acc
|
|
154
|
+
},
|
|
155
|
+
{} as Partial<Record<MappedActionType, Set<string>>>,
|
|
156
|
+
)
|
|
142
157
|
|
|
143
|
-
return documentIdsByAction.flatMap(([actionType, documentIds]) =>
|
|
144
|
-
Array.from(documentIds).map(
|
|
145
|
-
(documentId): DocumentEvent => ({
|
|
158
|
+
return Object.entries(documentIdsByAction).flatMap(([actionType, documentIds]) =>
|
|
159
|
+
Array.from(documentIds ?? []).map(
|
|
160
|
+
(documentId): DocumentEvent => ({
|
|
161
|
+
type: actionMap[actionType as MappedActionType],
|
|
162
|
+
documentId,
|
|
163
|
+
outgoing,
|
|
164
|
+
}),
|
|
146
165
|
),
|
|
147
166
|
)
|
|
148
167
|
}
|
|
@@ -7,7 +7,7 @@ import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
|
7
7
|
import {type SelectorContext} from '../store/createStateSourceAction'
|
|
8
8
|
import {MultiKeyWeakMap} from '../utils/MultiKeyWeakMap'
|
|
9
9
|
import {type DocumentAction} from './actions'
|
|
10
|
-
import {ActionError, PermissionActionError, processActions} from './processActions'
|
|
10
|
+
import {ActionError, PermissionActionError, processActions} from './processActions/processActions'
|
|
11
11
|
import {type DocumentSet} from './processMutations'
|
|
12
12
|
import {type SyncTransactionState} from './reducers'
|
|
13
13
|
|
|
@@ -0,0 +1,135 @@
|
|
|
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, 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)) {
|
|
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)) {
|
|
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 (!versionId && !checkGrant(grants.create, working[draftId] as SanityDocument)) {
|
|
121
|
+
throw new PermissionActionError({
|
|
122
|
+
documentId,
|
|
123
|
+
transactionId,
|
|
124
|
+
message: `You do not have permission to create a draft for document "${documentId}".`,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
outgoingMutations.push(...mutations)
|
|
129
|
+
outgoingActions.push({
|
|
130
|
+
actionType: 'sanity.action.document.version.create',
|
|
131
|
+
publishedId,
|
|
132
|
+
attributes: newDocWorking,
|
|
133
|
+
})
|
|
134
|
+
return {base, working}
|
|
135
|
+
}
|
|
@@ -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, 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])) {
|
|
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])
|
|
76
|
+
const cantDeletePublished =
|
|
77
|
+
working[publishedId] && !checkGrant(grants.update, working[publishedId])
|
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {DocumentId, getDraftId, getVersionId} from '@sanity/id-utils'
|
|
2
|
+
import {type Mutation} from '@sanity/types'
|
|
3
|
+
|
|
4
|
+
import {isReleasePerspective} from '../../releases/utils/isReleasePerspective'
|
|
5
|
+
import {type DiscardDocumentAction} 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 handleDiscard(
|
|
16
|
+
action: DiscardDocumentAction,
|
|
17
|
+
ctx: ActionHandlerContext,
|
|
18
|
+
): ActionHandlerResult {
|
|
19
|
+
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations} = ctx
|
|
20
|
+
let {base, working} = ctx
|
|
21
|
+
|
|
22
|
+
const documentId = getId(action.documentId)
|
|
23
|
+
|
|
24
|
+
if (action.liveEdit) {
|
|
25
|
+
throw new ActionError({
|
|
26
|
+
documentId,
|
|
27
|
+
transactionId,
|
|
28
|
+
message: `Cannot discard changes for liveEdit document "${documentId}". LiveEdit documents do not support drafts.`,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// draft/published or version logic
|
|
33
|
+
const versionId = isReleasePerspective(action.perspective)
|
|
34
|
+
? getVersionId(DocumentId(documentId), action.perspective.releaseName)
|
|
35
|
+
: getDraftId(DocumentId(documentId))
|
|
36
|
+
const mutations: Mutation[] = [{delete: {id: versionId}}]
|
|
37
|
+
|
|
38
|
+
if (!working[versionId]) {
|
|
39
|
+
throw new ActionError({
|
|
40
|
+
documentId,
|
|
41
|
+
transactionId,
|
|
42
|
+
message: `There is no draft or version available to discard for document "${documentId}".`,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!checkGrant(grants.update, working[versionId])) {
|
|
47
|
+
throw new PermissionActionError({
|
|
48
|
+
documentId,
|
|
49
|
+
transactionId,
|
|
50
|
+
message: `You do not have permission to discard changes for document "${documentId}".`,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
base = processMutations({documents: base, transactionId, mutations, timestamp})
|
|
55
|
+
working = processMutations({documents: working, transactionId, mutations, timestamp})
|
|
56
|
+
|
|
57
|
+
outgoingMutations.push(...mutations)
|
|
58
|
+
outgoingActions.push({
|
|
59
|
+
actionType: 'sanity.action.document.version.discard',
|
|
60
|
+
versionId,
|
|
61
|
+
})
|
|
62
|
+
return {base, working}
|
|
63
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {diffValue} from '@sanity/diff-patch'
|
|
2
|
+
import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
|
|
3
|
+
import {type Mutation, type PatchOperations, type SanityDocument} from '@sanity/types'
|
|
4
|
+
|
|
5
|
+
import {isReleasePerspective} from '../../releases/utils/isReleasePerspective'
|
|
6
|
+
import {type EditDocumentAction} from '../actions'
|
|
7
|
+
import {getId, processMutations} from '../processMutations'
|
|
8
|
+
import {
|
|
9
|
+
ActionError,
|
|
10
|
+
type ActionHandlerContext,
|
|
11
|
+
type ActionHandlerResult,
|
|
12
|
+
applySingleDocPatch,
|
|
13
|
+
checkGrant,
|
|
14
|
+
PermissionActionError,
|
|
15
|
+
} from './shared'
|
|
16
|
+
|
|
17
|
+
export function handleEdit(
|
|
18
|
+
action: EditDocumentAction,
|
|
19
|
+
ctx: ActionHandlerContext,
|
|
20
|
+
): ActionHandlerResult {
|
|
21
|
+
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations} = ctx
|
|
22
|
+
let {base, working} = ctx
|
|
23
|
+
|
|
24
|
+
const documentId = getId(action.documentId)
|
|
25
|
+
|
|
26
|
+
if (action.liveEdit) {
|
|
27
|
+
const result = applySingleDocPatch({
|
|
28
|
+
base,
|
|
29
|
+
working,
|
|
30
|
+
documentId,
|
|
31
|
+
patches: action.patches,
|
|
32
|
+
transactionId,
|
|
33
|
+
timestamp,
|
|
34
|
+
grants,
|
|
35
|
+
})
|
|
36
|
+
// liveEdit documents use the mutation endpoint directly -- we don't send actions
|
|
37
|
+
outgoingMutations.push(...result.workingMutations)
|
|
38
|
+
return {base: result.base, working: result.working}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const versionId = isReleasePerspective(action.perspective)
|
|
42
|
+
? getVersionId(DocumentId(documentId), action.perspective.releaseName)
|
|
43
|
+
: undefined
|
|
44
|
+
const draftId = getDraftId(DocumentId(documentId))
|
|
45
|
+
const publishedId = getPublishedId(DocumentId(documentId))
|
|
46
|
+
const patchDocumentId = isReleasePerspective(action.perspective) ? versionId! : draftId
|
|
47
|
+
const userPatches = action.patches?.map((patch) => ({
|
|
48
|
+
patch: {id: patchDocumentId, ...patch},
|
|
49
|
+
}))
|
|
50
|
+
|
|
51
|
+
// skip this action if there are no associated patches
|
|
52
|
+
if (!userPatches?.length) return {base, working}
|
|
53
|
+
|
|
54
|
+
if (isReleasePerspective(action.perspective)) {
|
|
55
|
+
if (!working[versionId!] && !base[versionId!]) {
|
|
56
|
+
throw new ActionError({
|
|
57
|
+
documentId,
|
|
58
|
+
transactionId,
|
|
59
|
+
message: `This document does not exist in the release. Please create it or add it to the release first.`,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
} else if (
|
|
63
|
+
(!working[draftId] && !working[publishedId]) ||
|
|
64
|
+
(!base[draftId] && !base[publishedId])
|
|
65
|
+
) {
|
|
66
|
+
throw new ActionError({
|
|
67
|
+
documentId,
|
|
68
|
+
transactionId,
|
|
69
|
+
message: `Cannot edit document because it does not exist in draft or published form.`,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const baseMutations: Mutation[] = []
|
|
74
|
+
// don't create a draft from the published version in a release perspective
|
|
75
|
+
if (!isReleasePerspective(action.perspective) && !base[draftId] && base[publishedId]) {
|
|
76
|
+
// otherwise make a draft from the published version
|
|
77
|
+
baseMutations.push({create: {...base[publishedId], _id: draftId}})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// the above statement and guards should make this never be null or undefined
|
|
81
|
+
const baseBefore = base[patchDocumentId] ?? base[publishedId]
|
|
82
|
+
if (userPatches) {
|
|
83
|
+
baseMutations.push(...userPatches)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
base = processMutations({
|
|
87
|
+
documents: base,
|
|
88
|
+
transactionId,
|
|
89
|
+
mutations: baseMutations,
|
|
90
|
+
timestamp,
|
|
91
|
+
})
|
|
92
|
+
// this one will always be defined because a patch mutation will never
|
|
93
|
+
// delete an input document
|
|
94
|
+
const baseAfter = base[patchDocumentId] as SanityDocument
|
|
95
|
+
const patches = diffValue(baseBefore, baseAfter)
|
|
96
|
+
|
|
97
|
+
const workingMutations: Mutation[] = []
|
|
98
|
+
if (!isReleasePerspective(action.perspective) && !working[draftId] && working[publishedId]) {
|
|
99
|
+
const newDraftFromPublished = {...working[publishedId], _id: draftId}
|
|
100
|
+
|
|
101
|
+
if (!checkGrant(grants.create, newDraftFromPublished)) {
|
|
102
|
+
throw new PermissionActionError({
|
|
103
|
+
documentId,
|
|
104
|
+
transactionId,
|
|
105
|
+
message: `You do not have permission to create a draft for editing this document.`,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
workingMutations.push({create: newDraftFromPublished})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// the first if statement should make this never be null or undefined
|
|
113
|
+
const workingBefore = working[patchDocumentId] ?? working[publishedId]
|
|
114
|
+
if (!checkGrant(grants.update, workingBefore!)) {
|
|
115
|
+
throw new PermissionActionError({
|
|
116
|
+
documentId,
|
|
117
|
+
transactionId,
|
|
118
|
+
message: `You do not have permission to edit document "${documentId}".`,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
workingMutations.push(...patches.map((patch) => ({patch: {id: patchDocumentId, ...patch}})))
|
|
122
|
+
|
|
123
|
+
working = processMutations({
|
|
124
|
+
documents: working,
|
|
125
|
+
transactionId,
|
|
126
|
+
mutations: workingMutations,
|
|
127
|
+
timestamp,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
outgoingMutations.push(...workingMutations)
|
|
131
|
+
outgoingActions.push(
|
|
132
|
+
...patches.map((patch) => ({
|
|
133
|
+
actionType: 'sanity.action.document.edit' as const,
|
|
134
|
+
draftId: patchDocumentId,
|
|
135
|
+
publishedId,
|
|
136
|
+
patch: patch as PatchOperations,
|
|
137
|
+
})),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return {base, working}
|
|
141
|
+
}
|