@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.
Files changed (57) hide show
  1. package/dist/_chunks-dts/utils.d.ts +171 -19
  2. package/dist/_chunks-es/_internal.js +41 -26
  3. package/dist/_chunks-es/_internal.js.map +1 -1
  4. package/dist/_chunks-es/createGroqSearchFilter.js +25 -9
  5. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  6. package/dist/_chunks-es/telemetryManager.js +25 -19
  7. package/dist/_chunks-es/telemetryManager.js.map +1 -1
  8. package/dist/_chunks-es/version.js +1 -1
  9. package/dist/_exports/_internal.d.ts +27 -11
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +723 -418
  12. package/dist/index.js.map +1 -1
  13. package/package.json +16 -16
  14. package/src/_exports/index.ts +23 -2
  15. package/src/auth/refreshStampedToken.test.ts +2 -2
  16. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +116 -0
  17. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +27 -9
  18. package/src/config/sanityConfig.ts +12 -0
  19. package/src/document/actions.test.ts +112 -1
  20. package/src/document/actions.ts +148 -1
  21. package/src/document/applyDocumentActions.ts +4 -3
  22. package/src/document/documentStore.ts +7 -6
  23. package/src/document/events.test.ts +57 -2
  24. package/src/document/events.ts +43 -24
  25. package/src/document/permissions.ts +1 -1
  26. package/src/document/processActions/create.ts +135 -0
  27. package/src/document/processActions/delete.ts +100 -0
  28. package/src/document/processActions/discard.ts +63 -0
  29. package/src/document/processActions/edit.ts +141 -0
  30. package/src/document/processActions/processActions.ts +209 -0
  31. package/src/document/processActions/publish.ts +120 -0
  32. package/src/document/processActions/releaseArchive.ts +77 -0
  33. package/src/document/processActions/releaseCreate.ts +59 -0
  34. package/src/document/processActions/releaseDelete.ts +65 -0
  35. package/src/document/processActions/releaseEdit.ts +36 -0
  36. package/src/document/processActions/releasePublish.ts +45 -0
  37. package/src/document/processActions/releaseSchedule.ts +87 -0
  38. package/src/document/processActions/releaseUtil.ts +31 -0
  39. package/src/document/processActions/shared.ts +139 -0
  40. package/src/document/processActions/unpublish.ts +85 -0
  41. package/src/document/processActions.test.ts +424 -2
  42. package/src/document/reducers.ts +41 -6
  43. package/src/releases/getPerspectiveState.test.ts +1 -1
  44. package/src/releases/releasesStore.test.ts +50 -1
  45. package/src/releases/releasesStore.ts +41 -18
  46. package/src/releases/utils/sortReleases.test.ts +2 -2
  47. package/src/releases/utils/sortReleases.ts +1 -1
  48. package/src/telemetry/environment.test.ts +119 -0
  49. package/src/telemetry/environment.ts +92 -0
  50. package/src/telemetry/{__telemetry__/sdk.telemetry.ts → events.ts} +9 -9
  51. package/src/telemetry/initTelemetry.test.ts +240 -16
  52. package/src/telemetry/initTelemetry.ts +39 -16
  53. package/src/telemetry/telemetryManager.test.ts +129 -65
  54. package/src/telemetry/telemetryManager.ts +41 -29
  55. package/src/document/processActions.ts +0 -735
  56. package/src/telemetry/devMode.test.ts +0 -60
  57. package/src/telemetry/devMode.ts +0 -41
