@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,308 @@
1
+ import {diffValue} from '@sanity/diff-patch'
2
+ import {jsonMatch, stringifyPath} from '@sanity/json-match'
3
+ import {type Mutation, type PatchOperations, type SanityDocument} from '@sanity/types'
4
+ import {evaluateSync, type ExprNode} from 'groq-js'
5
+
6
+ import {type Grant} from '../permissions'
7
+ import {type DocumentSet, processMutations} from '../processMutations'
8
+ import {type HttpAction} from '../reducers'
9
+
10
+ export interface ActionHandlerContext {
11
+ base: DocumentSet
12
+ working: DocumentSet
13
+ transactionId: string
14
+ timestamp: string
15
+ grants: Record<Grant, ExprNode>
16
+ /**
17
+ * The current user's ID, used by GROQ's `identity()` when evaluating ACL
18
+ * filters. May be `undefined` before the user has loaded; in that case
19
+ * `identity()` evaluates to null.
20
+ */
21
+ identity: string | undefined
22
+ outgoingActions: HttpAction[]
23
+ outgoingMutations: Mutation[]
24
+ }
25
+
26
+ export interface ActionHandlerResult {
27
+ base: DocumentSet
28
+ working: DocumentSet
29
+ }
30
+
31
+ export function checkGrant(
32
+ grantExpr: ExprNode,
33
+ document: SanityDocument,
34
+ identity: string | undefined,
35
+ ): boolean {
36
+ const value = evaluateSync(grantExpr, {params: {document}, identity})
37
+ return value.type === 'boolean' && value.data
38
+ }
39
+
40
+ interface ActionErrorOptions {
41
+ message: string
42
+ documentId: string
43
+ transactionId: string
44
+ }
45
+
46
+ /**
47
+ * Thrown when a precondition for an action failed.
48
+ */
49
+ export class ActionError extends Error implements ActionErrorOptions {
50
+ documentId!: string
51
+ transactionId!: string
52
+
53
+ constructor(options: ActionErrorOptions) {
54
+ super(options.message)
55
+ Object.assign(this, options)
56
+ }
57
+ }
58
+
59
+ export class PermissionActionError extends ActionError {}
60
+
61
+ /**
62
+ * Creates a `processMutations` wrapper that surfaces application failures as
63
+ * `ActionError`s when the caller asked to preserve their patch operations.
64
+ * With preserved operations, patches can legitimately fail to apply (e.g.
65
+ * re-applied onto a diverged document during a rebase), so wrapping lets a
66
+ * rebase skip the transaction instead of failing the store. Without
67
+ * `preserveOperations`, errors are rethrown untouched.
68
+ */
69
+ export function createMutationApplier(options: {
70
+ documentId: string
71
+ transactionId: string
72
+ timestamp: string
73
+ preserveOperations: boolean | undefined
74
+ }): (
75
+ documents: DocumentSet,
76
+ mutations: Mutation[],
77
+ documentSetName: 'base' | 'working',
78
+ ) => DocumentSet {
79
+ const {documentId, transactionId, timestamp, preserveOperations} = options
80
+ return (documents, mutations, documentSetName) => {
81
+ try {
82
+ return processMutations({documents, transactionId, mutations, timestamp})
83
+ } catch (error) {
84
+ if (!preserveOperations) throw error
85
+ throw new ActionError({
86
+ documentId,
87
+ transactionId,
88
+ message: `Failed to apply patches to the ${documentSetName} document: ${
89
+ error instanceof Error ? error.message : 'Unknown error'
90
+ }`,
91
+ })
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Re-applies the operational intent of user-supplied `inc`/`dec` patches to a
98
+ * set of snapshot-diffed patches.
99
+ *
100
+ * The outgoing patches sent to Content Lake are re-derived by diffing the
101
+ * base document before and after the user's patches were applied. That diff
102
+ * collapses an `inc` into a `set` of the resulting number, which turns
103
+ * concurrent increments from different clients into last-write-wins. This
104
+ * helper finds diffed `set` operations that are exactly explained by the
105
+ * user's `inc`/`dec` operations and swaps them back, so the server applies
106
+ * the increment against whatever value is current at commit time.
107
+ *
108
+ * A `set` is only swapped when its value equals the base value plus the
109
+ * accumulated user deltas for that exact path; anything else (e.g. an `inc`
110
+ * combined with a `set` of the same field) keeps the diffed `set`.
111
+ *
112
+ * Content Lake fails the whole transaction when `inc`/`dec` targets a missing
113
+ * path (unlike `set`, which always succeeds). To avoid turning a concurrent
114
+ * field removal into a transaction revert, the emitted patch seeds the target
115
+ * with a `setIfMissing` of the base value; the server executes `setIfMissing`
116
+ * before `inc`/`dec` within a single patch, so a concurrently removed field
117
+ * degrades to the same result the diffed `set` would have produced. A path
118
+ * into a concurrently removed keyed array item can still fail the
119
+ * transaction (`setIfMissing` cannot create array items); the transaction is
120
+ * then reverted and reported, which is preferable to silently resurrecting
121
+ * the item.
122
+ */
123
+ export function preserveNumericOperations(
124
+ baseBefore: SanityDocument | null | undefined,
125
+ userPatches: PatchOperations[] | undefined,
126
+ diffedPatches: PatchOperations[],
127
+ ): PatchOperations[] {
128
+ if (!baseBefore || !userPatches?.length) return diffedPatches
129
+
130
+ // accumulate the user's inc/dec deltas per concrete (stringified) path,
131
+ // tracking the expected final value by replaying the additions in order
132
+ const targets = new Map<string, {baseValue: number; expectedValue: number; delta: number}>()
133
+ for (const patch of userPatches) {
134
+ for (const [op, sign] of [
135
+ ['inc', 1],
136
+ ['dec', -1],
137
+ ] as const) {
138
+ const record = patch[op]
139
+ if (!record) continue
140
+ for (const [pathExpression, amount] of Object.entries(record)) {
141
+ if (typeof amount !== 'number') continue
142
+ for (const match of jsonMatch(baseBefore, pathExpression)) {
143
+ const path = stringifyPath(match.path)
144
+ const existing = targets.get(path)
145
+ if (existing) {
146
+ existing.expectedValue += sign * amount
147
+ existing.delta += sign * amount
148
+ } else if (typeof match.value === 'number') {
149
+ targets.set(path, {
150
+ baseValue: match.value,
151
+ expectedValue: match.value + sign * amount,
152
+ delta: sign * amount,
153
+ })
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ if (!targets.size) return diffedPatches
160
+
161
+ const preserved = new Map<string, {delta: number; baseValue: number}>()
162
+ const next = diffedPatches.flatMap((patch) => {
163
+ if (!patch.set) return [patch]
164
+
165
+ const keptSet: Record<string, unknown> = {}
166
+ for (const [path, value] of Object.entries(patch.set)) {
167
+ const target = targets.get(path)
168
+ if (target && typeof value === 'number' && value === target.expectedValue) {
169
+ preserved.set(path, {delta: target.delta, baseValue: target.baseValue})
170
+ } else {
171
+ keptSet[path] = value
172
+ }
173
+ }
174
+
175
+ if (Object.keys(keptSet).length === Object.keys(patch.set).length) return [patch]
176
+ const {set: _set, ...rest} = patch
177
+ const withKept = Object.keys(keptSet).length ? {...rest, set: keptSet} : rest
178
+ return Object.keys(withKept).length ? [withKept] : []
179
+ })
180
+
181
+ if (!preserved.size) return next
182
+
183
+ const setIfMissing: Record<string, unknown> = {}
184
+ const inc: Record<string, number> = {}
185
+ const dec: Record<string, number> = {}
186
+ for (const [path, {delta, baseValue}] of preserved) {
187
+ setIfMissing[path] = baseValue
188
+ if (delta >= 0) inc[path] = delta
189
+ else dec[path] = -delta
190
+ }
191
+ return [
192
+ ...next,
193
+ {
194
+ setIfMissing,
195
+ ...(Object.keys(inc).length && {inc}),
196
+ ...(Object.keys(dec).length && {dec}),
197
+ },
198
+ ]
199
+ }
200
+
201
+ interface ApplySingleDocPatchOptions {
202
+ base: DocumentSet
203
+ working: DocumentSet
204
+ documentId: string
205
+ patches: PatchOperations[] | undefined
206
+ transactionId: string
207
+ timestamp: string
208
+ grants: Record<Grant, ExprNode>
209
+ identity: string | undefined
210
+ /**
211
+ * Error message thrown when the target document does not exist in either
212
+ * the base or working set.
213
+ */
214
+ notFoundMessage?: string
215
+ /**
216
+ * Error message thrown when the working document fails the `update` grant.
217
+ */
218
+ permissionMessage?: string
219
+ /**
220
+ * When `true`, the given patches are used verbatim instead of being
221
+ * re-derived by diffing the base document before and after application.
222
+ * Patch application failures are surfaced as `ActionError`s so a rebase
223
+ * can skip the transaction instead of failing the store.
224
+ */
225
+ preserveOperations?: boolean
226
+ }
227
+
228
+ interface ApplySingleDocPatchResult {
229
+ base: DocumentSet
230
+ working: DocumentSet
231
+ /**
232
+ * Patch operations representing the minimal diff between base before and
233
+ * after the user's patches were applied. These are the patches that should
234
+ * be sent to the server (or applied to the working set as mutations).
235
+ */
236
+ diffedPatches: PatchOperations[]
237
+ /**
238
+ * Mutation envelopes for `diffedPatches` already keyed to `documentId`.
239
+ * Useful for callers that want to push them to `outgoingMutations`.
240
+ */
241
+ workingMutations: Mutation[]
242
+ }
243
+
244
+ /**
245
+ * Shared logic for applying user-provided patches to a single document that
246
+ * is identified by an exact ID (no draft/published wrapping). Used by the
247
+ * liveEdit branch of `document.edit` and by `release.edit`.
248
+ *
249
+ * Returns the updated base + working sets, plus the diffed patches in both
250
+ * raw and mutation form so the caller can decide what to send to the server.
251
+ */
252
+ export function applySingleDocPatch({
253
+ base: initialBase,
254
+ working: initialWorking,
255
+ documentId,
256
+ patches,
257
+ transactionId,
258
+ timestamp,
259
+ grants,
260
+ identity,
261
+ notFoundMessage = 'Cannot edit document because it does not exist.',
262
+ permissionMessage = `You do not have permission to edit document "${documentId}".`,
263
+ preserveOperations,
264
+ }: ApplySingleDocPatchOptions): ApplySingleDocPatchResult {
265
+ let base = initialBase
266
+ let working = initialWorking
267
+
268
+ const userPatches = patches?.map((patch) => ({patch: {id: documentId, ...patch}}))
269
+
270
+ if (!userPatches?.length) {
271
+ return {base, working, diffedPatches: [], workingMutations: []}
272
+ }
273
+
274
+ if (!working[documentId] || !base[documentId]) {
275
+ throw new ActionError({documentId, transactionId, message: notFoundMessage})
276
+ }
277
+
278
+ const applyMutations = createMutationApplier({
279
+ documentId,
280
+ transactionId,
281
+ timestamp,
282
+ preserveOperations,
283
+ })
284
+
285
+ const baseBefore = base[documentId]
286
+ base = applyMutations(base, userPatches, 'base')
287
+ const baseAfter = base[documentId]
288
+ const diffedPatches = preserveOperations
289
+ ? (patches as PatchOperations[])
290
+ : preserveNumericOperations(
291
+ baseBefore,
292
+ patches,
293
+ diffValue(baseBefore, baseAfter) as PatchOperations[],
294
+ )
295
+
296
+ const workingBefore = working[documentId] as SanityDocument
297
+ if (!checkGrant(grants.update, workingBefore, identity)) {
298
+ throw new PermissionActionError({documentId, transactionId, message: permissionMessage})
299
+ }
300
+
301
+ const workingMutations: Mutation[] = diffedPatches.map((patch) => ({
302
+ patch: {id: documentId, ...patch},
303
+ }))
304
+
305
+ working = applyMutations(working, workingMutations, 'working')
306
+
307
+ return {base, working, diffedPatches, workingMutations}
308
+ }
@@ -0,0 +1,85 @@
1
+ import {DocumentId, getDraftId, getPublishedId} from '@sanity/id-utils'
2
+ import {type Mutation, type SanityDocument} from '@sanity/types'
3
+
4
+ import {isReleasePerspective} from '../../releases/utils/isReleasePerspective'
5
+ import {type UnpublishDocumentAction} 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 handleUnpublish(
16
+ action: UnpublishDocumentAction,
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 || isReleasePerspective(action.perspective)) {
25
+ throw new ActionError({
26
+ documentId,
27
+ transactionId,
28
+ message: `Cannot unpublish this document. Unpublishing is not supported for liveEdit or version (release) documents.`,
29
+ })
30
+ }
31
+
32
+ // Standard draft/published or version logic
33
+ const draftId = getDraftId(DocumentId(documentId))
34
+ const publishedId = getPublishedId(DocumentId(documentId))
35
+
36
+ if (!working[publishedId] && !base[publishedId]) {
37
+ throw new ActionError({
38
+ documentId,
39
+ transactionId,
40
+ message: `Cannot unpublish because the document "${documentId}" is not currently published.`,
41
+ })
42
+ }
43
+
44
+ const sourceDoc = working[publishedId] ?? (base[publishedId] as SanityDocument)
45
+ const newDraftFromPublished = {...sourceDoc, _id: draftId}
46
+ const mutations: Mutation[] = [
47
+ {delete: {id: publishedId}},
48
+ {createIfNotExists: newDraftFromPublished},
49
+ ]
50
+
51
+ if (!checkGrant(grants.update, sourceDoc, identity)) {
52
+ throw new PermissionActionError({
53
+ documentId,
54
+ transactionId,
55
+ message: `You do not have permission to unpublish the document "${documentId}".`,
56
+ })
57
+ }
58
+
59
+ if (!working[draftId] && !checkGrant(grants.create, newDraftFromPublished, identity)) {
60
+ throw new PermissionActionError({
61
+ documentId,
62
+ transactionId,
63
+ message: `You do not have permission to create a draft from the published version of "${documentId}".`,
64
+ })
65
+ }
66
+
67
+ base = processMutations({
68
+ documents: base,
69
+ transactionId,
70
+ mutations: [
71
+ {delete: {id: publishedId}},
72
+ {createIfNotExists: {...(base[publishedId] ?? sourceDoc), _id: draftId}},
73
+ ],
74
+ timestamp,
75
+ })
76
+ working = processMutations({documents: working, transactionId, mutations, timestamp})
77
+
78
+ outgoingMutations.push(...mutations)
79
+ outgoingActions.push({
80
+ actionType: 'sanity.action.document.unpublish',
81
+ draftId,
82
+ publishedId,
83
+ })
84
+ return {base, working}
85
+ }