@sanity/sdk 2.12.0 → 2.14.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/createGroqSearchFilter.d.ts +925 -0
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +261 -225
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/version.js +1 -1
- package/dist/_exports/_internal.d.ts +3 -2
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/index.d.ts +1856 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +207 -133
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authStore.test.ts +96 -1
- package/src/auth/authStore.ts +55 -24
- package/src/auth/handleAuthCallback.test.ts +23 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +68 -1
- package/src/auth/logout.ts +22 -3
- package/src/auth/refreshStampedToken.test.ts +15 -0
- package/src/auth/refreshStampedToken.ts +12 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +17 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +9 -0
- package/src/document/applyDocumentActions.test.ts +24 -0
- package/src/document/applyDocumentActions.ts +13 -2
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.test.ts +69 -0
- package/src/document/documentStore.ts +36 -5
- 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 +4 -3
- package/src/document/processActions/processActions.ts +9 -0
- package/src/document/processActions/publish.ts +4 -4
- package/src/document/processActions/releaseArchive.ts +4 -4
- package/src/document/processActions/releaseCreate.ts +2 -2
- package/src/document/processActions/releaseDelete.ts +2 -2
- package/src/document/processActions/releaseEdit.ts +2 -1
- package/src/document/processActions/releasePublish.ts +2 -2
- package/src/document/processActions/releaseSchedule.ts +4 -4
- package/src/document/processActions/shared.ts +15 -3
- package/src/document/processActions/unpublish.ts +3 -3
- package/src/document/reducers.ts +4 -3
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/dist/_chunks-dts/utils.d.ts +0 -2774
|
@@ -18,7 +18,7 @@ export function handleEdit(
|
|
|
18
18
|
action: EditDocumentAction,
|
|
19
19
|
ctx: ActionHandlerContext,
|
|
20
20
|
): ActionHandlerResult {
|
|
21
|
-
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations} = ctx
|
|
21
|
+
const {transactionId, timestamp, grants, identity, outgoingActions, outgoingMutations} = ctx
|
|
22
22
|
let {base, working} = ctx
|
|
23
23
|
|
|
24
24
|
const documentId = getId(action.documentId)
|
|
@@ -32,6 +32,7 @@ export function handleEdit(
|
|
|
32
32
|
transactionId,
|
|
33
33
|
timestamp,
|
|
34
34
|
grants,
|
|
35
|
+
identity,
|
|
35
36
|
})
|
|
36
37
|
// liveEdit documents use the mutation endpoint directly -- we don't send actions
|
|
37
38
|
outgoingMutations.push(...result.workingMutations)
|
|
@@ -98,7 +99,7 @@ export function handleEdit(
|
|
|
98
99
|
if (!isReleasePerspective(action.perspective) && !working[draftId] && working[publishedId]) {
|
|
99
100
|
const newDraftFromPublished = {...working[publishedId], _id: draftId}
|
|
100
101
|
|
|
101
|
-
if (!checkGrant(grants.create, newDraftFromPublished)) {
|
|
102
|
+
if (!checkGrant(grants.create, newDraftFromPublished, identity)) {
|
|
102
103
|
throw new PermissionActionError({
|
|
103
104
|
documentId,
|
|
104
105
|
transactionId,
|
|
@@ -111,7 +112,7 @@ export function handleEdit(
|
|
|
111
112
|
|
|
112
113
|
// the first if statement should make this never be null or undefined
|
|
113
114
|
const workingBefore = working[patchDocumentId] ?? working[publishedId]
|
|
114
|
-
if (!checkGrant(grants.update, workingBefore
|
|
115
|
+
if (!checkGrant(grants.update, workingBefore!, identity)) {
|
|
115
116
|
throw new PermissionActionError({
|
|
116
117
|
documentId,
|
|
117
118
|
transactionId,
|
|
@@ -65,6 +65,13 @@ interface ProcessActionsOptions {
|
|
|
65
65
|
*/
|
|
66
66
|
grants: Record<Grant, ExprNode>
|
|
67
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
|
+
|
|
68
75
|
// // TODO: implement initial values from the schema?
|
|
69
76
|
// initialValues?: {[TDocumentType in string]?: {_type: string}}
|
|
70
77
|
}
|
|
@@ -116,6 +123,7 @@ export function processActions({
|
|
|
116
123
|
base: initialBase,
|
|
117
124
|
timestamp,
|
|
118
125
|
grants,
|
|
126
|
+
identity,
|
|
119
127
|
}: ProcessActionsOptions): ProcessActionsResult {
|
|
120
128
|
let base: DocumentSet = {...initialBase}
|
|
121
129
|
let working: DocumentSet = {...initialWorking}
|
|
@@ -148,6 +156,7 @@ export function processActions({
|
|
|
148
156
|
transactionId,
|
|
149
157
|
timestamp,
|
|
150
158
|
grants,
|
|
159
|
+
identity,
|
|
151
160
|
outgoingActions,
|
|
152
161
|
outgoingMutations,
|
|
153
162
|
})
|
|
@@ -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,
|
|
@@ -12,7 +12,7 @@ export function handleReleaseArchive(
|
|
|
12
12
|
action: ArchiveReleaseAction,
|
|
13
13
|
ctx: ActionHandlerContext,
|
|
14
14
|
): ActionHandlerResult {
|
|
15
|
-
const {base, working, grants, outgoingActions, transactionId} = ctx
|
|
15
|
+
const {base, working, grants, outgoingActions, transactionId, identity} = ctx
|
|
16
16
|
|
|
17
17
|
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
18
18
|
const existing = working[releaseDocumentId] ?? base[releaseDocumentId]
|
|
@@ -24,7 +24,7 @@ export function handleReleaseArchive(
|
|
|
24
24
|
})
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
if (!checkGrant(grants.update, existing)) {
|
|
27
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
28
28
|
throw new PermissionActionError({
|
|
29
29
|
documentId: releaseDocumentId,
|
|
30
30
|
transactionId,
|
|
@@ -48,7 +48,7 @@ export function handleReleaseUnarchive(
|
|
|
48
48
|
action: UnarchiveReleaseAction,
|
|
49
49
|
ctx: ActionHandlerContext,
|
|
50
50
|
): ActionHandlerResult {
|
|
51
|
-
const {base, working, grants, outgoingActions, transactionId} = ctx
|
|
51
|
+
const {base, working, grants, outgoingActions, transactionId, identity} = ctx
|
|
52
52
|
|
|
53
53
|
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
54
54
|
const existing = working[releaseDocumentId] ?? base[releaseDocumentId]
|
|
@@ -60,7 +60,7 @@ export function handleReleaseUnarchive(
|
|
|
60
60
|
})
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
if (!checkGrant(grants.update, existing)) {
|
|
63
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
64
64
|
throw new PermissionActionError({
|
|
65
65
|
documentId: releaseDocumentId,
|
|
66
66
|
transactionId,
|
|
@@ -15,7 +15,7 @@ export function handleReleaseCreate(
|
|
|
15
15
|
action: CreateReleaseAction,
|
|
16
16
|
ctx: ActionHandlerContext,
|
|
17
17
|
): ActionHandlerResult {
|
|
18
|
-
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations} = ctx
|
|
18
|
+
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations, identity} = ctx
|
|
19
19
|
let {base, working} = ctx
|
|
20
20
|
|
|
21
21
|
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
@@ -40,7 +40,7 @@ export function handleReleaseCreate(
|
|
|
40
40
|
base = processMutations({documents: base, transactionId, mutations, timestamp})
|
|
41
41
|
working = processMutations({documents: working, transactionId, mutations, timestamp})
|
|
42
42
|
|
|
43
|
-
if (!checkGrant(grants.create, working[releaseDocumentId] as SanityDocument)) {
|
|
43
|
+
if (!checkGrant(grants.create, working[releaseDocumentId] as SanityDocument, identity)) {
|
|
44
44
|
throw new PermissionActionError({
|
|
45
45
|
documentId: releaseDocumentId,
|
|
46
46
|
transactionId,
|
|
@@ -19,7 +19,7 @@ export function handleReleaseDelete(
|
|
|
19
19
|
action: DeleteReleaseAction,
|
|
20
20
|
ctx: ActionHandlerContext,
|
|
21
21
|
): ActionHandlerResult {
|
|
22
|
-
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations} = ctx
|
|
22
|
+
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations, identity} = ctx
|
|
23
23
|
let {base, working} = ctx
|
|
24
24
|
|
|
25
25
|
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
@@ -42,7 +42,7 @@ export function handleReleaseDelete(
|
|
|
42
42
|
})
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
if (!checkGrant(grants.update, existing)) {
|
|
45
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
46
46
|
throw new PermissionActionError({
|
|
47
47
|
documentId: releaseDocumentId,
|
|
48
48
|
transactionId,
|
|
@@ -6,7 +6,7 @@ export function handleReleaseEdit(
|
|
|
6
6
|
action: EditReleaseAction,
|
|
7
7
|
ctx: ActionHandlerContext,
|
|
8
8
|
): ActionHandlerResult {
|
|
9
|
-
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations} = ctx
|
|
9
|
+
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations, identity} = ctx
|
|
10
10
|
const {base, working} = ctx
|
|
11
11
|
|
|
12
12
|
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
@@ -19,6 +19,7 @@ export function handleReleaseEdit(
|
|
|
19
19
|
transactionId,
|
|
20
20
|
timestamp,
|
|
21
21
|
grants,
|
|
22
|
+
identity,
|
|
22
23
|
notFoundMessage: `Cannot edit release "${action.releaseId}" because it does not exist.`,
|
|
23
24
|
permissionMessage: `You do not have permission to edit release "${action.releaseId}".`,
|
|
24
25
|
})
|
|
@@ -12,7 +12,7 @@ export function handleReleasePublish(
|
|
|
12
12
|
action: PublishReleaseAction,
|
|
13
13
|
ctx: ActionHandlerContext,
|
|
14
14
|
): ActionHandlerResult {
|
|
15
|
-
const {base, working, grants, outgoingActions, transactionId} = ctx
|
|
15
|
+
const {base, working, grants, outgoingActions, transactionId, identity} = ctx
|
|
16
16
|
|
|
17
17
|
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
18
18
|
const existing = working[releaseDocumentId] ?? base[releaseDocumentId]
|
|
@@ -24,7 +24,7 @@ export function handleReleasePublish(
|
|
|
24
24
|
})
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
if (!checkGrant(grants.update, existing)) {
|
|
27
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
28
28
|
throw new PermissionActionError({
|
|
29
29
|
documentId: releaseDocumentId,
|
|
30
30
|
transactionId,
|
|
@@ -12,7 +12,7 @@ export function handleReleaseSchedule(
|
|
|
12
12
|
action: ScheduleReleaseAction,
|
|
13
13
|
ctx: ActionHandlerContext,
|
|
14
14
|
): ActionHandlerResult {
|
|
15
|
-
const {base, working, grants, outgoingActions, transactionId} = ctx
|
|
15
|
+
const {base, working, grants, outgoingActions, transactionId, identity} = ctx
|
|
16
16
|
|
|
17
17
|
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
18
18
|
|
|
@@ -33,7 +33,7 @@ export function handleReleaseSchedule(
|
|
|
33
33
|
})
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
if (!checkGrant(grants.update, existing)) {
|
|
36
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
37
37
|
throw new PermissionActionError({
|
|
38
38
|
documentId: releaseDocumentId,
|
|
39
39
|
transactionId,
|
|
@@ -58,7 +58,7 @@ export function handleReleaseUnschedule(
|
|
|
58
58
|
action: UnscheduleReleaseAction,
|
|
59
59
|
ctx: ActionHandlerContext,
|
|
60
60
|
): ActionHandlerResult {
|
|
61
|
-
const {base, working, grants, outgoingActions, transactionId} = ctx
|
|
61
|
+
const {base, working, grants, outgoingActions, transactionId, identity} = ctx
|
|
62
62
|
|
|
63
63
|
const releaseDocumentId = getReleaseDocumentId(action.releaseId)
|
|
64
64
|
const existing = working[releaseDocumentId] ?? base[releaseDocumentId]
|
|
@@ -70,7 +70,7 @@ export function handleReleaseUnschedule(
|
|
|
70
70
|
})
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
if (!checkGrant(grants.update, existing)) {
|
|
73
|
+
if (!checkGrant(grants.update, existing, identity)) {
|
|
74
74
|
throw new PermissionActionError({
|
|
75
75
|
documentId: releaseDocumentId,
|
|
76
76
|
transactionId,
|
|
@@ -12,6 +12,12 @@ export interface ActionHandlerContext {
|
|
|
12
12
|
transactionId: string
|
|
13
13
|
timestamp: string
|
|
14
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
|
|
15
21
|
outgoingActions: HttpAction[]
|
|
16
22
|
outgoingMutations: Mutation[]
|
|
17
23
|
}
|
|
@@ -21,8 +27,12 @@ export interface ActionHandlerResult {
|
|
|
21
27
|
working: DocumentSet
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
export function checkGrant(
|
|
25
|
-
|
|
30
|
+
export function checkGrant(
|
|
31
|
+
grantExpr: ExprNode,
|
|
32
|
+
document: SanityDocument,
|
|
33
|
+
identity: string | undefined,
|
|
34
|
+
): boolean {
|
|
35
|
+
const value = evaluateSync(grantExpr, {params: {document}, identity})
|
|
26
36
|
return value.type === 'boolean' && value.data
|
|
27
37
|
}
|
|
28
38
|
|
|
@@ -55,6 +65,7 @@ interface ApplySingleDocPatchOptions {
|
|
|
55
65
|
transactionId: string
|
|
56
66
|
timestamp: string
|
|
57
67
|
grants: Record<Grant, ExprNode>
|
|
68
|
+
identity: string | undefined
|
|
58
69
|
/**
|
|
59
70
|
* Error message thrown when the target document does not exist in either
|
|
60
71
|
* the base or working set.
|
|
@@ -98,6 +109,7 @@ export function applySingleDocPatch({
|
|
|
98
109
|
transactionId,
|
|
99
110
|
timestamp,
|
|
100
111
|
grants,
|
|
112
|
+
identity,
|
|
101
113
|
notFoundMessage = 'Cannot edit document because it does not exist.',
|
|
102
114
|
permissionMessage = `You do not have permission to edit document "${documentId}".`,
|
|
103
115
|
}: ApplySingleDocPatchOptions): ApplySingleDocPatchResult {
|
|
@@ -120,7 +132,7 @@ export function applySingleDocPatch({
|
|
|
120
132
|
const diffedPatches = diffValue(baseBefore, baseAfter) as PatchOperations[]
|
|
121
133
|
|
|
122
134
|
const workingBefore = working[documentId] as SanityDocument
|
|
123
|
-
if (!checkGrant(grants.update, workingBefore)) {
|
|
135
|
+
if (!checkGrant(grants.update, workingBefore, identity)) {
|
|
124
136
|
throw new PermissionActionError({documentId, transactionId, message: permissionMessage})
|
|
125
137
|
}
|
|
126
138
|
|
|
@@ -16,7 +16,7 @@ export function handleUnpublish(
|
|
|
16
16
|
action: UnpublishDocumentAction,
|
|
17
17
|
ctx: ActionHandlerContext,
|
|
18
18
|
): ActionHandlerResult {
|
|
19
|
-
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations} = ctx
|
|
19
|
+
const {transactionId, timestamp, grants, identity, outgoingActions, outgoingMutations} = ctx
|
|
20
20
|
let {base, working} = ctx
|
|
21
21
|
|
|
22
22
|
const documentId = getId(action.documentId)
|
|
@@ -48,7 +48,7 @@ export function handleUnpublish(
|
|
|
48
48
|
{createIfNotExists: newDraftFromPublished},
|
|
49
49
|
]
|
|
50
50
|
|
|
51
|
-
if (!checkGrant(grants.update, sourceDoc)) {
|
|
51
|
+
if (!checkGrant(grants.update, sourceDoc, identity)) {
|
|
52
52
|
throw new PermissionActionError({
|
|
53
53
|
documentId,
|
|
54
54
|
transactionId,
|
|
@@ -56,7 +56,7 @@ export function handleUnpublish(
|
|
|
56
56
|
})
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
if (!working[draftId] && !checkGrant(grants.create, newDraftFromPublished)) {
|
|
59
|
+
if (!working[draftId] && !checkGrant(grants.create, newDraftFromPublished, identity)) {
|
|
60
60
|
throw new PermissionActionError({
|
|
61
61
|
documentId,
|
|
62
62
|
transactionId,
|
package/src/document/reducers.ts
CHANGED
|
@@ -19,7 +19,7 @@ const EMPTY_REVISIONS: NonNullable<Required<DocumentState['unverifiedRevisions']
|
|
|
19
19
|
|
|
20
20
|
export type SyncTransactionState = Pick<
|
|
21
21
|
DocumentStoreState,
|
|
22
|
-
'queued' | 'applied' | 'documentStates' | 'outgoing' | 'grants'
|
|
22
|
+
'queued' | 'applied' | 'documentStates' | 'outgoing' | 'grants' | 'identity'
|
|
23
23
|
>
|
|
24
24
|
|
|
25
25
|
type DocumentHandleLike = Pick<DocumentHandle, 'perspective'> & {
|
|
@@ -232,6 +232,7 @@ export function applyFirstQueuedTransaction(prev: SyncTransactionState): SyncTra
|
|
|
232
232
|
base: working,
|
|
233
233
|
timestamp,
|
|
234
234
|
grants: prev.grants,
|
|
235
|
+
identity: prev.identity,
|
|
235
236
|
})
|
|
236
237
|
const applied: AppliedTransaction = {
|
|
237
238
|
...queued,
|
|
@@ -399,7 +400,7 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
399
400
|
|
|
400
401
|
for (const t of prev.applied) {
|
|
401
402
|
try {
|
|
402
|
-
const next = processActions({...t, working, grants: prev.grants})
|
|
403
|
+
const next = processActions({...t, working, grants: prev.grants, identity: prev.identity})
|
|
403
404
|
working = next.working
|
|
404
405
|
nextApplied.push({...t, ...next})
|
|
405
406
|
} catch (error) {
|
|
@@ -506,7 +507,7 @@ export function applyRemoteDocument(
|
|
|
506
507
|
// transaction again through the listener and this same flow will run then
|
|
507
508
|
for (const curr of prev.applied) {
|
|
508
509
|
try {
|
|
509
|
-
const next = processActions({...curr, working, grants: prev.grants})
|
|
510
|
+
const next = processActions({...curr, working, grants: prev.grants, identity: prev.identity})
|
|
510
511
|
working = next.working
|
|
511
512
|
// next includes an updated `previous` set and `working` set and updates
|
|
512
513
|
// the `outgoingAction` and `outgoingMutations`. the `base` set from the
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {type EditDocumentAction} from './actions'
|
|
4
|
+
import {getEffectiveDocumentModel, normalizeActionsForResource} from './resourceRules'
|
|
5
|
+
|
|
6
|
+
const datasetResource = {projectId: 'p', dataset: 'd'}
|
|
7
|
+
const canvasResource = {canvasId: 'canvas-1'}
|
|
8
|
+
const mediaLibraryResource = {mediaLibraryId: 'ml-1'}
|
|
9
|
+
|
|
10
|
+
function editAction(
|
|
11
|
+
overrides: Partial<EditDocumentAction> & Pick<EditDocumentAction, 'documentId' | 'documentType'>,
|
|
12
|
+
): EditDocumentAction {
|
|
13
|
+
return {
|
|
14
|
+
type: 'document.edit',
|
|
15
|
+
patches: [{set: {foo: 'bar'}}],
|
|
16
|
+
...overrides,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('getEffectiveDocumentModel', () => {
|
|
21
|
+
it('returns passthrough for no resource', () => {
|
|
22
|
+
expect(getEffectiveDocumentModel(undefined, 'anything')).toEqual({
|
|
23
|
+
liveEdit: undefined,
|
|
24
|
+
supportsReleases: true,
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns passthrough for dataset resources', () => {
|
|
29
|
+
expect(getEffectiveDocumentModel(datasetResource, 'author')).toEqual({
|
|
30
|
+
liveEdit: undefined,
|
|
31
|
+
supportsReleases: true,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('forces liveEdit and disallows releases for canvas resources', () => {
|
|
36
|
+
expect(getEffectiveDocumentModel(canvasResource, 'page')).toEqual({
|
|
37
|
+
liveEdit: true,
|
|
38
|
+
supportsReleases: false,
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('forces liveEdit for non-asset media library types', () => {
|
|
43
|
+
expect(getEffectiveDocumentModel(mediaLibraryResource, 'sanity.imageAsset')).toEqual({
|
|
44
|
+
liveEdit: true,
|
|
45
|
+
supportsReleases: false,
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('keeps draft/published model for sanity.asset in media library', () => {
|
|
50
|
+
expect(getEffectiveDocumentModel(mediaLibraryResource, 'sanity.asset')).toEqual({
|
|
51
|
+
liveEdit: false,
|
|
52
|
+
supportsReleases: false,
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('normalizeActionsForResource', () => {
|
|
58
|
+
let warnSpy: ReturnType<typeof vi.spyOn>
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
warnSpy.mockRestore()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('leaves dataset edits with a release perspective alone', () => {
|
|
69
|
+
const action = editAction({
|
|
70
|
+
documentId: 'versions.relA.doc1',
|
|
71
|
+
documentType: 'author',
|
|
72
|
+
perspective: {releaseName: 'relA'},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const result = normalizeActionsForResource([action], datasetResource)
|
|
76
|
+
|
|
77
|
+
expect(result[0]).toBe(action)
|
|
78
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('strips release perspective and forces liveEdit for canvas, warning once', () => {
|
|
82
|
+
const action = editAction({
|
|
83
|
+
documentId: 'versions.relA.doc1',
|
|
84
|
+
documentType: 'page',
|
|
85
|
+
perspective: {releaseName: 'relA'},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const [result] = normalizeActionsForResource([action], canvasResource) as [EditDocumentAction]
|
|
89
|
+
|
|
90
|
+
expect(result.liveEdit).toBe(true)
|
|
91
|
+
expect(result.perspective).toBeUndefined()
|
|
92
|
+
expect(result.documentId).toBe('doc1')
|
|
93
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
94
|
+
expect(warnSpy.mock.calls[0][0]).toContain('Canvas')
|
|
95
|
+
expect(warnSpy.mock.calls[0][0]).toContain('page (doc1)')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('strips release perspective and forces liveEdit for non-asset media library types', () => {
|
|
99
|
+
const action = editAction({
|
|
100
|
+
documentId: 'versions.relA.doc1',
|
|
101
|
+
documentType: 'sanity.imageAsset',
|
|
102
|
+
perspective: {releaseName: 'relA'},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const [result] = normalizeActionsForResource([action], mediaLibraryResource) as [
|
|
106
|
+
EditDocumentAction,
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
expect(result.liveEdit).toBe(true)
|
|
110
|
+
expect(result.perspective).toBeUndefined()
|
|
111
|
+
expect(result.documentId).toBe('doc1')
|
|
112
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
113
|
+
expect(warnSpy.mock.calls[0][0]).toContain('Media Library')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('strips release perspective but keeps draft/publish model for sanity.asset', () => {
|
|
117
|
+
const action = editAction({
|
|
118
|
+
documentId: 'versions.relA.doc1',
|
|
119
|
+
documentType: 'sanity.asset',
|
|
120
|
+
perspective: {releaseName: 'relA'},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const [result] = normalizeActionsForResource([action], mediaLibraryResource) as [
|
|
124
|
+
EditDocumentAction,
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
expect(result.liveEdit).toBeUndefined()
|
|
128
|
+
expect(result.perspective).toBeUndefined()
|
|
129
|
+
expect(result.documentId).toBe('doc1')
|
|
130
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('silently forces liveEdit for canvas with no release perspective', () => {
|
|
134
|
+
const action = editAction({
|
|
135
|
+
documentId: 'drafts.doc1',
|
|
136
|
+
documentType: 'page',
|
|
137
|
+
perspective: 'drafts',
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const [result] = normalizeActionsForResource([action], canvasResource) as [EditDocumentAction]
|
|
141
|
+
|
|
142
|
+
expect(result.liveEdit).toBe(true)
|
|
143
|
+
expect(result.documentId).toBe('doc1')
|
|
144
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('emits a single warning for multiple stripped actions in one call', () => {
|
|
148
|
+
const a = editAction({
|
|
149
|
+
documentId: 'versions.relA.doc1',
|
|
150
|
+
documentType: 'page',
|
|
151
|
+
perspective: {releaseName: 'relA'},
|
|
152
|
+
})
|
|
153
|
+
const b = editAction({
|
|
154
|
+
documentId: 'versions.relA.doc2',
|
|
155
|
+
documentType: 'page',
|
|
156
|
+
perspective: {releaseName: 'relA'},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
normalizeActionsForResource([a, b], canvasResource)
|
|
160
|
+
|
|
161
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
162
|
+
expect(warnSpy.mock.calls[0][0]).toContain('doc1')
|
|
163
|
+
expect(warnSpy.mock.calls[0][0]).toContain('doc2')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('passes non-edit actions through unchanged', () => {
|
|
167
|
+
const action = {
|
|
168
|
+
type: 'document.publish' as const,
|
|
169
|
+
documentId: 'doc1',
|
|
170
|
+
documentType: 'page',
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = normalizeActionsForResource([action], canvasResource)
|
|
174
|
+
|
|
175
|
+
expect(result[0]).toBe(action)
|
|
176
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
177
|
+
})
|
|
178
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {DocumentId, getPublishedId} from '@sanity/id-utils'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type DocumentResource,
|
|
5
|
+
isCanvasResource,
|
|
6
|
+
isMediaLibraryResource,
|
|
7
|
+
} from '../config/sanityConfig'
|
|
8
|
+
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
9
|
+
import {type Action, type DocumentAction} from './actions'
|
|
10
|
+
import {getEffectiveDocumentId} from './util'
|
|
11
|
+
|
|
12
|
+
export interface EffectiveDocModel {
|
|
13
|
+
/**
|
|
14
|
+
* If this is `undefined`, the resource has no opinion and the handle's `liveEdit`
|
|
15
|
+
* flag should be respected.
|
|
16
|
+
*/
|
|
17
|
+
liveEdit: boolean | undefined
|
|
18
|
+
/**
|
|
19
|
+
* When `false`, the resource does not support release perspectives. Callers
|
|
20
|
+
* should drop any release perspective and fall back to the standard path.
|
|
21
|
+
*/
|
|
22
|
+
supportsReleases: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MEDIA_LIBRARY_DRAFTED_TYPES = new Set(['sanity.asset'])
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Different resources have different "default" editing models.
|
|
29
|
+
*
|
|
30
|
+
* Canvas uses a liveEdit model.
|
|
31
|
+
* Medial Library is mostly liveEdit except for `sanity.asset`, which retains the
|
|
32
|
+
* draft/published model. Neither resource supports release perspectives.
|
|
33
|
+
*/
|
|
34
|
+
export function getEffectiveDocumentModel(
|
|
35
|
+
resource: DocumentResource | undefined,
|
|
36
|
+
documentType: string | undefined,
|
|
37
|
+
): EffectiveDocModel {
|
|
38
|
+
if (!resource) {
|
|
39
|
+
return {liveEdit: undefined, supportsReleases: true}
|
|
40
|
+
}
|
|
41
|
+
if (isCanvasResource(resource)) {
|
|
42
|
+
return {liveEdit: true, supportsReleases: false}
|
|
43
|
+
}
|
|
44
|
+
if (isMediaLibraryResource(resource)) {
|
|
45
|
+
const isDrafted = documentType ? MEDIA_LIBRARY_DRAFTED_TYPES.has(documentType) : false
|
|
46
|
+
return {liveEdit: !isDrafted, supportsReleases: false}
|
|
47
|
+
}
|
|
48
|
+
return {liveEdit: undefined, supportsReleases: true}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function describeResource(resource: DocumentResource | undefined): string {
|
|
52
|
+
if (resource && isCanvasResource(resource)) return 'Canvas'
|
|
53
|
+
if (resource && isMediaLibraryResource(resource)) return 'Media Library'
|
|
54
|
+
return 'this resource'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Rewrites edit actions to match the editing model of the bound resource.
|
|
59
|
+
*
|
|
60
|
+
* Canvas and Media Library resources do not support release perspectives, and
|
|
61
|
+
* Canvas (plus most Media Library types) does not have a draft/published
|
|
62
|
+
* model. When an edit action arrives with a release perspective for a resource
|
|
63
|
+
* that doesn't support it, the perspective is stripped and a single warning
|
|
64
|
+
* is logged. When the resource forces liveEdit, the action's `liveEdit` flag
|
|
65
|
+
* is set so the dispatcher takes the liveEdit branch.
|
|
66
|
+
*
|
|
67
|
+
* Only `document.edit` actions are normalized today — other action types
|
|
68
|
+
* (publish, unpublish, release actions, etc.) pass through unchanged.
|
|
69
|
+
*
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
export function normalizeActionsForResource(
|
|
73
|
+
actions: Action[],
|
|
74
|
+
resource: DocumentResource | undefined,
|
|
75
|
+
): Action[] {
|
|
76
|
+
// collect actions that may have changed in unexpected ways
|
|
77
|
+
const stripped: Array<{documentType: string; documentId: string}> = []
|
|
78
|
+
|
|
79
|
+
const normalized = actions.map((action) => {
|
|
80
|
+
if (action.type !== 'document.edit') return action
|
|
81
|
+
|
|
82
|
+
const {liveEdit: forcedLiveEdit, supportsReleases} = getEffectiveDocumentModel(
|
|
83
|
+
resource,
|
|
84
|
+
action.documentType,
|
|
85
|
+
)
|
|
86
|
+
const shouldRemovePerspective = isReleasePerspective(action.perspective) && !supportsReleases
|
|
87
|
+
const shouldForceLiveEdit = forcedLiveEdit === true && !action.liveEdit
|
|
88
|
+
|
|
89
|
+
if (!shouldRemovePerspective && !shouldForceLiveEdit) return action
|
|
90
|
+
|
|
91
|
+
const corrected: DocumentAction = {...action}
|
|
92
|
+
if (shouldForceLiveEdit) corrected.liveEdit = true
|
|
93
|
+
if (shouldRemovePerspective) corrected.perspective = undefined
|
|
94
|
+
|
|
95
|
+
// ensure we're using the right document ID for the corrected model
|
|
96
|
+
corrected.documentId = getEffectiveDocumentId({
|
|
97
|
+
...corrected,
|
|
98
|
+
documentId: getPublishedId(DocumentId(corrected.documentId)),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if (shouldRemovePerspective) {
|
|
102
|
+
stripped.push({documentType: action.documentType, documentId: corrected.documentId})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return corrected
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (stripped.length > 0) {
|
|
109
|
+
const docs = stripped.map((e) => `${e.documentType} (${e.documentId})`).join(', ')
|
|
110
|
+
// eslint-disable-next-line no-console
|
|
111
|
+
console.warn(
|
|
112
|
+
`[sanity-sdk] ${describeResource(resource)} does not support release perspectives — falling back to the standard editing path for: ${docs}`,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return normalized
|
|
117
|
+
}
|