@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.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.
Files changed (197) hide show
  1. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +1147 -0
  2. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
  3. package/dist/_chunks-es/_internal.js +134 -0
  4. package/dist/_chunks-es/_internal.js.map +1 -0
  5. package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
  6. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
  7. package/dist/_chunks-es/telemetryManager.js +93 -0
  8. package/dist/_chunks-es/telemetryManager.js.map +1 -0
  9. package/dist/_exports/_internal.d.ts +95 -0
  10. package/dist/_exports/_internal.d.ts.map +1 -0
  11. package/dist/_exports/_internal.js +24 -0
  12. package/dist/_exports/_internal.js.map +1 -0
  13. package/dist/index.d.ts +1079 -1076
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +1967 -2302
  16. package/dist/index.js.map +1 -1
  17. package/package.json +48 -47
  18. package/src/_exports/_internal.ts +26 -0
  19. package/src/_exports/index.ts +120 -11
  20. package/src/access/checkPermissions.test.ts +55 -0
  21. package/src/access/checkPermissions.ts +54 -0
  22. package/src/agent/agentActions.test.ts +16 -60
  23. package/src/agent/agentActions.ts +25 -38
  24. package/src/applications/applications.test-d.ts +83 -0
  25. package/src/applications/applications.test.ts +130 -0
  26. package/src/applications/applications.ts +364 -0
  27. package/src/applications/userApplications.test.ts +126 -0
  28. package/src/applications/userApplications.ts +261 -0
  29. package/src/auth/authLogger.ts +30 -0
  30. package/src/auth/authMode.test.ts +25 -0
  31. package/src/auth/authMode.ts +6 -3
  32. package/src/auth/authStore.test.ts +194 -13
  33. package/src/auth/authStore.ts +59 -26
  34. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  35. package/src/auth/getOrganizationVerificationState.ts +4 -4
  36. package/src/auth/handleAuthCallback.test.ts +35 -1
  37. package/src/auth/handleAuthCallback.ts +25 -6
  38. package/src/auth/logout.test.ts +75 -2
  39. package/src/auth/logout.ts +23 -4
  40. package/src/auth/refreshStampedToken.test.ts +219 -469
  41. package/src/auth/refreshStampedToken.ts +79 -195
  42. package/src/auth/studioAuth.ts +1 -1
  43. package/src/auth/studioModeAuth.ts +2 -1
  44. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  45. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  46. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  47. package/src/client/clientStore.test.ts +62 -37
  48. package/src/client/clientStore.ts +45 -75
  49. package/src/client/liveEvents.test.ts +151 -0
  50. package/src/client/liveEvents.ts +107 -0
  51. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  52. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  53. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  54. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  55. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  56. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  57. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  58. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  59. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  60. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  61. package/src/comlink/node/getNodeState.test.ts +1 -1
  62. package/src/comlink/node/getNodeState.ts +2 -1
  63. package/src/config/__tests__/handles.test.ts +18 -12
  64. package/src/config/handles.ts +25 -7
  65. package/src/config/sanityConfig.ts +109 -79
  66. package/src/datasets/datasets.test.ts +2 -2
  67. package/src/datasets/datasets.ts +10 -4
  68. package/src/document/actions.test.ts +115 -33
  69. package/src/document/actions.ts +212 -15
  70. package/src/document/applyDocumentActions.test.ts +38 -12
  71. package/src/document/applyDocumentActions.ts +30 -9
  72. package/src/document/documentConstants.ts +7 -0
  73. package/src/document/documentStore.concurrency.test.ts +1306 -0
  74. package/src/document/documentStore.test.ts +673 -278
  75. package/src/document/documentStore.ts +162 -71
  76. package/src/document/events.test.ts +57 -2
  77. package/src/document/events.ts +82 -26
  78. package/src/document/listen.test.ts +56 -0
  79. package/src/document/listen.ts +74 -21
  80. package/src/document/listenerEventOperators.test.ts +311 -0
  81. package/src/document/listenerEventOperators.ts +217 -0
  82. package/src/document/patchOperations.test.ts +49 -2
  83. package/src/document/patchOperations.ts +50 -0
  84. package/src/document/permissions.test.ts +105 -60
  85. package/src/document/permissions.ts +44 -19
  86. package/src/document/processActions/create.ts +138 -0
  87. package/src/document/processActions/delete.ts +100 -0
  88. package/src/document/processActions/discard.ts +63 -0
  89. package/src/document/processActions/edit.ts +154 -0
  90. package/src/document/processActions/processActions.ts +218 -0
  91. package/src/document/processActions/publish.ts +120 -0
  92. package/src/document/processActions/releaseArchive.ts +77 -0
  93. package/src/document/processActions/releaseCreate.ts +59 -0
  94. package/src/document/processActions/releaseDelete.ts +65 -0
  95. package/src/document/processActions/releaseEdit.ts +37 -0
  96. package/src/document/processActions/releasePublish.ts +45 -0
  97. package/src/document/processActions/releaseSchedule.ts +87 -0
  98. package/src/document/processActions/releaseUtil.ts +31 -0
  99. package/src/document/processActions/shared.ts +308 -0
  100. package/src/document/processActions/unpublish.ts +85 -0
  101. package/src/document/processActions.test.ts +1129 -121
  102. package/src/document/reducers.test.ts +639 -64
  103. package/src/document/reducers.ts +187 -48
  104. package/src/document/resourceRules.test.ts +178 -0
  105. package/src/document/resourceRules.ts +117 -0
  106. package/src/document/sharedListener.test.ts +16 -14
  107. package/src/document/sharedListener.ts +24 -8
  108. package/src/document/util.ts +14 -0
  109. package/src/favorites/favorites.test.ts +5 -6
  110. package/src/installations/installations.test-d.ts +68 -0
  111. package/src/installations/installations.test.ts +62 -0
  112. package/src/installations/installations.ts +202 -0
  113. package/src/organization/organization.test-d.ts +36 -0
  114. package/src/organization/organization.test.ts +138 -0
  115. package/src/organization/organization.ts +138 -0
  116. package/src/organizations/organizations.test-d.ts +56 -0
  117. package/src/organizations/organizations.test.ts +150 -0
  118. package/src/organizations/organizations.ts +107 -0
  119. package/src/presence/bifurTransport.test.ts +46 -6
  120. package/src/presence/bifurTransport.ts +19 -2
  121. package/src/presence/presenceStore.test.ts +94 -23
  122. package/src/presence/presenceStore.ts +85 -29
  123. package/src/preview/getPreviewState.test.ts +137 -0
  124. package/src/preview/getPreviewState.ts +69 -0
  125. package/src/preview/previewProjectionUtils.test.ts +9 -22
  126. package/src/preview/previewProjectionUtils.ts +7 -3
  127. package/src/preview/resolvePreview.test.ts +64 -0
  128. package/src/preview/resolvePreview.ts +43 -0
  129. package/src/preview/types.ts +10 -0
  130. package/src/project/project.test-d.ts +37 -0
  131. package/src/project/project.test.ts +157 -12
  132. package/src/project/project.ts +138 -25
  133. package/src/projection/getProjectionState.test.ts +18 -20
  134. package/src/projection/getProjectionState.ts +23 -3
  135. package/src/projection/projectionQuery.ts +5 -5
  136. package/src/projection/projectionStore.test.ts +1 -1
  137. package/src/projection/resolveProjection.test.ts +1 -2
  138. package/src/projection/resolveProjection.ts +16 -0
  139. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  140. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  141. package/src/projection/types.ts +8 -0
  142. package/src/projects/projects.test-d.ts +32 -0
  143. package/src/projects/projects.test.ts +108 -42
  144. package/src/projects/projects.ts +63 -27
  145. package/src/query/queryStore.test.ts +259 -123
  146. package/src/query/queryStore.ts +78 -63
  147. package/src/query/reducers.test.ts +0 -42
  148. package/src/query/reducers.ts +2 -18
  149. package/src/releases/getPerspectiveState.test.ts +17 -23
  150. package/src/releases/getPerspectiveState.ts +8 -20
  151. package/src/releases/observeReleases.test.ts +127 -0
  152. package/src/releases/observeReleases.ts +102 -0
  153. package/src/releases/releasesStore.test.ts +100 -53
  154. package/src/releases/releasesStore.ts +69 -44
  155. package/src/releases/utils/sortReleases.test.ts +38 -2
  156. package/src/releases/utils/sortReleases.ts +15 -10
  157. package/src/store/createActionBinder.test.ts +87 -90
  158. package/src/store/createActionBinder.ts +79 -25
  159. package/src/store/createSanityInstance.test.ts +83 -13
  160. package/src/store/createSanityInstance.ts +77 -2
  161. package/src/store/createStateSourceAction.test.ts +64 -2
  162. package/src/store/createStateSourceAction.ts +39 -44
  163. package/src/store/createStoreInstance.test.ts +4 -2
  164. package/src/store/fetcherStore.test-d.ts +121 -0
  165. package/src/store/fetcherStore.test.ts +299 -0
  166. package/src/store/fetcherStore.ts +801 -0
  167. package/src/telemetry/environment.test.ts +119 -0
  168. package/src/telemetry/environment.ts +92 -0
  169. package/src/telemetry/events.ts +42 -0
  170. package/src/telemetry/initTelemetry.test.ts +432 -0
  171. package/src/telemetry/initTelemetry.ts +218 -0
  172. package/src/telemetry/telemetryManager.test.ts +327 -0
  173. package/src/telemetry/telemetryManager.ts +199 -0
  174. package/src/users/reducers.test.ts +6 -1
  175. package/src/users/reducers.ts +5 -6
  176. package/src/users/types.ts +4 -4
  177. package/src/users/usersStore.test.ts +16 -12
  178. package/src/users/usersStore.ts +5 -1
  179. package/src/utils/buildQuery.test.ts +14 -0
  180. package/src/utils/buildQuery.ts +17 -0
  181. package/src/utils/createFetcherStore.test.ts +7 -5
  182. package/src/utils/createFetcherStore.ts +8 -5
  183. package/src/utils/getStagingApiHost.test.ts +21 -0
  184. package/src/utils/getStagingApiHost.ts +14 -0
  185. package/src/utils/ids.test.ts +1 -29
  186. package/src/utils/ids.ts +0 -10
  187. package/src/utils/includeShape.ts +36 -0
  188. package/src/utils/isImportError.test.ts +72 -0
  189. package/src/utils/isImportError.ts +34 -0
  190. package/src/utils/logger.test.ts +12 -0
  191. package/src/utils/logger.ts +8 -3
  192. package/src/utils/object.test.ts +95 -0
  193. package/src/utils/object.ts +142 -0
  194. package/src/utils/setCleanupTimeout.ts +37 -0
  195. package/src/document/processActions.ts +0 -707
  196. package/src/utils/defineIntent.test.ts +0 -477
  197. package/src/utils/defineIntent.ts +0 -244
