@sanity/sdk 2.11.1 → 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 (47) 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 +15 -4
  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 +355 -75
  12. package/dist/index.js.map +1 -1
  13. package/package.json +8 -8
  14. package/src/_exports/index.ts +23 -2
  15. package/src/config/sanityConfig.ts +12 -0
  16. package/src/document/actions.test.ts +112 -1
  17. package/src/document/actions.ts +148 -1
  18. package/src/document/applyDocumentActions.ts +4 -3
  19. package/src/document/documentStore.ts +6 -5
  20. package/src/document/events.test.ts +57 -2
  21. package/src/document/events.ts +43 -24
  22. package/src/document/processActions/edit.ts +9 -44
  23. package/src/document/processActions/processActions.ts +44 -3
  24. package/src/document/processActions/releaseArchive.ts +77 -0
  25. package/src/document/processActions/releaseCreate.ts +59 -0
  26. package/src/document/processActions/releaseDelete.ts +65 -0
  27. package/src/document/processActions/releaseEdit.ts +36 -0
  28. package/src/document/processActions/releasePublish.ts +45 -0
  29. package/src/document/processActions/releaseSchedule.ts +87 -0
  30. package/src/document/processActions/releaseUtil.ts +31 -0
  31. package/src/document/processActions/shared.ts +94 -2
  32. package/src/document/processActions.test.ts +423 -1
  33. package/src/document/reducers.ts +40 -5
  34. package/src/releases/getPerspectiveState.test.ts +1 -1
  35. package/src/releases/releasesStore.test.ts +50 -1
  36. package/src/releases/releasesStore.ts +41 -18
  37. package/src/releases/utils/sortReleases.test.ts +2 -2
  38. package/src/releases/utils/sortReleases.ts +1 -1
  39. package/src/telemetry/environment.test.ts +119 -0
  40. package/src/telemetry/environment.ts +92 -0
  41. package/src/telemetry/{__telemetry__/sdk.telemetry.ts → events.ts} +9 -9
  42. package/src/telemetry/initTelemetry.test.ts +240 -16
  43. package/src/telemetry/initTelemetry.ts +39 -16
  44. package/src/telemetry/telemetryManager.test.ts +129 -65
  45. package/src/telemetry/telemetryManager.ts +41 -29
  46. package/src/telemetry/devMode.test.ts +0 -60
  47. package/src/telemetry/devMode.ts +0 -41
@@ -9,6 +9,7 @@ import {
9
9
  ActionError,
10
10
  type ActionHandlerContext,
11
11
  type ActionHandlerResult,
12
+ applySingleDocPatch,
12
13
  checkGrant,
13
14
  PermissionActionError,
14
15
  } from './shared'