@@ -0,0 +1,209 @@
1
+ import {type Mutation} from '@sanity/types'
2
+ import {type ExprNode} from 'groq-js'
3
+
4
+ import {type Action, type DocumentAction} from '../actions'
5
+ import {type Grant} from '../permissions'
6
+ import {type DocumentSet} from '../processMutations'
7
+ import {type HttpAction} from '../reducers'
8
+ import {handleCreate} from './create'
9
+ import {handleDelete} from './delete'
10
+ import {handleDiscard} from './discard'
11
+ import {handleEdit} from './edit'
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'
20
+ import {
21
+ ActionError,
22
+ type ActionHandlerContext,
23
+ type ActionHandlerResult,
24
+ PermissionActionError,
25
+ } from './shared'
26
+ import {handleUnpublish} from './unpublish'
27
+
28
+ export {ActionError, PermissionActionError}
29
+
30
+ interface ProcessActionsOptions {
31
+ /**
32
+ * The ID of this transaction. This will become the resulting `_rev` for all
33
+ * documents affected by changes derived from the current set of actions.
34
+ */
35
+ transactionId: string
36
+
37
+ /**
38
+ * The actions to apply to the given documents
39
+ */
40
+ actions: Action[]
41
+
42
+ /**
43
+ * The set of documents these actions were intended to be applied to. These
44
+ * set of documents should be captured right before a queued action is
45
+ * applied.
46
+ */
47
+ base: DocumentSet
48
+
49
+ /**
50
+ * The current "working" set of documents. A patch will be created by applying
51
+ * the actions to the base. This patch will then be applied to the working
52
+ * set for conflict resolution. Initially, this value should match the base
53
+ * set.
54
+ */
55
+ working: DocumentSet
56
+
57
+ /**
58
+ * The timestamp to use for `_updateAt` and other similar timestamps for this
59
+ * transaction
60
+ */
61
+ timestamp: string
62
+
63
+ /**
64
+ * the lookup with pre-parsed GROQ expressions
65
+ */
66
+ grants: Record<Grant, ExprNode>
67
+
68
+ // // TODO: implement initial values from the schema?
69
+ // initialValues?: {[TDocumentType in string]?: {_type: string}}
70
+ }
71
+
72
+ interface ProcessActionsResult {
73
+ /**
74
+ * The resulting document set after the actions have been applied. This is
75
+ * derived from the working documents.
76
+ */
77
+ working: DocumentSet
78
+ /**
79
+ * The document set before the actions have been applied. This is simply the
80
+ * input of the `working` document set.
81
+ */
82
+ previous: DocumentSet
83
+ /**
84
+ * The outgoing action that were collected when applying the actions. These
85
+ * are sent to the Actions HTTP API
86
+ */
87
+ outgoingActions: HttpAction[]
88
+ /**
89
+ * The outgoing mutations that were collected when applying the actions. These
90
+ * are here for debugging purposes.
91
+ */
92
+ outgoingMutations: Mutation[]
93
+ /**
94
+ * The previous revisions of the given documents before the actions were applied.
95
+ */
96
+ previousRevs: {[TDocumentId in string]?: string}
97
+ }
98
+
99
+ /**
100
+ * Applies the given set of actions to the working set of documents and converts
101
+ * high-level actions into lower-level outgoing mutations/actions that respect
102
+ * the current state of the working documents.
103
+ *
104
+ * Supports a "base" and "working" set of documents to allow actions to be
105
+ * applied on top of a different working set of documents in a 3-way merge
106
+ *
107
+ * Actions are applied to the base set of documents first. The difference
108
+ * between the base before and after is used to create a patch. This patch is
109
+ * then applied to the working set of documents and is set as the outgoing patch
110
+ * sent to the server.
111
+ */
112
+ export function processActions({
113
+ actions,
114
+ transactionId,
115
+ working: initialWorking,
116
+ base: initialBase,
117
+ timestamp,
118
+ grants,
119
+ }: ProcessActionsOptions): ProcessActionsResult {
120
+ let base: DocumentSet = {...initialBase}
121
+ let working: DocumentSet = {...initialWorking}
122
+
123
+ const outgoingActions: HttpAction[] = []
124
+ const outgoingMutations: Mutation[] = []
125
+
126
+ // liveEdit document actions go to the mutations API, since the actions API
127
+ // requires a draft+published pair. Mixing them with anything else in the same
128
+ // transaction would silently lose atomicity for the non-liveEdit operations,
129
+ // so require users to split the transaction.
130
+ // (Note that the reducers already does this for us -- you'd have to try hard to mix them.)
131
+ const liveEditAction = actions.find((action) => !isReleaseAction(action) && action.liveEdit) as
132
+ | DocumentAction
133
+ | undefined
134
+ const otherAction = actions.find((action) => isReleaseAction(action) || !action.liveEdit)
135
+ if (liveEditAction && otherAction) {
136
+ throw new ActionError({
137
+ documentId: liveEditAction.documentId!,
138
+ transactionId,
139
+ message:
140
+ 'Cannot combine liveEdit document actions with other actions in the same transaction. Submit them as separate transactions.',
141
+ })
142
+ }
143
+
144
+ for (const action of actions) {
145
+ const result = dispatch(action, {
146
+ base,
147
+ working,
148
+ transactionId,
149
+ timestamp,
150
+ grants,
151
+ outgoingActions,
152
+ outgoingMutations,
153
+ })
154
+ base = result.base
155
+ working = result.working
156
+ }
157
+
158
+ const previousRevs = Object.fromEntries(
159
+ Object.entries(initialWorking).map(([id, doc]) => [id, doc?._rev]),
160
+ )
161
+
162
+ return {
163
+ working,
164
+ outgoingActions,
165
+ outgoingMutations,
166
+ previous: initialWorking,
167
+ previousRevs,
168
+ }
169
+ }
170
+
171
+ function dispatch(action: Action, ctx: ActionHandlerContext): ActionHandlerResult {
172
+ switch (action.type) {
173
+ case 'document.create':
174
+ return handleCreate(action, ctx)
175
+ case 'document.delete':
176
+ return handleDelete(action, ctx)
177
+ case 'document.discard':
178
+ return handleDiscard(action, ctx)
179
+ case 'document.edit':
180
+ return handleEdit(action, ctx)
181
+ case 'document.publish':
182
+ return handlePublish(action, ctx)
183
+ case 'document.unpublish':
184
+ return handleUnpublish(action, ctx)
185
+ case 'release.create':
186
+ return handleReleaseCreate(action, ctx)
187
+ case 'release.edit':
188
+ return handleReleaseEdit(action, ctx)
189
+ case 'release.publish':
190
+ return handleReleasePublish(action, ctx)
191
+ case 'release.schedule':
192
+ return handleReleaseSchedule(action, ctx)
193
+ case 'release.unschedule':
194
+ return handleReleaseUnschedule(action, ctx)
195
+ case 'release.archive':
196
+ return handleReleaseArchive(action, ctx)
197
+ case 'release.unarchive':
198
+ return handleReleaseUnarchive(action, ctx)
199
+ case 'release.delete':
200
+ return handleReleaseDelete(action, ctx)
201
+ default:
202
+ throw new Error(
203
+ `Unknown action type: "${
204
+ // @ts-expect-error invalid input
205
+ action.type
206
+ }". Please contact support if this issue persists.`,
207
+ )
208
+ }
209
+ }
@@ -0,0 +1,120 @@
1
+ import {DocumentId, getDraftId, getPublishedId} from '@sanity/id-utils'
2
+ import {type Mutation, type Reference, type SanityDocument} from '@sanity/types'
3
+
4
+ import {isReleasePerspective} from '../../releases/utils/isReleasePerspective'
5
+ import {isDeepEqual} from '../../utils/object'
6
+ import {type PublishDocumentAction} from '../actions'
7
+ import {getId, processMutations} from '../processMutations'
8
+ import {
9
+ ActionError,
10
+ type ActionHandlerContext,
11
+ type ActionHandlerResult,
12
+ checkGrant,
13
+ PermissionActionError,
14
+ } from './shared'
15
+
16
+ export function handlePublish(
17
+ action: PublishDocumentAction,
18
+ ctx: ActionHandlerContext,
19
+ ): ActionHandlerResult {
20
+ const {transactionId, timestamp, grants, outgoingActions, outgoingMutations} = ctx
21
+ let {base, working} = ctx
22
+
23
+ const documentId = getId(action.documentId)
24
+
25
+ if (action.liveEdit || isReleasePerspective(action.perspective)) {
26
+ throw new ActionError({
27
+ documentId,
28
+ transactionId,
29
+ message: `Cannot publish this document. Publishing is not supported for liveEdit or version (release) documents.`,
30
+ })
31
+ }
32
+
33
+ // Standard draft/published logic
34
+ const draftId = getDraftId(DocumentId(documentId))
35
+ const publishedId = getPublishedId(DocumentId(documentId))
36
+
37
+ const workingDraft = working[draftId]
38
+ const baseDraft = base[draftId]
39
+ if (!workingDraft || !baseDraft) {
40
+ throw new ActionError({
41
+ documentId,
42
+ transactionId,
43
+ message: `Cannot publish because no draft version was found for document "${documentId}".`,
44
+ })
45
+ }
46
+
47
+ // Before proceeding, verify that the working draft is identical to the base draft.
48
+ // TODO: is it enough just to check for the _rev or nah?
49
+ if (!isDeepEqual(workingDraft, baseDraft)) {
50
+ throw new ActionError({
51
+ documentId,
52
+ transactionId,
53
+ message: `Publish aborted: The document has changed elsewhere. Please try again.`,
54
+ })
55
+ }
56
+
57
+ const newPublishedFromDraft = {...strengthenOnPublish(workingDraft), _id: publishedId}
58
+
59
+ const mutations: Mutation[] = [{delete: {id: draftId}}, {createOrReplace: newPublishedFromDraft}]
60
+
61
+ if (working[draftId] && !checkGrant(grants.update, working[draftId])) {
62
+ throw new PermissionActionError({
63
+ documentId,
64
+ transactionId,
65
+ message: `Publish failed: You do not have permission to update the draft for "${documentId}".`,
66
+ })
67
+ }
68
+
69
+ if (working[publishedId] && !checkGrant(grants.update, newPublishedFromDraft)) {
70
+ throw new PermissionActionError({
71
+ documentId,
72
+ transactionId,
73
+ message: `Publish failed: You do not have permission to update the published version of "${documentId}".`,
74
+ })
75
+ } else if (!working[publishedId] && !checkGrant(grants.create, newPublishedFromDraft)) {
76
+ throw new PermissionActionError({
77
+ documentId,
78
+ transactionId,
79
+ message: `Publish failed: You do not have permission to publish a new version of "${documentId}".`,
80
+ })
81
+ }
82
+
83
+ base = processMutations({documents: base, transactionId, mutations, timestamp})
84
+ working = processMutations({documents: working, transactionId, mutations, timestamp})
85
+
86
+ outgoingMutations.push(...mutations)
87
+ outgoingActions.push({
88
+ actionType: 'sanity.action.document.publish',
89
+ draftId,
90
+ publishedId,
91
+ })
92
+ return {base, working}
93
+ }
94
+
95
+ function strengthenOnPublish(draft: SanityDocument): SanityDocument {
96
+ const isStrengthenReference = (
97
+ value: object,
98
+ ): value is Reference & Required<Pick<Reference, '_strengthenOnPublish'>> =>
99
+ '_strengthenOnPublish' in value
100
+
101
+ function strengthen(value: unknown): unknown {
102
+ if (typeof value !== 'object' || !value) return value
103
+
104
+ if (isStrengthenReference(value)) {
105
+ const {_strengthenOnPublish, _weak, ...rest} = value
106
+ return {
107
+ ...rest,
108
+ ...(_strengthenOnPublish.weak && {_weak: true}),
109
+ }
110
+ }
111
+
112
+ if (Array.isArray(value)) {
113
+ return value.map(strengthen)
114
+ }
115
+
116
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, strengthen(v)]))
117
+ }
118
+
119
+ return strengthen(draft) as SanityDocument
120
+ }
@@ -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} = 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)) {
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} = 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)) {
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} = 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)) {
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} = 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)) {
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,36 @@
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} = 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
+ notFoundMessage: `Cannot edit release "${action.releaseId}" because it does not exist.`,
23
+ permissionMessage: `You do not have permission to edit release "${action.releaseId}".`,
24
+ })
25
+
26
+ outgoingMutations.push(...result.workingMutations)
27
+ outgoingActions.push(
28
+ ...result.diffedPatches.map((patch) => ({
29
+ actionType: 'sanity.action.release.edit' as const,
30
+ releaseId: action.releaseId,
31
+ patch,
32
+ })),
33
+ )
34
+
35
+ return {base: result.base, working: result.working}
36
+ }
@@ -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} = 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)) {
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} = 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)) {
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} = 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)) {
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
+ }