@@ -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, identity, 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], identity)) {
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], identity)
76
+ const cantDeletePublished =
77
+ working[publishedId] && !checkGrant(grants.update, working[publishedId], identity)
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, identity, 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], identity)) {
47
+ throw new PermissionActionError({
48
+ documentId,
49
+ transactionId,
50
+ message: `You do not have permission to discard changes for document "${documentId}".`,
51
+ })
52
+ }
53
+
54
+ base = processMutations({documents: base, transactionId, mutations, timestamp})
55
+ working = processMutations({documents: working, transactionId, mutations, timestamp})
56
+
57
+ outgoingMutations.push(...mutations)
58
+ outgoingActions.push({
59
+ actionType: 'sanity.action.document.version.discard',
60
+ versionId,
61
+ })
62
+ return {base, working}
63
+ }
@@ -0,0 +1,154 @@
1
+ import {diffValue} from '@sanity/diff-patch'
2
+ import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
3
+ import {type Mutation, type PatchOperations, type SanityDocument} from '@sanity/types'
4
+
5
+ import {isReleasePerspective} from '../../releases/utils/isReleasePerspective'
6
+ import {type EditDocumentAction} from '../actions'
7
+ import {getId} from '../processMutations'
8
+ import {
9
+ ActionError,
10
+ type ActionHandlerContext,
11
+ type ActionHandlerResult,
12
+ applySingleDocPatch,
13
+ checkGrant,
14
+ createMutationApplier,
15
+ PermissionActionError,
16
+ preserveNumericOperations,
17
+ } from './shared'
18
+
19
+ export function handleEdit(
20
+ action: EditDocumentAction,
21
+ ctx: ActionHandlerContext,
22
+ ): ActionHandlerResult {
23
+ const {transactionId, timestamp, grants, identity, outgoingActions, outgoingMutations} = ctx
24
+ let {base, working} = ctx
25
+
26
+ const documentId = getId(action.documentId)
27
+
28
+ if (action.liveEdit) {
29
+ const result = applySingleDocPatch({
30
+ base,
31
+ working,
32
+ documentId,
33
+ patches: action.patches,
34
+ transactionId,
35
+ timestamp,
36
+ grants,
37
+ identity,
38
+ preserveOperations: action.preserveOperations,
39
+ })
40
+ // liveEdit documents use the mutation endpoint directly -- we don't send actions
41
+ outgoingMutations.push(...result.workingMutations)
42
+ return {base: result.base, working: result.working}
43
+ }
44
+
45
+ const versionId = isReleasePerspective(action.perspective)
46
+ ? getVersionId(DocumentId(documentId), action.perspective.releaseName)
47
+ : undefined
48
+ const draftId = getDraftId(DocumentId(documentId))
49
+ const publishedId = getPublishedId(DocumentId(documentId))
50
+ const patchDocumentId = isReleasePerspective(action.perspective) ? versionId! : draftId
51
+ const userPatches = action.patches?.map((patch) => ({
52
+ patch: {id: patchDocumentId, ...patch},
53
+ }))
54
+
55
+ // skip this action if there are no associated patches
56
+ if (!userPatches?.length) return {base, working}
57
+
58
+ if (isReleasePerspective(action.perspective)) {
59
+ if (!working[versionId!] && !base[versionId!]) {
60
+ throw new ActionError({
61
+ documentId,
62
+ transactionId,
63
+ message: `This document does not exist in the release. Please create it or add it to the release first.`,
64
+ })
65
+ }
66
+ } else if (
67
+ (!working[draftId] && !working[publishedId]) ||
68
+ (!base[draftId] && !base[publishedId])
69
+ ) {
70
+ throw new ActionError({
71
+ documentId,
72
+ transactionId,
73
+ message: `Cannot edit document because it does not exist in draft or published form.`,
74
+ })
75
+ }
76
+
77
+ const applyMutations = createMutationApplier({
78
+ documentId,
79
+ transactionId,
80
+ timestamp,
81
+ preserveOperations: action.preserveOperations,
82
+ })
83
+
84
+ const baseMutations: Mutation[] = []
85
+ // don't create a draft from the published version in a release perspective
86
+ if (!isReleasePerspective(action.perspective) && !base[draftId] && base[publishedId]) {
87
+ // otherwise make a draft from the published version
88
+ baseMutations.push({create: {...base[publishedId], _id: draftId}})
89
+ }
90
+
91
+ // the above statement and guards should make this never be null or undefined
92
+ const baseBefore = base[patchDocumentId] ?? base[publishedId]
93
+ if (userPatches) {
94
+ baseMutations.push(...userPatches)
95
+ }
96
+
97
+ base = applyMutations(base, baseMutations, 'base')
98
+ // this one will always be defined because a patch mutation will never
99
+ // delete an input document
100
+ const baseAfter = base[patchDocumentId] as SanityDocument
101
+ // when the caller asked us to preserve their operations, forward their
102
+ // patches verbatim instead of re-deriving them from a snapshot diff. this
103
+ // keeps keyed array operations addressable so concurrent edits from other
104
+ // clients interleave instead of being overwritten. otherwise, diff the
105
+ // snapshots but swap back any `inc`/`dec` the diff collapsed into a `set`,
106
+ // so concurrent increments accumulate instead of last-write-winning
107
+ const patches = action.preserveOperations
108
+ ? (action.patches ?? [])
109
+ : preserveNumericOperations(
110
+ baseBefore,
111
+ action.patches,
112
+ diffValue(baseBefore, baseAfter) as PatchOperations[],
113
+ )
114
+
115
+ const workingMutations: Mutation[] = []
116
+ if (!isReleasePerspective(action.perspective) && !working[draftId] && working[publishedId]) {
117
+ const newDraftFromPublished = {...working[publishedId], _id: draftId}
118
+
119
+ if (!checkGrant(grants.create, newDraftFromPublished, identity)) {
120
+ throw new PermissionActionError({
121
+ documentId,
122
+ transactionId,
123
+ message: `You do not have permission to create a draft for editing this document.`,
124
+ })
125
+ }
126
+
127
+ workingMutations.push({create: newDraftFromPublished})
128
+ }
129
+
130
+ // the first if statement should make this never be null or undefined
131
+ const workingBefore = working[patchDocumentId] ?? working[publishedId]
132
+ if (!checkGrant(grants.update, workingBefore!, identity)) {
133
+ throw new PermissionActionError({
134
+ documentId,
135
+ transactionId,
136
+ message: `You do not have permission to edit document "${documentId}".`,
137
+ })
138
+ }
139
+ workingMutations.push(...patches.map((patch) => ({patch: {id: patchDocumentId, ...patch}})))
140
+
141
+ working = applyMutations(working, workingMutations, 'working')
142
+
143
+ outgoingMutations.push(...workingMutations)
144
+ outgoingActions.push(
145
+ ...patches.map((patch) => ({
146
+ actionType: 'sanity.action.document.edit' as const,
147
+ draftId: patchDocumentId,
148
+ publishedId,
149
+ patch: patch as PatchOperations,
150
+ })),
151
+ )
152
+
153
+ return {base, working}
154
+ }
@@ -0,0 +1,218 @@
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
+ /**
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
+
75
+ // // TODO: implement initial values from the schema?
76
+ // initialValues?: {[TDocumentType in string]?: {_type: string}}
77
+ }
78
+
79
+ interface ProcessActionsResult {
80
+ /**
81
+ * The resulting document set after the actions have been applied. This is
82
+ * derived from the working documents.
83
+ */
84
+ working: DocumentSet
85
+ /**
86
+ * The document set before the actions have been applied. This is simply the
87
+ * input of the `working` document set.
88
+ */
89
+ previous: DocumentSet
90
+ /**
91
+ * The outgoing action that were collected when applying the actions. These
92
+ * are sent to the Actions HTTP API
93
+ */
94
+ outgoingActions: HttpAction[]
95
+ /**
96
+ * The outgoing mutations that were collected when applying the actions. These
97
+ * are here for debugging purposes.
98
+ */
99
+ outgoingMutations: Mutation[]
100
+ /**
101
+ * The previous revisions of the given documents before the actions were applied.
102
+ */
103
+ previousRevs: {[TDocumentId in string]?: string}
104
+ }
105
+
106
+ /**
107
+ * Applies the given set of actions to the working set of documents and converts
108
+ * high-level actions into lower-level outgoing mutations/actions that respect
109
+ * the current state of the working documents.
110
+ *
111
+ * Supports a "base" and "working" set of documents to allow actions to be
112
+ * applied on top of a different working set of documents in a 3-way merge
113
+ *
114
+ * Actions are applied to the base set of documents first. The difference
115
+ * between the base before and after is used to create a patch. This patch is
116
+ * then applied to the working set of documents and is set as the outgoing patch
117
+ * sent to the server.
118
+ */
119
+ export function processActions({
120
+ actions,
121
+ transactionId,
122
+ working: initialWorking,
123
+ base: initialBase,
124
+ timestamp,
125
+ grants,
126
+ identity,
127
+ }: ProcessActionsOptions): ProcessActionsResult {
128
+ let base: DocumentSet = {...initialBase}
129
+ let working: DocumentSet = {...initialWorking}
130
+
131
+ const outgoingActions: HttpAction[] = []
132
+ const outgoingMutations: Mutation[] = []
133
+
134
+ // liveEdit document actions go to the mutations API, since the actions API
135
+ // requires a draft+published pair. Mixing them with anything else in the same
136
+ // transaction would silently lose atomicity for the non-liveEdit operations,
137
+ // so require users to split the transaction.
138
+ // (Note that the reducers already does this for us -- you'd have to try hard to mix them.)
139
+ const liveEditAction = actions.find((action) => !isReleaseAction(action) && action.liveEdit) as
140
+ | DocumentAction
141
+ | undefined
142
+ const otherAction = actions.find((action) => isReleaseAction(action) || !action.liveEdit)
143
+ if (liveEditAction && otherAction) {
144
+ throw new ActionError({
145
+ documentId: liveEditAction.documentId!,
146
+ transactionId,
147
+ message:
148
+ 'Cannot combine liveEdit document actions with other actions in the same transaction. Submit them as separate transactions.',
149
+ })
150
+ }
151
+
152
+ for (const action of actions) {
153
+ const result = dispatch(action, {
154
+ base,
155
+ working,
156
+ transactionId,
157
+ timestamp,
158
+ grants,
159
+ identity,
160
+ outgoingActions,
161
+ outgoingMutations,
162
+ })
163
+ base = result.base
164
+ working = result.working
165
+ }
166
+
167
+ const previousRevs = Object.fromEntries(
168
+ Object.entries(initialWorking).map(([id, doc]) => [id, doc?._rev]),
169
+ )
170
+
171
+ return {
172
+ working,
173
+ outgoingActions,
174
+ outgoingMutations,
175
+ previous: initialWorking,
176
+ previousRevs,
177
+ }
178
+ }
179
+
180
+ function dispatch(action: Action, ctx: ActionHandlerContext): ActionHandlerResult {
181
+ switch (action.type) {
182
+ case 'document.create':
183
+ return handleCreate(action, ctx)
184
+ case 'document.delete':
185
+ return handleDelete(action, ctx)
186
+ case 'document.discard':
187
+ return handleDiscard(action, ctx)
188
+ case 'document.edit':
189
+ return handleEdit(action, ctx)
190
+ case 'document.publish':
191
+ return handlePublish(action, ctx)
192
+ case 'document.unpublish':
193
+ return handleUnpublish(action, ctx)
194
+ case 'release.create':
195
+ return handleReleaseCreate(action, ctx)
196
+ case 'release.edit':
197
+ return handleReleaseEdit(action, ctx)
198
+ case 'release.publish':
199
+ return handleReleasePublish(action, ctx)
200
+ case 'release.schedule':
201
+ return handleReleaseSchedule(action, ctx)
202
+ case 'release.unschedule':
203
+ return handleReleaseUnschedule(action, ctx)
204
+ case 'release.archive':
205
+ return handleReleaseArchive(action, ctx)
206
+ case 'release.unarchive':
207
+ return handleReleaseUnarchive(action, ctx)
208
+ case 'release.delete':
209
+ return handleReleaseDelete(action, ctx)
210
+ default:
211
+ throw new Error(
212
+ `Unknown action type: "${
213
+ // @ts-expect-error invalid input
214
+ action.type
215
+ }". Please contact support if this issue persists.`,
216
+ )
217
+ }
218
+ }
@@ -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, identity, 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], identity)) {
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, identity)) {
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, identity)) {
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
+ }