@@ -23,54 +24,18 @@ export function handleEdit(
23
24
  const documentId = getId(action.documentId)
24
25
 
25
26
  if (action.liveEdit) {
26
- // Single-document mode (liveEdit or release perspective): edit directly without draft logic
27
- const userPatches = action.patches?.map((patch) => ({patch: {id: documentId, ...patch}}))
28
-
29
- // skip this action if there are no associated patches
30
- if (!userPatches?.length) return {base, working}
31
-
32
- if (!working[documentId] || !base[documentId]) {
33
- throw new ActionError({
34
- documentId,
35
- transactionId,
36
- message: `Cannot edit document because it does not exist.`,
37
- })
38
- }
39
-
40
- const baseBefore = base[documentId] as SanityDocument
41
- if (userPatches) {
42
- base = processMutations({
43
- documents: base,
44
- transactionId,
45
- mutations: userPatches,
46
- timestamp,
47
- })
48
- }
49
-
50
- const baseAfter = base[documentId] as SanityDocument
51
- const patches = diffValue(baseBefore, baseAfter)
52
-
53
- const workingBefore = working[documentId] as SanityDocument
54
- if (!checkGrant(grants.update, workingBefore)) {
55
- throw new PermissionActionError({
56
- documentId,
57
- transactionId,
58
- message: `You do not have permission to edit document "${documentId}".`,
59
- })
60
- }
61
-
62
- const workingMutations = patches.map((patch) => ({patch: {id: documentId, ...patch}}))
63
-
64
- working = processMutations({
65
- documents: working,
27
+ const result = applySingleDocPatch({
28
+ base,
29
+ working,
30
+ documentId,
31
+ patches: action.patches,
66
32
  transactionId,
67
- mutations: workingMutations,
68
33
  timestamp,
34
+ grants,
69
35
  })
70
-
71
36
  // liveEdit documents use the mutation endpoint directly -- we don't send actions
72
- outgoingMutations.push(...workingMutations)
73
- return {base, working}
37
+ outgoingMutations.push(...result.workingMutations)
38
+ return {base: result.base, working: result.working}
74
39
  }
75
40
 
76
41
  const versionId = isReleasePerspective(action.perspective)
@@ -1,7 +1,7 @@
1
1
  import {type Mutation} from '@sanity/types'
2
2
  import {type ExprNode} from 'groq-js'
3
3
 
4
- import {type DocumentAction} from '../actions'
4
+ import {type Action, type DocumentAction} from '../actions'
5
5
  import {type Grant} from '../permissions'
6
6
  import {type DocumentSet} from '../processMutations'
7
7
  import {type HttpAction} from '../reducers'
@@ -10,6 +10,13 @@ import {handleDelete} from './delete'
10
10
  import {handleDiscard} from './discard'
11
11
  import {handleEdit} from './edit'
12
12
  import {handlePublish} from './publish'
13
+ import {handleReleaseArchive, handleReleaseUnarchive} from './releaseArchive'
14
+ import {handleReleaseCreate} from './releaseCreate'
15
+ import {handleReleaseDelete} from './releaseDelete'
16
+ import {handleReleaseEdit} from './releaseEdit'
17
+ import {handleReleasePublish} from './releasePublish'
18
+ import {handleReleaseSchedule, handleReleaseUnschedule} from './releaseSchedule'
19
+ import {isReleaseAction} from './releaseUtil'
13
20
  import {
14
21
  ActionError,
15
22
  type ActionHandlerContext,
@@ -30,7 +37,7 @@ interface ProcessActionsOptions {
30
37
  /**
31
38
  * The actions to apply to the given documents
32
39
  */
33
- actions: DocumentAction[]
40
+ actions: Action[]
34
41
 
35
42
  /**
36
43
  * The set of documents these actions were intended to be applied to. These
@@ -116,6 +123,24 @@ export function processActions({
116
123
  const outgoingActions: HttpAction[] = []
117
124
  const outgoingMutations: Mutation[] = []
118
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
+
119
144
  for (const action of actions) {
120
145
  const result = dispatch(action, {
121
146
  base,
@@ -143,7 +168,7 @@ export function processActions({
143
168
  }
144
169
  }
145
170
 
146
- function dispatch(action: DocumentAction, ctx: ActionHandlerContext): ActionHandlerResult {
171
+ function dispatch(action: Action, ctx: ActionHandlerContext): ActionHandlerResult {
147
172
  switch (action.type) {
148
173
  case 'document.create':
149
174
  return handleCreate(action, ctx)
@@ -157,6 +182,22 @@ function dispatch(action: DocumentAction, ctx: ActionHandlerContext): ActionHand
157
182
  return handlePublish(action, ctx)
158
183
  case 'document.unpublish':
159
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)
160
201
  default:
161
202
  throw new Error(
162
203
  `Unknown action type: "${
@@ -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
+ }
@@ -0,0 +1,31 @@
1
+ import {type Action, type ReleaseAction} from '../actions'
2
+
3
+ /**
4
+ * Path prefix shared by every release document. Matches studio's
5
+ * `RELEASE_DOCUMENTS_PATH` constant.
6
+ */
7
+ const RELEASE_DOCUMENTS_PATH = '_.releases'
8
+
9
+ /**
10
+ * Returns the full release document ID for the given release name.
11
+ * e.g. `getReleaseDocumentId('my-release') === '_.releases.my-release'`
12
+ * @beta
13
+ */
14
+ export function getReleaseDocumentId(releaseId: string): string {
15
+ return `${RELEASE_DOCUMENTS_PATH}.${releaseId}`
16
+ }
17
+
18
+ const RELEASE_ACTION_TYPES = new Set([
19
+ 'release.create',
20
+ 'release.edit',
21
+ 'release.publish',
22
+ 'release.schedule',
23
+ 'release.unschedule',
24
+ 'release.archive',
25
+ 'release.unarchive',
26
+ 'release.delete',
27
+ ])
28
+
29
+ export function isReleaseAction(action: Action): action is ReleaseAction {
30
+ return RELEASE_ACTION_TYPES.has(action.type)
31
+ }
@@ -1,8 +1,9 @@
1
- import {type Mutation, type SanityDocument} from '@sanity/types'
1
+ import {diffValue} from '@sanity/diff-patch'
2
+ import {type Mutation, type PatchOperations, type SanityDocument} from '@sanity/types'
2
3
  import {evaluateSync, type ExprNode} from 'groq-js'
3
4
 
4
5
  import {type Grant} from '../permissions'
5
- import {type DocumentSet} from '../processMutations'
6
+ import {type DocumentSet, processMutations} from '../processMutations'
6
7
  import {type HttpAction} from '../reducers'
7
8
 
8
9
  export interface ActionHandlerContext {
@@ -45,3 +46,94 @@ export class ActionError extends Error implements ActionErrorOptions {
45
46
  }
46
47
 
47
48
  export class PermissionActionError extends ActionError {}
49
+
50
+ interface ApplySingleDocPatchOptions {
51
+ base: DocumentSet
52
+ working: DocumentSet
53
+ documentId: string
54
+ patches: PatchOperations[] | undefined
55
+ transactionId: string
56
+ timestamp: string
57
+ grants: Record<Grant, ExprNode>
58
+ /**
59
+ * Error message thrown when the target document does not exist in either
60
+ * the base or working set.
61
+ */
62
+ notFoundMessage?: string
63
+ /**
64
+ * Error message thrown when the working document fails the `update` grant.
65
+ */
66
+ permissionMessage?: string
67
+ }
68
+
69
+ interface ApplySingleDocPatchResult {
70
+ base: DocumentSet
71
+ working: DocumentSet
72
+ /**
73
+ * Patch operations representing the minimal diff between base before and
74
+ * after the user's patches were applied. These are the patches that should
75
+ * be sent to the server (or applied to the working set as mutations).
76
+ */
77
+ diffedPatches: PatchOperations[]
78
+ /**
79
+ * Mutation envelopes for `diffedPatches` already keyed to `documentId`.
80
+ * Useful for callers that want to push them to `outgoingMutations`.
81
+ */
82
+ workingMutations: Mutation[]
83
+ }
84
+
85
+ /**
86
+ * Shared logic for applying user-provided patches to a single document that
87
+ * is identified by an exact ID (no draft/published wrapping). Used by the
88
+ * liveEdit branch of `document.edit` and by `release.edit`.
89
+ *
90
+ * Returns the updated base + working sets, plus the diffed patches in both
91
+ * raw and mutation form so the caller can decide what to send to the server.
92
+ */
93
+ export function applySingleDocPatch({
94
+ base: initialBase,
95
+ working: initialWorking,
96
+ documentId,
97
+ patches,
98
+ transactionId,
99
+ timestamp,
100
+ grants,
101
+ notFoundMessage = 'Cannot edit document because it does not exist.',
102
+ permissionMessage = `You do not have permission to edit document "${documentId}".`,
103
+ }: ApplySingleDocPatchOptions): ApplySingleDocPatchResult {
104
+ let base = initialBase
105
+ let working = initialWorking
106
+
107
+ const userPatches = patches?.map((patch) => ({patch: {id: documentId, ...patch}}))
108
+
109
+ if (!userPatches?.length) {
110
+ return {base, working, diffedPatches: [], workingMutations: []}
111
+ }
112
+
113
+ if (!working[documentId] || !base[documentId]) {
114
+ throw new ActionError({documentId, transactionId, message: notFoundMessage})
115
+ }
116
+
117
+ const baseBefore = base[documentId]
118
+ base = processMutations({documents: base, transactionId, mutations: userPatches, timestamp})
119
+ const baseAfter = base[documentId]
120
+ const diffedPatches = diffValue(baseBefore, baseAfter) as PatchOperations[]
121
+
122
+ const workingBefore = working[documentId] as SanityDocument
123
+ if (!checkGrant(grants.update, workingBefore)) {
124
+ throw new PermissionActionError({documentId, transactionId, message: permissionMessage})
125
+ }
126
+
127
+ const workingMutations: Mutation[] = diffedPatches.map((patch) => ({
128
+ patch: {id: documentId, ...patch},
129
+ }))
130
+
131
+ working = processMutations({
132
+ documents: working,
133
+ transactionId,
134
+ mutations: workingMutations,
135
+ timestamp,
136
+ })
137
+
138
+ return {base, working, diffedPatches, workingMutations}
139
+ }