@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.2

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 (241) hide show
  1. package/README.md +12 -0
  2. package/dist/_exports/_internal.d.ts +163 -0
  3. package/dist/_exports/_internal.d.ts.map +1 -0
  4. package/dist/_exports/_internal.js +332 -0
  5. package/dist/_exports/_internal.js.map +1 -0
  6. package/dist/_exports/agent.d.ts +65 -0
  7. package/dist/_exports/agent.d.ts.map +1 -0
  8. package/dist/_exports/agent.js +71 -0
  9. package/dist/_exports/agent.js.map +1 -0
  10. package/dist/_exports/comlink.d.ts +118 -0
  11. package/dist/_exports/comlink.d.ts.map +1 -0
  12. package/dist/_exports/comlink.js +69 -0
  13. package/dist/_exports/comlink.js.map +1 -0
  14. package/dist/authStore-CtE9bLHW.js +754 -0
  15. package/dist/authStore-CtE9bLHW.js.map +1 -0
  16. package/dist/authStore-DKFX8fn7.d.ts +3179 -0
  17. package/dist/authStore-DKFX8fn7.d.ts.map +1 -0
  18. package/dist/clientStore-BtE2-q2i.js +90 -0
  19. package/dist/clientStore-BtE2-q2i.js.map +1 -0
  20. package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
  21. package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
  22. package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
  23. package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
  24. package/dist/getEnv-D5RGkQLu.js +8 -0
  25. package/dist/getEnv-D5RGkQLu.js.map +1 -0
  26. package/dist/getNodeState-CR3__B2D.js +67 -0
  27. package/dist/getNodeState-CR3__B2D.js.map +1 -0
  28. package/dist/index.d.ts +3 -2332
  29. package/dist/index.js +4171 -4332
  30. package/dist/index.js.map +1 -1
  31. package/dist/logger-DAtJb5RQ.js +173 -0
  32. package/dist/logger-DAtJb5RQ.js.map +1 -0
  33. package/dist/object-CxXlSVg7.js +83 -0
  34. package/dist/object-CxXlSVg7.js.map +1 -0
  35. package/dist/queryStore-DPf9sN3V.js +1346 -0
  36. package/dist/queryStore-DPf9sN3V.js.map +1 -0
  37. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  38. package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
  39. package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
  40. package/dist/telemetryManager-Dq_qCkeI.js +127 -0
  41. package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
  42. package/dist/version-BChjsi4u.js +9 -0
  43. package/dist/version-BChjsi4u.js.map +1 -0
  44. package/package.json +47 -52
  45. package/src/_exports/_internal.ts +27 -0
  46. package/src/_exports/agent.ts +19 -0
  47. package/src/_exports/comlink.ts +16 -0
  48. package/src/_exports/index.ts +125 -64
  49. package/src/access/checkPermissions.test.ts +55 -0
  50. package/src/access/checkPermissions.ts +54 -0
  51. package/src/agent/agentActions.test.ts +16 -60
  52. package/src/agent/agentActions.ts +25 -38
  53. package/src/applications/applications.test-d.ts +88 -0
  54. package/src/applications/applications.test.ts +130 -0
  55. package/src/applications/applications.ts +367 -0
  56. package/src/auth/authLogger.ts +30 -0
  57. package/src/auth/authStore.test.ts +194 -13
  58. package/src/auth/authStore.ts +58 -26
  59. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  60. package/src/auth/getOrganizationVerificationState.ts +4 -4
  61. package/src/auth/handleAuthCallback.test.ts +35 -1
  62. package/src/auth/handleAuthCallback.ts +25 -6
  63. package/src/auth/logout.test.ts +75 -2
  64. package/src/auth/logout.ts +23 -4
  65. package/src/auth/refreshStampedToken.test.ts +219 -469
  66. package/src/auth/refreshStampedToken.ts +79 -195
  67. package/src/auth/studioAuth.ts +1 -1
  68. package/src/auth/studioModeAuth.ts +2 -1
  69. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  70. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  71. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  72. package/src/client/clientStore.test.ts +62 -37
  73. package/src/client/clientStore.ts +45 -75
  74. package/src/client/liveEvents.test.ts +151 -0
  75. package/src/client/liveEvents.ts +107 -0
  76. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  77. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  78. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  79. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  80. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  81. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  82. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  83. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  84. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  85. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  86. package/src/comlink/node/getNodeState.test.ts +1 -1
  87. package/src/comlink/node/getNodeState.ts +2 -1
  88. package/src/comments/addonDatasetStore.test.ts +215 -0
  89. package/src/comments/addonDatasetStore.ts +249 -0
  90. package/src/comments/buildCommentThreads.test.ts +83 -0
  91. package/src/comments/buildCommentThreads.ts +54 -0
  92. package/src/comments/commentActions.test.ts +540 -0
  93. package/src/comments/commentActions.ts +555 -0
  94. package/src/comments/commentFieldPath.test.ts +40 -0
  95. package/src/comments/commentFieldPath.ts +44 -0
  96. package/src/comments/commentsConstants.ts +80 -0
  97. package/src/comments/commentsStore.test.ts +419 -0
  98. package/src/comments/commentsStore.ts +483 -0
  99. package/src/comments/normalizeComment.test.ts +116 -0
  100. package/src/comments/normalizeComment.ts +45 -0
  101. package/src/comments/observeComments.test.ts +193 -0
  102. package/src/comments/observeComments.ts +113 -0
  103. package/src/comments/reducers.test.ts +277 -0
  104. package/src/comments/reducers.ts +316 -0
  105. package/src/comments/types.ts +249 -0
  106. package/src/comments/weakenReferences.test.ts +47 -0
  107. package/src/comments/weakenReferences.ts +29 -0
  108. package/src/config/__tests__/handles.test.ts +18 -12
  109. package/src/config/handles.ts +25 -7
  110. package/src/config/sanityConfig.ts +40 -82
  111. package/src/datasets/datasets.test.ts +2 -2
  112. package/src/datasets/datasets.ts +10 -4
  113. package/src/document/actions.test.ts +115 -33
  114. package/src/document/actions.ts +212 -15
  115. package/src/document/applyDocumentActions.test.ts +35 -13
  116. package/src/document/applyDocumentActions.ts +30 -9
  117. package/src/document/documentConstants.ts +15 -0
  118. package/src/document/documentStore.concurrency.test.ts +1306 -0
  119. package/src/document/documentStore.test.ts +753 -273
  120. package/src/document/documentStore.ts +197 -75
  121. package/src/document/events.test.ts +57 -2
  122. package/src/document/events.ts +82 -26
  123. package/src/document/listen.test.ts +56 -0
  124. package/src/document/listen.ts +74 -21
  125. package/src/document/listenerEventOperators.test.ts +311 -0
  126. package/src/document/listenerEventOperators.ts +217 -0
  127. package/src/document/patchOperations.test.ts +76 -2
  128. package/src/document/patchOperations.ts +60 -1
  129. package/src/document/permissions.test.ts +107 -60
  130. package/src/document/permissions.ts +44 -19
  131. package/src/document/processActions/create.ts +138 -0
  132. package/src/document/processActions/delete.ts +100 -0
  133. package/src/document/processActions/discard.ts +63 -0
  134. package/src/document/processActions/edit.ts +154 -0
  135. package/src/document/processActions/processActions.ts +218 -0
  136. package/src/document/processActions/publish.ts +120 -0
  137. package/src/document/processActions/releaseArchive.ts +77 -0
  138. package/src/document/processActions/releaseCreate.ts +59 -0
  139. package/src/document/processActions/releaseDelete.ts +65 -0
  140. package/src/document/processActions/releaseEdit.ts +37 -0
  141. package/src/document/processActions/releasePublish.ts +45 -0
  142. package/src/document/processActions/releaseSchedule.ts +87 -0
  143. package/src/document/processActions/releaseUtil.ts +31 -0
  144. package/src/document/processActions/shared.ts +308 -0
  145. package/src/document/processActions/unpublish.ts +85 -0
  146. package/src/document/processActions.test.ts +1129 -121
  147. package/src/document/reducers.test.ts +772 -64
  148. package/src/document/reducers.ts +205 -51
  149. package/src/document/resourceRules.test.ts +178 -0
  150. package/src/document/resourceRules.ts +117 -0
  151. package/src/document/sharedListener.test.ts +16 -14
  152. package/src/document/sharedListener.ts +24 -8
  153. package/src/document/util.test.ts +46 -0
  154. package/src/document/util.ts +50 -0
  155. package/src/favorites/favorites.test.ts +5 -6
  156. package/src/installations/installations.test-d.ts +73 -0
  157. package/src/installations/installations.test.ts +62 -0
  158. package/src/installations/installations.ts +206 -0
  159. package/src/organization/organization.test-d.ts +36 -0
  160. package/src/organization/organization.test.ts +138 -0
  161. package/src/organization/organization.ts +138 -0
  162. package/src/organizations/organizations.test-d.ts +56 -0
  163. package/src/organizations/organizations.test.ts +150 -0
  164. package/src/organizations/organizations.ts +107 -0
  165. package/src/presence/bifurTransport.test.ts +201 -9
  166. package/src/presence/bifurTransport.ts +98 -14
  167. package/src/presence/paths.test.ts +70 -0
  168. package/src/presence/paths.ts +41 -0
  169. package/src/presence/presenceStore.test.ts +915 -24
  170. package/src/presence/presenceStore.ts +576 -104
  171. package/src/presence/types.ts +175 -3
  172. package/src/preview/previewProjectionUtils.test.ts +9 -22
  173. package/src/preview/previewProjectionUtils.ts +7 -3
  174. package/src/preview/types.ts +0 -15
  175. package/src/project/project.test-d.ts +37 -0
  176. package/src/project/project.test.ts +157 -12
  177. package/src/project/project.ts +138 -25
  178. package/src/projection/getProjectionState.test.ts +18 -20
  179. package/src/projection/getProjectionState.ts +23 -3
  180. package/src/projection/projectionQuery.ts +5 -5
  181. package/src/projection/projectionStore.test.ts +1 -9
  182. package/src/projection/resolveProjection.test.ts +1 -2
  183. package/src/projection/resolveProjection.ts +16 -0
  184. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  185. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  186. package/src/projects/projects.test-d.ts +32 -0
  187. package/src/projects/projects.test.ts +108 -42
  188. package/src/projects/projects.ts +63 -27
  189. package/src/query/queryStore.test.ts +259 -123
  190. package/src/query/queryStore.ts +85 -69
  191. package/src/query/reducers.test.ts +0 -42
  192. package/src/query/reducers.ts +2 -18
  193. package/src/releases/getPerspectiveState.test.ts +17 -23
  194. package/src/releases/getPerspectiveState.ts +8 -20
  195. package/src/releases/observeReleases.test.ts +127 -0
  196. package/src/releases/observeReleases.ts +102 -0
  197. package/src/releases/releasesStore.test.ts +100 -53
  198. package/src/releases/releasesStore.ts +69 -44
  199. package/src/releases/utils/sortReleases.test.ts +38 -2
  200. package/src/releases/utils/sortReleases.ts +15 -10
  201. package/src/store/createActionBinder.test.ts +109 -90
  202. package/src/store/createActionBinder.ts +36 -28
  203. package/src/store/createSanityInstance.test.ts +15 -13
  204. package/src/store/createSanityInstance.ts +14 -2
  205. package/src/store/createStateSourceAction.test.ts +64 -2
  206. package/src/store/createStateSourceAction.ts +38 -43
  207. package/src/store/createStoreInstance.test.ts +4 -2
  208. package/src/store/fetcherStore.test-d.ts +121 -0
  209. package/src/store/fetcherStore.test.ts +299 -0
  210. package/src/store/fetcherStore.ts +801 -0
  211. package/src/telemetry/environment.test.ts +119 -0
  212. package/src/telemetry/environment.ts +92 -0
  213. package/src/telemetry/events.ts +42 -0
  214. package/src/telemetry/initTelemetry.test.ts +432 -0
  215. package/src/telemetry/initTelemetry.ts +218 -0
  216. package/src/telemetry/telemetryManager.test.ts +327 -0
  217. package/src/telemetry/telemetryManager.ts +199 -0
  218. package/src/users/reducers.test.ts +3 -1
  219. package/src/users/reducers.ts +5 -6
  220. package/src/users/types.ts +4 -4
  221. package/src/users/usersStore.test.ts +16 -12
  222. package/src/users/usersStore.ts +5 -1
  223. package/src/utils/buildQuery.test.ts +14 -0
  224. package/src/utils/buildQuery.ts +17 -0
  225. package/src/utils/createFetcherStore.test.ts +7 -5
  226. package/src/utils/createFetcherStore.ts +8 -5
  227. package/src/utils/getStagingApiHost.test.ts +21 -0
  228. package/src/utils/getStagingApiHost.ts +14 -0
  229. package/src/utils/ids.test.ts +1 -29
  230. package/src/utils/ids.ts +0 -10
  231. package/src/utils/includeShape.ts +36 -0
  232. package/src/utils/isImportError.test.ts +72 -0
  233. package/src/utils/isImportError.ts +34 -0
  234. package/src/utils/logger.test.ts +12 -0
  235. package/src/utils/logger.ts +8 -3
  236. package/src/utils/object.test.ts +95 -0
  237. package/src/utils/object.ts +142 -0
  238. package/src/utils/setCleanupTimeout.ts +37 -0
  239. package/src/document/processActions.ts +0 -707
  240. package/src/utils/defineIntent.test.ts +0 -477
  241. package/src/utils/defineIntent.ts +0 -244
@@ -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, identity} = 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, identity)) {
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,37 @@
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, identity} = 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
+ identity,
23
+ notFoundMessage: `Cannot edit release "${action.releaseId}" because it does not exist.`,
24
+ permissionMessage: `You do not have permission to edit release "${action.releaseId}".`,
25
+ })
26
+
27
+ outgoingMutations.push(...result.workingMutations)
28
+ outgoingActions.push(
29
+ ...result.diffedPatches.map((patch) => ({
30
+ actionType: 'sanity.action.release.edit' as const,
31
+ releaseId: action.releaseId,
32
+ patch,
33
+ })),
34
+ )
35
+
36
+ return {base: result.base, working: result.working}
37
+ }
@@ -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, identity} = 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, identity)) {
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, identity} = 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, identity)) {
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, identity} = 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, identity)) {
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
+ }
@@ -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
+ }