@sanity/sdk 2.11.0 → 2.11.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-es/createGroqSearchFilter.js +10 -5
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/version.js +1 -1
- package/dist/index.js +396 -371
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- 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/document/documentStore.ts +1 -1
- 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 +176 -0
- package/src/document/processActions/processActions.ts +168 -0
- package/src/document/processActions/publish.ts +120 -0
- package/src/document/processActions/shared.ts +47 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +1 -1
- package/src/document/reducers.ts +1 -1
- package/src/document/processActions.ts +0 -735
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -48,36 +48,36 @@
|
|
|
48
48
|
"browserslist": "extends @sanity/browserslist-config",
|
|
49
49
|
"prettier": "@sanity/prettier-config",
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@sanity/bifur-client": "^0.
|
|
51
|
+
"@sanity/bifur-client": "^1.0.0",
|
|
52
52
|
"@sanity/client": "^7.22.0",
|
|
53
|
-
"@sanity/comlink": "^
|
|
53
|
+
"@sanity/comlink": "^4.0.1",
|
|
54
54
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
55
55
|
"@sanity/diff-patch": "^6.0.0",
|
|
56
56
|
"@sanity/id-utils": "^1.0.0",
|
|
57
|
-
"@sanity/image-url": "^2.
|
|
57
|
+
"@sanity/image-url": "^2.1.1",
|
|
58
58
|
"@sanity/json-match": "^1.0.5",
|
|
59
59
|
"@sanity/message-protocol": "^0.23.0",
|
|
60
60
|
"@sanity/mutate": "^0.16.1",
|
|
61
61
|
"@sanity/telemetry": "^1.1.0",
|
|
62
|
-
"@sanity/types": "^5.
|
|
62
|
+
"@sanity/types": "^5.24.0",
|
|
63
63
|
"groq": "3.88.1-typegen-experimental.0",
|
|
64
64
|
"groq-js": "^1.30.1",
|
|
65
65
|
"reselect": "^5.1.1",
|
|
66
66
|
"rxjs": "^7.8.2",
|
|
67
|
-
"zustand": "^5.0.
|
|
67
|
+
"zustand": "^5.0.13"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@sanity/browserslist-config": "^1.0.5",
|
|
71
71
|
"@sanity/pkg-utils": "^8.1.29",
|
|
72
72
|
"@sanity/prettier-config": "^1.0.6",
|
|
73
|
-
"@types/node": "^
|
|
73
|
+
"@types/node": "^24.12.3",
|
|
74
74
|
"@vitest/coverage-v8": "4.1.5",
|
|
75
|
-
"eslint": "^9.
|
|
76
|
-
"prettier": "^3.
|
|
75
|
+
"eslint": "^9.39.4",
|
|
76
|
+
"prettier": "^3.8.3",
|
|
77
77
|
"rollup-plugin-visualizer": "^5.14.0",
|
|
78
|
-
"typescript": "^5.
|
|
79
|
-
"vite": "^7.
|
|
80
|
-
"vitest": "^4.1.
|
|
78
|
+
"typescript": "^5.9.3",
|
|
79
|
+
"vite": "^7.3.3",
|
|
80
|
+
"vitest": "^4.1.5",
|
|
81
81
|
"@repo/config-eslint": "0.0.0",
|
|
82
82
|
"@repo/package.bundle": "3.82.0",
|
|
83
83
|
"@repo/config-test": "0.0.1",
|
|
@@ -59,8 +59,8 @@ describe('refreshStampedToken', () => {
|
|
|
59
59
|
request: vi.fn(
|
|
60
60
|
async (
|
|
61
61
|
_name: string,
|
|
62
|
-
_options: LockOptions | LockGrantedCallback
|
|
63
|
-
callback?: LockGrantedCallback
|
|
62
|
+
_options: LockOptions | LockGrantedCallback<unknown>,
|
|
63
|
+
callback?: LockGrantedCallback<unknown>,
|
|
64
64
|
) => {
|
|
65
65
|
const actualCallback = typeof _options === 'function' ? _options : callback
|
|
66
66
|
if (!actualCallback) return false
|
|
@@ -112,4 +112,120 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
112
112
|
|
|
113
113
|
subscription.unsubscribe()
|
|
114
114
|
})
|
|
115
|
+
|
|
116
|
+
it('recovers from a fetch error when a new token is set', () => {
|
|
117
|
+
const error = new Error('Unauthorized')
|
|
118
|
+
const mockUser = {id: 'recovered-user'} as CurrentUser
|
|
119
|
+
const mockRequest = vi
|
|
120
|
+
.fn()
|
|
121
|
+
.mockReturnValueOnce(throwError(() => error))
|
|
122
|
+
.mockReturnValueOnce(of(mockUser))
|
|
123
|
+
const mockClient = {observable: {request: mockRequest}}
|
|
124
|
+
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
125
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
126
|
+
|
|
127
|
+
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
128
|
+
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
129
|
+
|
|
130
|
+
// First token causes a 401 — state should transition to ERROR
|
|
131
|
+
state.set('setLoggedIn', {
|
|
132
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'expired-token', currentUser: null},
|
|
133
|
+
})
|
|
134
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error}})
|
|
135
|
+
|
|
136
|
+
// Simulate comlink providing a fresh token (setAuthToken sets LOGGED_IN with new token)
|
|
137
|
+
state.set('setNewToken', {
|
|
138
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'fresh-token', currentUser: null},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Subscription should still be alive — re-fetches /users/me with the new token
|
|
142
|
+
expect(state.get()).toMatchObject({
|
|
143
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'fresh-token', currentUser: mockUser},
|
|
144
|
+
})
|
|
145
|
+
expect(mockRequest).toHaveBeenCalledTimes(2)
|
|
146
|
+
|
|
147
|
+
subscription.unsubscribe()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('recovers from multiple consecutive fetch errors', () => {
|
|
151
|
+
const error1 = new Error('Unauthorized')
|
|
152
|
+
const error2 = new Error('Unauthorized again')
|
|
153
|
+
const mockUser = {id: 'finally-recovered'} as CurrentUser
|
|
154
|
+
const mockRequest = vi
|
|
155
|
+
.fn()
|
|
156
|
+
.mockReturnValueOnce(throwError(() => error1))
|
|
157
|
+
.mockReturnValueOnce(throwError(() => error2))
|
|
158
|
+
.mockReturnValueOnce(of(mockUser))
|
|
159
|
+
const mockClient = {observable: {request: mockRequest}}
|
|
160
|
+
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
161
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
162
|
+
|
|
163
|
+
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
164
|
+
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
165
|
+
|
|
166
|
+
// First attempt fails
|
|
167
|
+
state.set('setLoggedIn', {
|
|
168
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-1', currentUser: null},
|
|
169
|
+
})
|
|
170
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error: error1}})
|
|
171
|
+
|
|
172
|
+
// Second attempt also fails
|
|
173
|
+
state.set('setNewToken', {
|
|
174
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-2', currentUser: null},
|
|
175
|
+
})
|
|
176
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error: error2}})
|
|
177
|
+
|
|
178
|
+
// Third attempt succeeds
|
|
179
|
+
state.set('setNewToken', {
|
|
180
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-3', currentUser: null},
|
|
181
|
+
})
|
|
182
|
+
expect(state.get()).toMatchObject({
|
|
183
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-3', currentUser: mockUser},
|
|
184
|
+
})
|
|
185
|
+
expect(mockRequest).toHaveBeenCalledTimes(3)
|
|
186
|
+
|
|
187
|
+
subscription.unsubscribe()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('does not re-fetch with the same token but recovers with a different token', () => {
|
|
191
|
+
const error = new Error('Unauthorized')
|
|
192
|
+
const mockUser = {id: 'recovered-user'} as CurrentUser
|
|
193
|
+
const mockRequest = vi
|
|
194
|
+
.fn()
|
|
195
|
+
.mockReturnValueOnce(throwError(() => error))
|
|
196
|
+
.mockReturnValueOnce(of(mockUser))
|
|
197
|
+
const mockClient = {observable: {request: mockRequest}}
|
|
198
|
+
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
199
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
200
|
+
|
|
201
|
+
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
202
|
+
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
203
|
+
|
|
204
|
+
// First attempt fails
|
|
205
|
+
state.set('setLoggedIn', {
|
|
206
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'same-token', currentUser: null},
|
|
207
|
+
})
|
|
208
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error}})
|
|
209
|
+
|
|
210
|
+
// Same token should be blocked by distinctUntilChanged — no re-fetch
|
|
211
|
+
state.set('setNewToken', {
|
|
212
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'same-token', currentUser: null},
|
|
213
|
+
})
|
|
214
|
+
expect(mockRequest).toHaveBeenCalledTimes(1)
|
|
215
|
+
|
|
216
|
+
// A different token should pass distinctUntilChanged and trigger recovery
|
|
217
|
+
state.set('setNewToken', {
|
|
218
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'different-token', currentUser: null},
|
|
219
|
+
})
|
|
220
|
+
expect(state.get()).toMatchObject({
|
|
221
|
+
authState: {
|
|
222
|
+
type: AuthStateType.LOGGED_IN,
|
|
223
|
+
token: 'different-token',
|
|
224
|
+
currentUser: mockUser,
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
expect(mockRequest).toHaveBeenCalledTimes(2)
|
|
228
|
+
|
|
229
|
+
subscription.unsubscribe()
|
|
230
|
+
})
|
|
115
231
|
})
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import {type CurrentUser} from '@sanity/types'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
catchError,
|
|
4
|
+
distinctUntilChanged,
|
|
5
|
+
EMPTY,
|
|
6
|
+
filter,
|
|
7
|
+
map,
|
|
8
|
+
type Subscription,
|
|
9
|
+
switchMap,
|
|
10
|
+
} from 'rxjs'
|
|
3
11
|
|
|
4
12
|
import {type StoreContext} from '../store/defineStore'
|
|
5
13
|
import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
|
|
@@ -60,11 +68,24 @@ export const subscribeToStateAndFetchCurrentUser = (
|
|
|
60
68
|
}),
|
|
61
69
|
),
|
|
62
70
|
switchMap((client) =>
|
|
63
|
-
client.observable
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
client.observable
|
|
72
|
+
.request<CurrentUser>({
|
|
73
|
+
uri: '/users/me',
|
|
74
|
+
method: 'GET',
|
|
75
|
+
tag: 'users.get-current',
|
|
76
|
+
})
|
|
77
|
+
.pipe(
|
|
78
|
+
/**
|
|
79
|
+
* Catch inside switchMap so the outer subscription survives.
|
|
80
|
+
* Without this, a 401 terminates the subscription permanently
|
|
81
|
+
* and subsequent token refreshes via comlink never re-fetch /users/me.
|
|
82
|
+
* @see SDK-1409
|
|
83
|
+
*/
|
|
84
|
+
catchError((error) => {
|
|
85
|
+
state.set('setError', {authState: {type: AuthStateType.ERROR, error}})
|
|
86
|
+
return EMPTY
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
68
89
|
),
|
|
69
90
|
)
|
|
70
91
|
|
|
@@ -77,8 +98,5 @@ export const subscribeToStateAndFetchCurrentUser = (
|
|
|
77
98
|
: prev.authState,
|
|
78
99
|
}))
|
|
79
100
|
},
|
|
80
|
-
error: (error) => {
|
|
81
|
-
state.set('setError', {authState: {type: AuthStateType.ERROR, error}})
|
|
82
|
-
},
|
|
83
101
|
})
|
|
84
102
|
}
|
|
@@ -59,7 +59,7 @@ 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
63
|
import {
|
|
64
64
|
type AppliedTransaction,
|
|
65
65
|
applyFirstQueuedTransaction,
|
|
@@ -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
|
+
}
|