@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
@@ -1,10 +1,10 @@
1
- import {type Reference, type SanityDocument} from '@sanity/types'
1
+ import {type CreateMutation, type Reference, type SanityDocument} from '@sanity/types'
2
2
  import {parse} from 'groq-js'
3
3
  import {describe, expect, it} from 'vitest'
4
4
 
5
- import {type DocumentAction} from './actions'
6
- import {ActionError, processActions} from './processActions'
7
- import {type DocumentSet} from './processMutations'
5
+ import {type Action, type DocumentAction} from './actions'
6
+ import {ActionError, processActions} from './processActions/processActions'
7
+ import {type DocumentSet, processMutations} from './processMutations'
8
8
 
9
9
  // Helper: Create a sample document that conforms to SanityDocument.
10
10
  const createDoc = (id: string, title: string, rev: string = 'initial'): SanityDocument => ({
@@ -50,7 +50,6 @@ describe('processActions', () => {
50
50
  documentId: 'doc1',
51
51
  type: 'document.create',
52
52
  documentType: 'article',
53
- resource: {projectId: 'p', dataset: 'd'},
54
53
  },
55
54
  ]
56
55
  const result = processActions({
@@ -97,7 +96,6 @@ describe('processActions', () => {
97
96
  documentId: 'doc1',
98
97
  type: 'document.create',
99
98
  documentType: 'article',
100
- resource: {projectId: 'p', dataset: 'd'},
101
99
  },
102
100
  ]
103
101
  expect(() =>
@@ -115,7 +113,6 @@ describe('processActions', () => {
115
113
  documentId: 'doc1',
116
114
  type: 'document.create',
117
115
  documentType: 'article',
118
- resource: {projectId: 'p', dataset: 'd'},
119
116
  },
120
117
  ]
121
118
  const result = processActions({
@@ -144,7 +141,6 @@ describe('processActions', () => {
144
141
  documentId: 'doc1',
145
142
  type: 'document.create',
146
143
  documentType: 'article',
147
- resource: {projectId: 'p', dataset: 'd'},
148
144
  },
149
145
  ]
150
146
  const grants = {...defaultGrants, create: alwaysDeny}
@@ -166,7 +162,6 @@ describe('processActions', () => {
166
162
  author: 'John Doe',
167
163
  count: 42,
168
164
  },
169
- resource: {projectId: 'p', dataset: 'd'},
170
165
  },
171
166
  ]
172
167
  const result = processActions({
@@ -203,7 +198,6 @@ describe('processActions', () => {
203
198
  title: 'Overridden Title',
204
199
  newField: 'New Value',
205
200
  },
206
- resource: {projectId: 'p', dataset: 'd'},
207
201
  },
208
202
  ]
209
203
  const result = processActions({
@@ -233,7 +227,6 @@ describe('processActions', () => {
233
227
  type: 'document.create',
234
228
  documentType: 'article',
235
229
  initialValue: {},
236
- resource: {projectId: 'p', dataset: 'd'},
237
230
  },
238
231
  ]
239
232
  const result = processActions({
@@ -265,7 +258,6 @@ describe('processActions', () => {
265
258
  documentId: 'doc1',
266
259
  documentType: 'article',
267
260
  type: 'document.delete',
268
- resource: {projectId: 'p', dataset: 'd'},
269
261
  },
270
262
  ]
271
263
  const result = processActions({
@@ -302,7 +294,6 @@ describe('processActions', () => {
302
294
  documentId: 'doc1',
303
295
  documentType: 'article',
304
296
  type: 'document.delete',
305
- resource: {projectId: 'p', dataset: 'd'},
306
297
  },
307
298
  ]
308
299
  const result = processActions({
@@ -335,7 +326,6 @@ describe('processActions', () => {
335
326
  documentId: 'doc1',
336
327
  documentType: 'article',
337
328
  type: 'document.delete',
338
- resource: {projectId: 'p', dataset: 'd'},
339
329
  },
340
330
  ]
341
331
  const grants = {...defaultGrants, update: alwaysDeny}
@@ -355,7 +345,6 @@ describe('processActions', () => {
355
345
  documentId: 'doc1',
356
346
  documentType: 'article',
357
347
  type: 'document.discard',
358
- resource: {projectId: 'p', dataset: 'd'},
359
348
  },
360
349
  ]
361
350
  const result = processActions({
@@ -385,7 +374,6 @@ describe('processActions', () => {
385
374
  documentId: 'doc1',
386
375
  documentType: 'article',
387
376
  type: 'document.discard',
388
- resource: {projectId: 'p', dataset: 'd'},
389
377
  },
390
378
  ]
391
379
  const grants = {...defaultGrants, update: alwaysDeny}
@@ -406,7 +394,6 @@ describe('processActions', () => {
406
394
  documentType: 'article',
407
395
  type: 'document.edit',
408
396
  patches: [{set: {title: 'Edited Title'}}],
409
- resource: {projectId: 'p', dataset: 'd'},
410
397
  },
411
398
  ]
412
399
  const result = processActions({
@@ -439,7 +426,6 @@ describe('processActions', () => {
439
426
  documentType: 'article',
440
427
  type: 'document.edit',
441
428
  patches: [{set: {title: 'New Draft Title'}}],
442
- resource: {projectId: 'p', dataset: 'd'},
443
429
  },
444
430
  ]
445
431
  const result = processActions({
@@ -457,6 +443,190 @@ describe('processActions', () => {
457
443
  expect(result.outgoingActions[0].actionType).toBe('sanity.action.document.edit')
458
444
  })
459
445
 
446
+ it('preserves inc/dec operations instead of collapsing them into sets', () => {
447
+ const draft = {...createDoc('drafts.doc1', 'Title', '1'), count: 5, score: 10}
448
+ const base: DocumentSet = {'drafts.doc1': draft}
449
+ const working: DocumentSet = {'drafts.doc1': draft}
450
+ const actions: DocumentAction[] = [
451
+ {
452
+ documentId: 'doc1',
453
+ documentType: 'article',
454
+ type: 'document.edit',
455
+ patches: [{inc: {count: 1}, dec: {score: 3}}],
456
+ },
457
+ ]
458
+ const result = processActions({
459
+ actions,
460
+ transactionId,
461
+ base,
462
+ working,
463
+ timestamp,
464
+ grants: defaultGrants,
465
+ })
466
+
467
+ // the local working copy reflects the applied increments
468
+ expect(result.working['drafts.doc1']).toMatchObject({count: 6, score: 7})
469
+ // the outgoing patch keeps the operational inc/dec instead of a set.
470
+ // the setIfMissing seeds the base value in case the field was removed
471
+ // concurrently: the server runs setIfMissing before inc/dec, and inc on
472
+ // a missing path would otherwise fail the whole transaction
473
+ const patches = result.outgoingActions.map((action) => 'patch' in action && action.patch)
474
+ expect(patches).toEqual([
475
+ {setIfMissing: {count: 5, score: 10}, inc: {count: 1}, dec: {score: 3}},
476
+ ])
477
+ })
478
+
479
+ it('accumulates multiple inc/dec records on the same path into one operation', () => {
480
+ const draft = {...createDoc('drafts.doc1', 'Title', '1'), count: 5}
481
+ const base: DocumentSet = {'drafts.doc1': draft}
482
+ const working: DocumentSet = {'drafts.doc1': draft}
483
+ const actions: DocumentAction[] = [
484
+ {
485
+ documentId: 'doc1',
486
+ documentType: 'article',
487
+ type: 'document.edit',
488
+ patches: [{inc: {count: 1}}, {inc: {count: 2}}, {dec: {count: 4}}],
489
+ },
490
+ ]
491
+ const result = processActions({
492
+ actions,
493
+ transactionId,
494
+ base,
495
+ working,
496
+ timestamp,
497
+ grants: defaultGrants,
498
+ })
499
+
500
+ // 5 + 1 + 2 - 4 = 4 locally
501
+ expect(result.working['drafts.doc1']).toMatchObject({count: 4})
502
+ // the three records collapse into one accumulated dec (net -1) with the
503
+ // base value seeded for the concurrent-removal degradation path
504
+ const patches = result.outgoingActions.map((action) => 'patch' in action && action.patch)
505
+ expect(patches).toEqual([{setIfMissing: {count: 5}, dec: {count: 1}}])
506
+ })
507
+
508
+ it('keeps the diffed set when an inc is combined with a set of the same field', () => {
509
+ const draft = {...createDoc('drafts.doc1', 'Title', '1'), count: 5}
510
+ const base: DocumentSet = {'drafts.doc1': draft}
511
+ const working: DocumentSet = {'drafts.doc1': draft}
512
+ const actions: DocumentAction[] = [
513
+ {
514
+ documentId: 'doc1',
515
+ documentType: 'article',
516
+ type: 'document.edit',
517
+ // set to 100 first, then inc: the final value (101) is not
518
+ // explainable by the inc alone, so it must remain a set
519
+ patches: [{set: {count: 100}}, {inc: {count: 1}}],
520
+ },
521
+ ]
522
+ const result = processActions({
523
+ actions,
524
+ transactionId,
525
+ base,
526
+ working,
527
+ timestamp,
528
+ grants: defaultGrants,
529
+ })
530
+
531
+ expect(result.working['drafts.doc1']).toMatchObject({count: 101})
532
+ const patches = result.outgoingActions.map((action) => 'patch' in action && action.patch)
533
+ expect(patches).toEqual([{set: {count: 101}}])
534
+ })
535
+
536
+ it('preserves inc operations targeting keyed array items', () => {
537
+ const draft = {
538
+ ...createDoc('drafts.doc1', 'Title', '1'),
539
+ items: [{_key: 'itemA', qty: 2}],
540
+ }
541
+ const base: DocumentSet = {'drafts.doc1': draft}
542
+ const working: DocumentSet = {'drafts.doc1': draft}
543
+ const actions: DocumentAction[] = [
544
+ {
545
+ documentId: 'doc1',
546
+ documentType: 'article',
547
+ type: 'document.edit',
548
+ patches: [{inc: {'items[_key=="itemA"].qty': 3}}],
549
+ },
550
+ ]
551
+ const result = processActions({
552
+ actions,
553
+ transactionId,
554
+ base,
555
+ working,
556
+ timestamp,
557
+ grants: defaultGrants,
558
+ })
559
+
560
+ expect(result.working['drafts.doc1']?.['items']).toEqual([{_key: 'itemA', qty: 5}])
561
+ const patches = result.outgoingActions.map((action) => 'patch' in action && action.patch)
562
+ expect(patches).toEqual([
563
+ {setIfMissing: {'items[_key=="itemA"].qty': 2}, inc: {'items[_key=="itemA"].qty': 3}},
564
+ ])
565
+ })
566
+
567
+ it('preserves inc operations through the liveEdit mutation path', () => {
568
+ const liveDoc = {...createLiveEditDoc('live-doc', 'Title', '1'), count: 5}
569
+ const base: DocumentSet = {'live-doc': liveDoc}
570
+ const working: DocumentSet = {'live-doc': liveDoc}
571
+ const actions: DocumentAction[] = [
572
+ {
573
+ documentId: 'live-doc',
574
+ documentType: 'liveArticle',
575
+ type: 'document.edit',
576
+ liveEdit: true,
577
+ patches: [{inc: {count: 2}}],
578
+ },
579
+ ]
580
+ const result = processActions({
581
+ actions,
582
+ transactionId,
583
+ base,
584
+ working,
585
+ timestamp,
586
+ grants: defaultGrants,
587
+ })
588
+
589
+ expect(result.working['live-doc']).toMatchObject({count: 7})
590
+ expect(result.outgoingMutations).toEqual([
591
+ {patch: {id: 'live-doc', setIfMissing: {count: 5}, inc: {count: 2}}},
592
+ ])
593
+ })
594
+
595
+ it('degrades to the diffed set result when the inc target was removed concurrently', () => {
596
+ const draft = {...createDoc('drafts.doc1', 'Title', '1'), count: 5}
597
+ const base: DocumentSet = {'drafts.doc1': draft}
598
+ const working: DocumentSet = {'drafts.doc1': draft}
599
+ const actions: DocumentAction[] = [
600
+ {
601
+ documentId: 'doc1',
602
+ documentType: 'article',
603
+ type: 'document.edit',
604
+ patches: [{inc: {count: 1}}],
605
+ },
606
+ ]
607
+ const result = processActions({
608
+ actions,
609
+ transactionId,
610
+ base,
611
+ working,
612
+ timestamp,
613
+ grants: defaultGrants,
614
+ })
615
+ const patch = result.outgoingActions.map((action) => 'patch' in action && action.patch)[0]
616
+
617
+ // simulate the server applying the emitted patch to a document where
618
+ // another client removed the counter in the meantime: setIfMissing
619
+ // seeds the base value, then inc applies, matching what the collapsed
620
+ // set would have written (instead of inc failing the transaction)
621
+ const {count: _count, ...draftWithoutCount} = draft
622
+ const serverResult = processMutations({
623
+ documents: {'drafts.doc1': draftWithoutCount as SanityDocument},
624
+ mutations: [{patch: {id: 'drafts.doc1', ...(patch as object)}}],
625
+ transactionId: 'txn-server',
626
+ })
627
+ expect(serverResult['drafts.doc1']).toMatchObject({count: 6})
628
+ })
629
+
460
630
  it('should edit a document when base and working diverge', () => {
461
631
  const publishedBase = createDoc('doc1', 'Original Boring Title')
462
632
  const publishedWorking = createDoc('doc1', 'Local Boring Title')
@@ -468,7 +638,6 @@ describe('processActions', () => {
468
638
  documentType: 'article',
469
639
  type: 'document.edit',
470
640
  patches: [{set: {title: 'Original Cool Title'}}],
471
- resource: {projectId: 'p', dataset: 'd'},
472
641
  },
473
642
  ]
474
643
  const result = processActions({
@@ -498,7 +667,6 @@ describe('processActions', () => {
498
667
  documentType: 'article',
499
668
  type: 'document.edit',
500
669
  patches: [{set: {title: 'Should Fail'}}],
501
- resource: {projectId: 'p', dataset: 'd'},
502
670
  },
503
671
  ]
504
672
  expect(() =>
@@ -516,7 +684,6 @@ describe('processActions', () => {
516
684
  documentType: 'article',
517
685
  type: 'document.edit',
518
686
  patches: [{set: {title: 'Edited Title'}}],
519
- resource: {projectId: 'p', dataset: 'd'},
520
687
  },
521
688
  ]
522
689
  const grants = {...defaultGrants, create: alwaysDeny}
@@ -535,7 +702,6 @@ describe('processActions', () => {
535
702
  documentType: 'article',
536
703
  type: 'document.edit',
537
704
  patches: [{set: {title: 'New Title'}}],
538
- resource: {projectId: 'p', dataset: 'd'},
539
705
  },
540
706
  ]
541
707
  const grants = {...defaultGrants, update: alwaysDeny}
@@ -545,6 +711,151 @@ describe('processActions', () => {
545
711
  })
546
712
  })
547
713
 
714
+ describe('document.edit with preserveOperations', () => {
715
+ const createKeyedDoc = (): SanityDocument => ({
716
+ _id: 'drafts.doc1',
717
+ _type: 'article',
718
+ _createdAt: '2025-01-01T00:00:00.000Z',
719
+ _updatedAt: '2025-01-01T00:00:00.000Z',
720
+ _rev: 'initial',
721
+ items: [
722
+ {_key: 'a', value: 'first'},
723
+ {_key: 'b', value: 'second'},
724
+ ],
725
+ })
726
+
727
+ it('forwards the given patches verbatim instead of re-diffing', () => {
728
+ const draft = createKeyedDoc()
729
+ const base: DocumentSet = {'drafts.doc1': draft}
730
+ const working: DocumentSet = {'drafts.doc1': draft}
731
+ const patches = [
732
+ {insert: {after: 'items[_key=="a"]', items: [{_key: 'c', value: 'in between'}]}},
733
+ ]
734
+ const actions: DocumentAction[] = [
735
+ {
736
+ documentId: 'doc1',
737
+ documentType: 'article',
738
+ type: 'document.edit',
739
+ patches,
740
+ preserveOperations: true,
741
+ },
742
+ ]
743
+ const result = processActions({
744
+ actions,
745
+ transactionId,
746
+ base,
747
+ working,
748
+ timestamp,
749
+ grants: defaultGrants,
750
+ })
751
+
752
+ // the outgoing action carries the caller's keyed patch untouched
753
+ expect(result.outgoingActions).toEqual([
754
+ {
755
+ actionType: 'sanity.action.document.edit',
756
+ draftId: 'drafts.doc1',
757
+ publishedId: 'doc1',
758
+ patch: patches[0],
759
+ },
760
+ ])
761
+
762
+ // and it still applies to the local working copy
763
+ expect(result.working['drafts.doc1']?.['items']).toEqual([
764
+ {_key: 'a', value: 'first'},
765
+ {_key: 'c', value: 'in between'},
766
+ {_key: 'b', value: 'second'},
767
+ ])
768
+ })
769
+
770
+ it('applies keyed unsets and sets to the local working copy', () => {
771
+ const draft = createKeyedDoc()
772
+ const base: DocumentSet = {'drafts.doc1': draft}
773
+ const working: DocumentSet = {'drafts.doc1': draft}
774
+ const actions: DocumentAction[] = [
775
+ {
776
+ documentId: 'doc1',
777
+ documentType: 'article',
778
+ type: 'document.edit',
779
+ patches: [
780
+ {unset: ['items[_key=="a"]']},
781
+ {set: {'items[_key=="b"].value': 'updated second'}},
782
+ ],
783
+ preserveOperations: true,
784
+ },
785
+ ]
786
+ const result = processActions({
787
+ actions,
788
+ transactionId,
789
+ base,
790
+ working,
791
+ timestamp,
792
+ grants: defaultGrants,
793
+ })
794
+
795
+ expect(result.working['drafts.doc1']?.['items']).toEqual([
796
+ {_key: 'b', value: 'updated second'},
797
+ ])
798
+ expect(result.outgoingActions.map((action) => 'patch' in action && action.patch)).toEqual([
799
+ {unset: ['items[_key=="a"]']},
800
+ {set: {'items[_key=="b"].value': 'updated second'}},
801
+ ])
802
+ })
803
+
804
+ it('surfaces patch application failures as ActionError', () => {
805
+ const draft = {...createKeyedDoc(), count: 5}
806
+ const base: DocumentSet = {'drafts.doc1': draft}
807
+ const working: DocumentSet = {'drafts.doc1': draft}
808
+ const actions: DocumentAction[] = [
809
+ {
810
+ documentId: 'doc1',
811
+ documentType: 'article',
812
+ type: 'document.edit',
813
+ // diff-match-patch on a non-string value cannot apply
814
+ patches: [{diffMatchPatch: {count: '@@ -1,3 +1,3 @@\n-foo\n+bar\n'}}],
815
+ preserveOperations: true,
816
+ },
817
+ ]
818
+ expect(() =>
819
+ processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
820
+ ).toThrow(ActionError)
821
+ expect(() =>
822
+ processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
823
+ ).toThrow(/Failed to apply patches to the base document/)
824
+ })
825
+
826
+ it('forwards patches verbatim through the liveEdit mutation path', () => {
827
+ const liveDoc = {...createKeyedDoc(), _id: 'live-doc'}
828
+ const base: DocumentSet = {'live-doc': liveDoc}
829
+ const working: DocumentSet = {'live-doc': liveDoc}
830
+ const patches = [{insert: {before: 'items[_key=="a"]', items: [{_key: 'z', value: 'start'}]}}]
831
+ const actions: DocumentAction[] = [
832
+ {
833
+ documentId: 'live-doc',
834
+ documentType: 'liveArticle',
835
+ type: 'document.edit',
836
+ liveEdit: true,
837
+ patches,
838
+ preserveOperations: true,
839
+ },
840
+ ]
841
+ const result = processActions({
842
+ actions,
843
+ transactionId,
844
+ base,
845
+ working,
846
+ timestamp,
847
+ grants: defaultGrants,
848
+ })
849
+
850
+ expect(result.outgoingMutations).toEqual([{patch: {id: 'live-doc', ...patches[0]}}])
851
+ expect(result.working['live-doc']?.['items']).toEqual([
852
+ {_key: 'z', value: 'start'},
853
+ {_key: 'a', value: 'first'},
854
+ {_key: 'b', value: 'second'},
855
+ ])
856
+ })
857
+ })
858
+
548
859
  describe('document.publish', () => {
549
860
  it('should publish a draft document', () => {
550
861
  const draft = createDoc('drafts.doc1', 'Draft Title', '1')
@@ -555,7 +866,6 @@ describe('processActions', () => {
555
866
  documentId: 'doc1',
556
867
  documentType: 'article',
557
868
  type: 'document.publish',
558
- resource: {projectId: 'p', dataset: 'd'},
559
869
  },
560
870
  ]
561
871
  const result = processActions({
@@ -591,7 +901,6 @@ describe('processActions', () => {
591
901
  documentId: 'doc1',
592
902
  documentType: 'article',
593
903
  type: 'document.publish',
594
- resource: {projectId: 'p', dataset: 'd'},
595
904
  },
596
905
  ]
597
906
  expect(() =>
@@ -610,7 +919,6 @@ describe('processActions', () => {
610
919
  documentId: 'doc1',
611
920
  documentType: 'article',
612
921
  type: 'document.publish',
613
- resource: {projectId: 'p', dataset: 'd'},
614
922
  },
615
923
  ]
616
924
  expect(() =>
@@ -623,12 +931,7 @@ describe('processActions', () => {
623
931
  const base: DocumentSet = {'drafts.doc1': draft}
624
932
  const working: DocumentSet = {'drafts.doc1': draft}
625
933
  const actions: DocumentAction[] = [
626
- {
627
- documentId: 'doc1',
628
- documentType: 'article',
629
- type: 'document.publish',
630
- resource: {projectId: 'p', dataset: 'd'},
631
- },
934
+ {documentId: 'doc1', documentType: 'article', type: 'document.publish'},
632
935
  ]
633
936
  const grants = {...defaultGrants, update: alwaysDeny}
634
937
  expect(() =>
@@ -642,12 +945,7 @@ describe('processActions', () => {
642
945
  const base: DocumentSet = {'drafts.doc1': draft, 'doc1': published}
643
946
  const working: DocumentSet = {'drafts.doc1': draft, 'doc1': published}
644
947
  const actions: DocumentAction[] = [
645
- {
646
- documentId: 'doc1',
647
- documentType: 'article',
648
- type: 'document.publish',
649
- resource: {projectId: 'p', dataset: 'd'},
650
- },
948
+ {documentId: 'doc1', documentType: 'article', type: 'document.publish'},
651
949
  ]
652
950
  const grants = {
653
951
  ...defaultGrants,
@@ -666,12 +964,7 @@ describe('processActions', () => {
666
964
  const base: DocumentSet = {'drafts.doc1': draft}
667
965
  const working: DocumentSet = {'drafts.doc1': draft}
668
966
  const actions: DocumentAction[] = [
669
- {
670
- documentId: 'doc1',
671
- documentType: 'article',
672
- type: 'document.publish',
673
- resource: {projectId: 'p', dataset: 'd'},
674
- },
967
+ {documentId: 'doc1', documentType: 'article', type: 'document.publish'},
675
968
  ]
676
969
  const grants = {...defaultGrants, create: alwaysDeny}
677
970
  expect(() =>
@@ -707,7 +1000,6 @@ describe('processActions', () => {
707
1000
  documentId: 'doc1',
708
1001
  documentType: 'article',
709
1002
  type: 'document.publish',
710
- resource: {projectId: 'p', dataset: 'd'},
711
1003
  },
712
1004
  ]
713
1005
  const result = processActions({
@@ -740,7 +1032,6 @@ describe('processActions', () => {
740
1032
  documentId: 'doc1',
741
1033
  documentType: 'article',
742
1034
  type: 'document.unpublish',
743
- resource: {projectId: 'p', dataset: 'd'},
744
1035
  },
745
1036
  ]
746
1037
  const result = processActions({
@@ -775,7 +1066,6 @@ describe('processActions', () => {
775
1066
  documentId: 'doc1',
776
1067
  documentType: 'article',
777
1068
  type: 'document.unpublish',
778
- resource: {projectId: 'p', dataset: 'd'},
779
1069
  },
780
1070
  ]
781
1071
  expect(() =>
@@ -794,7 +1084,6 @@ describe('processActions', () => {
794
1084
  documentId: 'doc1',
795
1085
  documentType: 'article',
796
1086
  type: 'document.unpublish',
797
- resource: {projectId: 'p', dataset: 'd'},
798
1087
  },
799
1088
  ]
800
1089
  const result = processActions({
@@ -826,12 +1115,7 @@ describe('processActions', () => {
826
1115
  const base: DocumentSet = {doc1: published}
827
1116
  const working: DocumentSet = {doc1: published}
828
1117
  const actions: DocumentAction[] = [
829
- {
830
- documentId: 'doc1',
831
- documentType: 'article',
832
- type: 'document.unpublish',
833
- resource: {projectId: 'p', dataset: 'd'},
834
- },
1118
+ {documentId: 'doc1', documentType: 'article', type: 'document.unpublish'},
835
1119
  ]
836
1120
  const grants = {...defaultGrants, update: alwaysDeny}
837
1121
  expect(() =>
@@ -844,12 +1128,7 @@ describe('processActions', () => {
844
1128
  const base: DocumentSet = {doc1: published}
845
1129
  const working: DocumentSet = {doc1: published}
846
1130
  const actions: DocumentAction[] = [
847
- {
848
- documentId: 'doc1',
849
- documentType: 'article',
850
- type: 'document.unpublish',
851
- resource: {projectId: 'p', dataset: 'd'},
852
- },
1131
+ {documentId: 'doc1', documentType: 'article', type: 'document.unpublish'},
853
1132
  ]
854
1133
  const grants = {...defaultGrants, create: alwaysDeny}
855
1134
  expect(() =>
@@ -864,25 +1143,14 @@ describe('processActions', () => {
864
1143
  const base: DocumentSet = {doc1: published}
865
1144
  const working: DocumentSet = {doc1: published}
866
1145
  const actions: DocumentAction[] = [
867
- {
868
- documentId: 'doc1',
869
- documentType: 'article',
870
- type: 'document.create',
871
- resource: {projectId: 'p', dataset: 'd'},
872
- },
1146
+ {documentId: 'doc1', documentType: 'article', type: 'document.create'},
873
1147
  {
874
1148
  documentId: 'doc1',
875
1149
  documentType: 'article',
876
1150
  type: 'document.edit',
877
1151
  patches: [{set: {title: 'Edited Title'}}],
878
- resource: {projectId: 'p', dataset: 'd'},
879
- },
880
- {
881
- documentId: 'doc1',
882
- documentType: 'article',
883
- type: 'document.publish',
884
- resource: {projectId: 'p', dataset: 'd'},
885
1152
  },
1153
+ {documentId: 'doc1', documentType: 'article', type: 'document.publish'},
886
1154
  ]
887
1155
  const result = processActions({
888
1156
  actions,
@@ -928,7 +1196,6 @@ describe('processActions', () => {
928
1196
  type: 'document.create',
929
1197
  documentType: 'liveArticle',
930
1198
  liveEdit: true,
931
- resource: {projectId: 'p', dataset: 'd'},
932
1199
  },
933
1200
  ]
934
1201
 
@@ -947,38 +1214,63 @@ describe('processActions', () => {
947
1214
  expect(doc?._type).toBe('liveArticle')
948
1215
  expect(doc?._rev).toBe(transactionId)
949
1216
 
950
- // Should use document.create action, not version.create
951
- expect(result.outgoingActions).toHaveLength(1)
952
- const action = result.outgoingActions[0]
953
- expect(action.actionType).toBe('sanity.action.document.create')
954
- if ('attributes' in action && 'publishedId' in action) {
955
- expect(action.publishedId).toBe('live1')
956
- expect(action.attributes._id).toBe('live1')
957
- expect(action.attributes._type).toBe('liveArticle')
958
- } else {
959
- throw new Error('Expected action to have attributes and publishedId')
960
- }
1217
+ // Should not use actions
1218
+ expect(result.outgoingActions).toHaveLength(0)
1219
+ expect(result.outgoingMutations).toHaveLength(1)
1220
+ const mutation = result.outgoingMutations[0] as CreateMutation
1221
+ expect(mutation.create._id).toBe('live1')
1222
+ expect(mutation.create._type).toBe('liveArticle')
961
1223
  })
962
1224
 
963
- it('should throw an error if liveEdit document already exists', () => {
964
- const existingDoc = createLiveEditDoc('live1', 'Existing')
965
- const base: DocumentSet = {live1: existingDoc}
966
- const working: DocumentSet = {live1: existingDoc}
1225
+ it('should apply initialValue when creating a liveEdit document', () => {
1226
+ const base: DocumentSet = {}
1227
+ const working: DocumentSet = {}
967
1228
  const actions: DocumentAction[] = [
968
1229
  {
969
1230
  documentId: 'live1',
970
1231
  type: 'document.create',
971
1232
  documentType: 'liveArticle',
972
1233
  liveEdit: true,
973
- resource: {projectId: 'p', dataset: 'd'},
1234
+ initialValue: {title: 'Initial Title', count: 42},
974
1235
  },
975
1236
  ]
976
1237
 
977
- expect(() =>
978
- processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
979
- ).toThrow('This document already exists')
980
- })
981
- })
1238
+ const result = processActions({
1239
+ actions,
1240
+ transactionId,
1241
+ base,
1242
+ working,
1243
+ timestamp,
1244
+ grants: defaultGrants,
1245
+ })
1246
+
1247
+ const doc = result.working['live1']
1248
+ expect(doc?.['title']).toBe('Initial Title')
1249
+ expect(doc?.['count']).toBe(42)
1250
+
1251
+ const mutation = result.outgoingMutations[0] as CreateMutation
1252
+ expect(mutation.create['title']).toBe('Initial Title')
1253
+ expect(mutation.create['count']).toBe(42)
1254
+ })
1255
+
1256
+ it('should throw an error if liveEdit document already exists', () => {
1257
+ const existingDoc = createLiveEditDoc('live1', 'Existing')
1258
+ const base: DocumentSet = {live1: existingDoc}
1259
+ const working: DocumentSet = {live1: existingDoc}
1260
+ const actions: DocumentAction[] = [
1261
+ {
1262
+ documentId: 'live1',
1263
+ type: 'document.create',
1264
+ documentType: 'liveArticle',
1265
+ liveEdit: true,
1266
+ },
1267
+ ]
1268
+
1269
+ expect(() =>
1270
+ processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1271
+ ).toThrow('This document already exists')
1272
+ })
1273
+ })
982
1274
 
983
1275
  describe('document.edit', () => {
984
1276
  it('should edit a liveEdit document directly', () => {
@@ -992,7 +1284,6 @@ describe('processActions', () => {
992
1284
  documentType: 'liveArticle',
993
1285
  liveEdit: true,
994
1286
  patches: [{set: {title: 'Updated Title'}}],
995
- resource: {projectId: 'p', dataset: 'd'},
996
1287
  },
997
1288
  ]
998
1289
 
@@ -1010,12 +1301,15 @@ describe('processActions', () => {
1010
1301
  expect(editedDoc?.['title']).toBe('Updated Title')
1011
1302
  expect(editedDoc?._id).toBe('live1')
1012
1303
 
1013
- // Should use document.edit action with draftId prefixed (for server validation) but publishedId as the actual doc
1014
- expect(result.outgoingActions[0]).toMatchObject({
1015
- actionType: 'sanity.action.document.edit',
1016
- draftId: 'drafts.live1',
1017
- publishedId: 'live1',
1304
+ expect(result.outgoingMutations[0]).toEqual({
1305
+ patch: {
1306
+ id: 'live1',
1307
+ diffMatchPatch: {
1308
+ title: '@@ -1,12 +1,11 @@\n-Original\n+Updated\n Tit\n',
1309
+ },
1310
+ },
1018
1311
  })
1312
+ expect(result.outgoingActions).toHaveLength(0)
1019
1313
  })
1020
1314
 
1021
1315
  it('should throw an error if liveEdit document does not exist', () => {
@@ -1028,7 +1322,6 @@ describe('processActions', () => {
1028
1322
  documentType: 'liveArticle',
1029
1323
  liveEdit: true,
1030
1324
  patches: [{set: {title: 'New Title'}}],
1031
- resource: {projectId: 'p', dataset: 'd'},
1032
1325
  },
1033
1326
  ]
1034
1327
 
@@ -1049,7 +1342,6 @@ describe('processActions', () => {
1049
1342
  type: 'document.delete',
1050
1343
  documentType: 'liveArticle',
1051
1344
  liveEdit: true,
1052
- resource: {projectId: 'p', dataset: 'd'},
1053
1345
  },
1054
1346
  ]
1055
1347
 
@@ -1064,12 +1356,9 @@ describe('processActions', () => {
1064
1356
 
1065
1357
  expect(result.working['live1']).toBeNull()
1066
1358
 
1067
- expect(result.outgoingActions).toEqual([
1068
- {
1069
- actionType: 'sanity.action.document.delete',
1070
- publishedId: 'live1',
1071
- },
1072
- ])
1359
+ expect(result.outgoingActions).toHaveLength(0)
1360
+ expect(result.outgoingMutations).toHaveLength(1)
1361
+ expect(result.outgoingMutations[0]).toEqual({delete: {id: 'live1'}})
1073
1362
  })
1074
1363
 
1075
1364
  it('should throw an error if liveEdit document does not exist', () => {
@@ -1081,7 +1370,6 @@ describe('processActions', () => {
1081
1370
  type: 'document.delete',
1082
1371
  documentType: 'liveArticle',
1083
1372
  liveEdit: true,
1084
- resource: {projectId: 'p', dataset: 'd'},
1085
1373
  },
1086
1374
  ]
1087
1375
 
@@ -1102,16 +1390,12 @@ describe('processActions', () => {
1102
1390
  type: 'document.publish',
1103
1391
  documentType: 'liveArticle',
1104
1392
  liveEdit: true,
1105
- resource: {projectId: 'p', dataset: 'd'},
1106
1393
  },
1107
1394
  ]
1108
1395
 
1109
1396
  expect(() =>
1110
1397
  processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1111
- ).toThrow('Cannot publish liveEdit document')
1112
- expect(() =>
1113
- processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1114
- ).toThrow('LiveEdit documents do not support drafts or publishing')
1398
+ ).toThrow(/Publishing is not supported for liveEdit or version/)
1115
1399
  })
1116
1400
  })
1117
1401
 
@@ -1126,16 +1410,12 @@ describe('processActions', () => {
1126
1410
  type: 'document.unpublish',
1127
1411
  documentType: 'liveArticle',
1128
1412
  liveEdit: true,
1129
- resource: {projectId: 'p', dataset: 'd'},
1130
1413
  },
1131
1414
  ]
1132
1415
 
1133
1416
  expect(() =>
1134
1417
  processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1135
- ).toThrow('Cannot unpublish liveEdit document')
1136
- expect(() =>
1137
- processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1138
- ).toThrow('LiveEdit documents do not support drafts or publishing')
1418
+ ).toThrow(/Unpublishing is not supported for liveEdit or version/)
1139
1419
  })
1140
1420
  })
1141
1421
 
@@ -1150,17 +1430,745 @@ describe('processActions', () => {
1150
1430
  type: 'document.discard',
1151
1431
  documentType: 'liveArticle',
1152
1432
  liveEdit: true,
1153
- resource: {projectId: 'p', dataset: 'd'},
1154
1433
  },
1155
1434
  ]
1156
1435
 
1157
1436
  expect(() =>
1158
1437
  processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1159
- ).toThrow('Cannot discard changes for liveEdit document')
1438
+ ).toThrow('Cannot discard changes for liveEdit document "live1"')
1439
+
1160
1440
  expect(() =>
1161
1441
  processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1162
1442
  ).toThrow('LiveEdit documents do not support drafts')
1163
1443
  })
1164
1444
  })
1165
1445
  })
1446
+
1447
+ describe('release perspective (version) documents', () => {
1448
+ const releaseName = 'test-release'
1449
+ const perspective = {releaseName}
1450
+ const versionId = `versions.${releaseName}.doc1`
1451
+
1452
+ describe('document.create', () => {
1453
+ it('should create a version document for a release perspective', () => {
1454
+ const base: DocumentSet = {}
1455
+ const working: DocumentSet = {}
1456
+ const actions: DocumentAction[] = [
1457
+ {
1458
+ documentId: versionId,
1459
+ type: 'document.create',
1460
+ documentType: 'article',
1461
+ perspective,
1462
+ },
1463
+ ]
1464
+ const result = processActions({
1465
+ actions,
1466
+ transactionId,
1467
+ base,
1468
+ working,
1469
+ timestamp,
1470
+ grants: defaultGrants,
1471
+ })
1472
+
1473
+ const versionDoc = result.working[versionId]
1474
+ expect(versionDoc).toBeDefined()
1475
+ expect(versionDoc?._id).toBe(versionId)
1476
+ expect(versionDoc?._type).toBe('article')
1477
+ expect(versionDoc?._rev).toBe(transactionId)
1478
+
1479
+ expect(result.outgoingActions).toEqual([
1480
+ {
1481
+ actionType: 'sanity.action.document.version.create',
1482
+ publishedId: 'doc1',
1483
+ attributes: expect.objectContaining({_id: versionId, _type: 'article'}),
1484
+ },
1485
+ ])
1486
+ })
1487
+
1488
+ it('should create a version document using the published document as a base', () => {
1489
+ const published = createDoc('doc1', 'Published Title')
1490
+ const base: DocumentSet = {doc1: published}
1491
+ const working: DocumentSet = {doc1: published}
1492
+ const actions: DocumentAction[] = [
1493
+ {
1494
+ documentId: versionId,
1495
+ type: 'document.create',
1496
+ documentType: 'article',
1497
+ perspective,
1498
+ },
1499
+ ]
1500
+ const result = processActions({
1501
+ actions,
1502
+ transactionId,
1503
+ base,
1504
+ working,
1505
+ timestamp,
1506
+ grants: defaultGrants,
1507
+ })
1508
+
1509
+ const versionDoc = result.working[versionId]
1510
+ expect(versionDoc).toBeDefined()
1511
+ expect(versionDoc?._id).toBe(versionId)
1512
+ expect(versionDoc?.['title']).toBe('Published Title')
1513
+ })
1514
+
1515
+ it('should throw if a version document already exists', () => {
1516
+ const existingVersion = createDoc(versionId, 'Existing Version')
1517
+ const base: DocumentSet = {[versionId]: existingVersion}
1518
+ const working: DocumentSet = {[versionId]: existingVersion}
1519
+ const actions: DocumentAction[] = [
1520
+ {
1521
+ documentId: versionId,
1522
+ type: 'document.create',
1523
+ documentType: 'article',
1524
+ perspective,
1525
+ },
1526
+ ]
1527
+ expect(() =>
1528
+ processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1529
+ ).toThrow(/release version.*already exists/i)
1530
+ })
1531
+
1532
+ it('should create a version document with initial values', () => {
1533
+ const base: DocumentSet = {}
1534
+ const working: DocumentSet = {}
1535
+ const actions: DocumentAction[] = [
1536
+ {
1537
+ documentId: versionId,
1538
+ type: 'document.create',
1539
+ documentType: 'article',
1540
+ perspective,
1541
+ initialValue: {title: 'Release Version Title', count: 7},
1542
+ },
1543
+ ]
1544
+ const result = processActions({
1545
+ actions,
1546
+ transactionId,
1547
+ base,
1548
+ working,
1549
+ timestamp,
1550
+ grants: defaultGrants,
1551
+ })
1552
+
1553
+ const versionDoc = result.working[versionId]
1554
+ expect(versionDoc?.['title']).toBe('Release Version Title')
1555
+ expect(versionDoc?.['count']).toBe(7)
1556
+ })
1557
+
1558
+ it('should throw PermissionActionError if create permission is denied', () => {
1559
+ const base: DocumentSet = {}
1560
+ const working: DocumentSet = {}
1561
+ const actions: DocumentAction[] = [
1562
+ {
1563
+ documentId: versionId,
1564
+ type: 'document.create',
1565
+ documentType: 'article',
1566
+ perspective,
1567
+ },
1568
+ ]
1569
+ const grants = {...defaultGrants, create: alwaysDeny}
1570
+ expect(() =>
1571
+ processActions({actions, transactionId, base, working, timestamp, grants}),
1572
+ ).toThrow(/You do not have permission to create a release version/)
1573
+ })
1574
+ })
1575
+
1576
+ describe('document.edit', () => {
1577
+ it('should edit a version document directly', () => {
1578
+ const version = createDoc(versionId, 'Original Version Title')
1579
+ const base: DocumentSet = {[versionId]: version}
1580
+ const working: DocumentSet = {[versionId]: version}
1581
+ const actions: DocumentAction[] = [
1582
+ {
1583
+ documentId: versionId,
1584
+ type: 'document.edit',
1585
+ documentType: 'article',
1586
+ perspective,
1587
+ patches: [{set: {title: 'Updated Version Title'}}],
1588
+ },
1589
+ ]
1590
+ const result = processActions({
1591
+ actions,
1592
+ transactionId,
1593
+ base,
1594
+ working,
1595
+ timestamp,
1596
+ grants: defaultGrants,
1597
+ })
1598
+
1599
+ const editedDoc = result.working[versionId]
1600
+ expect(editedDoc?.['title']).toBe('Updated Version Title')
1601
+ expect(editedDoc?._id).toBe(versionId)
1602
+
1603
+ // outgoing action should use versionId as draftId and the base publishedId
1604
+ expect(result.outgoingActions[0]).toMatchObject({
1605
+ actionType: 'sanity.action.document.edit',
1606
+ draftId: versionId,
1607
+ publishedId: 'doc1',
1608
+ })
1609
+ })
1610
+
1611
+ it('should throw if the version document does not exist', () => {
1612
+ const base: DocumentSet = {}
1613
+ const working: DocumentSet = {}
1614
+ const actions: DocumentAction[] = [
1615
+ {
1616
+ documentId: versionId,
1617
+ type: 'document.edit',
1618
+ documentType: 'article',
1619
+ perspective,
1620
+ patches: [{set: {title: 'Should Fail'}}],
1621
+ },
1622
+ ]
1623
+ expect(() =>
1624
+ processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1625
+ ).toThrow(/does not exist in the release/)
1626
+ })
1627
+
1628
+ it('should throw PermissionActionError if update permission is denied', () => {
1629
+ const version = createDoc(versionId, 'Version Title')
1630
+ const base: DocumentSet = {[versionId]: version}
1631
+ const working: DocumentSet = {[versionId]: version}
1632
+ const actions: DocumentAction[] = [
1633
+ {
1634
+ documentId: versionId,
1635
+ type: 'document.edit',
1636
+ documentType: 'article',
1637
+ perspective,
1638
+ patches: [{set: {title: 'No Permission'}}],
1639
+ },
1640
+ ]
1641
+ const grants = {...defaultGrants, update: alwaysDeny}
1642
+ expect(() =>
1643
+ processActions({actions, transactionId, base, working, timestamp, grants}),
1644
+ ).toThrow(/You do not have permission to edit document/)
1645
+ })
1646
+ })
1647
+
1648
+ describe('document.discard', () => {
1649
+ it('should discard a version document', () => {
1650
+ const version = createDoc(versionId, 'Version Title')
1651
+ const base: DocumentSet = {[versionId]: version}
1652
+ const working: DocumentSet = {[versionId]: version}
1653
+ const actions: DocumentAction[] = [
1654
+ {
1655
+ documentId: versionId,
1656
+ type: 'document.discard',
1657
+ documentType: 'article',
1658
+ perspective,
1659
+ },
1660
+ ]
1661
+ const result = processActions({
1662
+ actions,
1663
+ transactionId,
1664
+ base,
1665
+ working,
1666
+ timestamp,
1667
+ grants: defaultGrants,
1668
+ })
1669
+
1670
+ expect(result.working[versionId]).toBeNull()
1671
+ expect(result.outgoingActions).toEqual([
1672
+ {
1673
+ actionType: 'sanity.action.document.version.discard',
1674
+ versionId,
1675
+ },
1676
+ ])
1677
+ })
1678
+
1679
+ it('should throw if there is no version document to discard', () => {
1680
+ const base: DocumentSet = {}
1681
+ const working: DocumentSet = {}
1682
+ const actions: DocumentAction[] = [
1683
+ {
1684
+ documentId: versionId,
1685
+ type: 'document.discard',
1686
+ documentType: 'article',
1687
+ perspective,
1688
+ },
1689
+ ]
1690
+ expect(() =>
1691
+ processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1692
+ ).toThrow(/no draft or version available to discard/)
1693
+ })
1694
+
1695
+ it('should throw PermissionActionError if update permission is denied', () => {
1696
+ const version = createDoc(versionId, 'Version Title')
1697
+ const base: DocumentSet = {[versionId]: version}
1698
+ const working: DocumentSet = {[versionId]: version}
1699
+ const actions: DocumentAction[] = [
1700
+ {
1701
+ documentId: versionId,
1702
+ type: 'document.discard',
1703
+ documentType: 'article',
1704
+ perspective,
1705
+ },
1706
+ ]
1707
+ const grants = {...defaultGrants, update: alwaysDeny}
1708
+ expect(() =>
1709
+ processActions({actions, transactionId, base, working, timestamp, grants}),
1710
+ ).toThrow(/You do not have permission to discard changes/)
1711
+ })
1712
+ })
1713
+
1714
+ describe('document.publish', () => {
1715
+ it('should throw for release perspective documents', () => {
1716
+ const version = createDoc(versionId, 'Version Title')
1717
+ const base: DocumentSet = {[versionId]: version}
1718
+ const working: DocumentSet = {[versionId]: version}
1719
+ const actions: DocumentAction[] = [
1720
+ {
1721
+ documentId: versionId,
1722
+ type: 'document.publish',
1723
+ documentType: 'article',
1724
+ perspective,
1725
+ },
1726
+ ]
1727
+ expect(() =>
1728
+ processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1729
+ ).toThrow(/Publishing is not supported for liveEdit or version/)
1730
+ })
1731
+ })
1732
+
1733
+ describe('document.delete', () => {
1734
+ it('should throw for release perspective documents', () => {
1735
+ const version = createDoc(versionId, 'Version Title')
1736
+ const base: DocumentSet = {[versionId]: version}
1737
+ const working: DocumentSet = {[versionId]: version}
1738
+ const actions: DocumentAction[] = [
1739
+ {
1740
+ documentId: versionId,
1741
+ type: 'document.delete',
1742
+ documentType: 'article',
1743
+ perspective,
1744
+ },
1745
+ ]
1746
+ expect(() =>
1747
+ processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
1748
+ ).toThrow(/Cannot delete a version document/)
1749
+ })
1750
+ })
1751
+ })
1752
+
1753
+ describe('release actions', () => {
1754
+ const releaseName = 'my-release'
1755
+ const releaseDocId = `_.releases.${releaseName}`
1756
+
1757
+ const createReleaseDoc = (overrides: Partial<SanityDocument> = {}): SanityDocument => ({
1758
+ _id: releaseDocId,
1759
+ _type: 'system.release',
1760
+ _createdAt: '2025-01-01T00:00:00.000Z',
1761
+ _updatedAt: '2025-01-01T00:00:00.000Z',
1762
+ _rev: 'initial',
1763
+ name: releaseName,
1764
+ state: 'active',
1765
+ metadata: {releaseType: 'undecided'},
1766
+ ...overrides,
1767
+ })
1768
+
1769
+ describe('release.create', () => {
1770
+ it('inserts an optimistic release doc and emits a release.create action', () => {
1771
+ const base: DocumentSet = {}
1772
+ const working: DocumentSet = {}
1773
+ const actions: Action[] = [
1774
+ {
1775
+ type: 'release.create',
1776
+ releaseId: releaseName,
1777
+ metadata: {title: 'My release', releaseType: 'asap'},
1778
+ },
1779
+ ]
1780
+
1781
+ const result = processActions({
1782
+ actions,
1783
+ transactionId,
1784
+ base,
1785
+ working,
1786
+ timestamp,
1787
+ grants: defaultGrants,
1788
+ })
1789
+
1790
+ const doc = result.working[releaseDocId]
1791
+ expect(doc).toBeDefined()
1792
+ expect(doc?._type).toBe('system.release')
1793
+ expect(doc?.['name']).toBe(releaseName)
1794
+ expect(doc?.['state']).toBe('active')
1795
+ expect(doc?.['metadata']).toEqual({title: 'My release', releaseType: 'asap'})
1796
+
1797
+ expect(result.outgoingActions).toEqual([
1798
+ {
1799
+ actionType: 'sanity.action.release.create',
1800
+ releaseId: releaseName,
1801
+ metadata: {title: 'My release', releaseType: 'asap'},
1802
+ },
1803
+ ])
1804
+ })
1805
+
1806
+ it('throws if the release already exists in base or working', () => {
1807
+ const existing = createReleaseDoc()
1808
+ expect(() =>
1809
+ processActions({
1810
+ actions: [
1811
+ {
1812
+ type: 'release.create',
1813
+ releaseId: releaseName,
1814
+ metadata: {releaseType: 'undecided'},
1815
+ },
1816
+ ],
1817
+ transactionId,
1818
+ base: {[releaseDocId]: existing},
1819
+ working: {[releaseDocId]: existing},
1820
+ timestamp,
1821
+ grants: defaultGrants,
1822
+ }),
1823
+ ).toThrow(/already exists/)
1824
+ })
1825
+
1826
+ it('throws PermissionActionError when the create grant is denied', () => {
1827
+ expect(() =>
1828
+ processActions({
1829
+ actions: [
1830
+ {
1831
+ type: 'release.create',
1832
+ releaseId: releaseName,
1833
+ metadata: {releaseType: 'undecided'},
1834
+ },
1835
+ ],
1836
+ transactionId,
1837
+ base: {},
1838
+ working: {},
1839
+ timestamp,
1840
+ grants: {...defaultGrants, create: alwaysDeny},
1841
+ }),
1842
+ ).toThrow(/permission to create release/)
1843
+ })
1844
+ })
1845
+
1846
+ describe('release.edit', () => {
1847
+ it('applies the patch optimistically and forwards the original patch to the outgoing action', () => {
1848
+ const existing = createReleaseDoc({
1849
+ metadata: {releaseType: 'undecided'},
1850
+ } as Partial<SanityDocument>)
1851
+ const patch = {set: {'metadata.title': 'Updated title'}}
1852
+
1853
+ const result = processActions({
1854
+ actions: [{type: 'release.edit', releaseId: releaseName, patch}],
1855
+ transactionId,
1856
+ base: {[releaseDocId]: existing},
1857
+ working: {[releaseDocId]: existing},
1858
+ timestamp,
1859
+ grants: defaultGrants,
1860
+ })
1861
+
1862
+ const updated = result.working[releaseDocId] as SanityDocument & {
1863
+ metadata: {title?: string; releaseType: string}
1864
+ }
1865
+ expect(updated.metadata.title).toBe('Updated title')
1866
+ // releaseType wasn't unset
1867
+ expect(updated.metadata.releaseType).toBe('undecided')
1868
+
1869
+ expect(result.outgoingActions).toEqual([
1870
+ {
1871
+ actionType: 'sanity.action.release.edit',
1872
+ releaseId: releaseName,
1873
+ patch,
1874
+ },
1875
+ ])
1876
+ })
1877
+
1878
+ it('throws when editing a release that does not exist', () => {
1879
+ expect(() =>
1880
+ processActions({
1881
+ actions: [
1882
+ {
1883
+ type: 'release.edit',
1884
+ releaseId: releaseName,
1885
+ patch: {set: {'metadata.title': 'x'}},
1886
+ },
1887
+ ],
1888
+ transactionId,
1889
+ base: {},
1890
+ working: {},
1891
+ timestamp,
1892
+ grants: defaultGrants,
1893
+ }),
1894
+ ).toThrow(/does not exist/)
1895
+ })
1896
+
1897
+ it('throws PermissionActionError when the working release fails the update grant', () => {
1898
+ const existing = createReleaseDoc()
1899
+ expect(() =>
1900
+ processActions({
1901
+ actions: [
1902
+ {
1903
+ type: 'release.edit',
1904
+ releaseId: releaseName,
1905
+ patch: {set: {'metadata.title': 'x'}},
1906
+ },
1907
+ ],
1908
+ transactionId,
1909
+ base: {[releaseDocId]: existing},
1910
+ working: {[releaseDocId]: existing},
1911
+ timestamp,
1912
+ grants: {...defaultGrants, update: alwaysDeny},
1913
+ }),
1914
+ ).toThrow(/permission to edit release/)
1915
+ })
1916
+ })
1917
+
1918
+ describe('fire-and-forget actions', () => {
1919
+ const existing = createReleaseDoc()
1920
+ const cases: Array<{
1921
+ name: string
1922
+ action: Action
1923
+ expectedOutgoing: Record<string, unknown>
1924
+ }> = [
1925
+ {
1926
+ name: 'release.publish',
1927
+ action: {type: 'release.publish', releaseId: releaseName},
1928
+ expectedOutgoing: {actionType: 'sanity.action.release.publish', releaseId: releaseName},
1929
+ },
1930
+ {
1931
+ name: 'release.schedule',
1932
+ action: {
1933
+ type: 'release.schedule',
1934
+ releaseId: releaseName,
1935
+ publishAt: '2026-01-01T00:00:00.000Z',
1936
+ },
1937
+ expectedOutgoing: {
1938
+ actionType: 'sanity.action.release.schedule',
1939
+ releaseId: releaseName,
1940
+ publishAt: '2026-01-01T00:00:00.000Z',
1941
+ },
1942
+ },
1943
+ {
1944
+ name: 'release.unschedule',
1945
+ action: {type: 'release.unschedule', releaseId: releaseName},
1946
+ expectedOutgoing: {
1947
+ actionType: 'sanity.action.release.unschedule',
1948
+ releaseId: releaseName,
1949
+ },
1950
+ },
1951
+ {
1952
+ name: 'release.archive',
1953
+ action: {type: 'release.archive', releaseId: releaseName},
1954
+ expectedOutgoing: {actionType: 'sanity.action.release.archive', releaseId: releaseName},
1955
+ },
1956
+ {
1957
+ name: 'release.unarchive',
1958
+ action: {type: 'release.unarchive', releaseId: releaseName},
1959
+ expectedOutgoing: {actionType: 'sanity.action.release.unarchive', releaseId: releaseName},
1960
+ },
1961
+ ]
1962
+
1963
+ it.each(cases)(
1964
+ '$name does not mutate local state and emits the matching outgoing action',
1965
+ ({action, expectedOutgoing}) => {
1966
+ const base: DocumentSet = {[releaseDocId]: existing}
1967
+ const working: DocumentSet = {[releaseDocId]: existing}
1968
+
1969
+ const result = processActions({
1970
+ actions: [action],
1971
+ transactionId,
1972
+ base,
1973
+ working,
1974
+ timestamp,
1975
+ grants: defaultGrants,
1976
+ })
1977
+
1978
+ // working and base reference the same doc instance — no optimistic
1979
+ // mutation was applied
1980
+ expect(result.working[releaseDocId]).toBe(existing)
1981
+ expect(result.outgoingActions).toEqual([expectedOutgoing])
1982
+ expect(result.outgoingMutations).toEqual([])
1983
+ },
1984
+ )
1985
+
1986
+ it.each(cases)('$name throws if the release does not exist', ({action}) => {
1987
+ expect(() =>
1988
+ processActions({
1989
+ actions: [action],
1990
+ transactionId,
1991
+ base: {},
1992
+ working: {},
1993
+ timestamp,
1994
+ grants: defaultGrants,
1995
+ }),
1996
+ ).toThrow(ActionError)
1997
+ })
1998
+
1999
+ it.each(cases)(
2000
+ '$name throws PermissionActionError when update grant is denied',
2001
+ ({action}) => {
2002
+ const base: DocumentSet = {[releaseDocId]: existing}
2003
+ const working: DocumentSet = {[releaseDocId]: existing}
2004
+ expect(() =>
2005
+ processActions({
2006
+ actions: [action],
2007
+ transactionId,
2008
+ base,
2009
+ working,
2010
+ timestamp,
2011
+ grants: {...defaultGrants, update: alwaysDeny},
2012
+ }),
2013
+ ).toThrow(/permission to/)
2014
+ },
2015
+ )
2016
+ })
2017
+
2018
+ describe('release.schedule publishAt validation', () => {
2019
+ const existing = createReleaseDoc()
2020
+
2021
+ it.each(['not-a-date', '', '2026-13-40T99:99:99Z'])(
2022
+ 'throws ActionError when publishAt is not a valid timestamp (%s)',
2023
+ (publishAt) => {
2024
+ expect(() =>
2025
+ processActions({
2026
+ actions: [{type: 'release.schedule', releaseId: releaseName, publishAt}],
2027
+ transactionId,
2028
+ base: {[releaseDocId]: existing},
2029
+ working: {[releaseDocId]: existing},
2030
+ timestamp,
2031
+ grants: defaultGrants,
2032
+ }),
2033
+ ).toThrow(/must be a valid ISO 8601 timestamp/)
2034
+ },
2035
+ )
2036
+ })
2037
+
2038
+ describe('release.delete', () => {
2039
+ it.each(['archived', 'published'] as const)(
2040
+ 'optimistically removes the local release when state is %s',
2041
+ (state) => {
2042
+ const existing = createReleaseDoc({state} as Partial<SanityDocument>)
2043
+ const result = processActions({
2044
+ actions: [{type: 'release.delete', releaseId: releaseName}],
2045
+ transactionId,
2046
+ base: {[releaseDocId]: existing},
2047
+ working: {[releaseDocId]: existing},
2048
+ timestamp,
2049
+ grants: defaultGrants,
2050
+ })
2051
+
2052
+ // processMutations sets deleted entries to null
2053
+ expect(result.working[releaseDocId]).toBeNull()
2054
+ expect(result.outgoingMutations).toEqual([{delete: {id: releaseDocId}}])
2055
+ expect(result.outgoingActions).toEqual([
2056
+ {actionType: 'sanity.action.release.delete', releaseId: releaseName},
2057
+ ])
2058
+ },
2059
+ )
2060
+
2061
+ it.each(['active', 'scheduled', 'archiving', 'publishing'] as const)(
2062
+ 'throws when state is %s (server only accepts archived/published)',
2063
+ (state) => {
2064
+ const existing = createReleaseDoc({state} as Partial<SanityDocument>)
2065
+ expect(() =>
2066
+ processActions({
2067
+ actions: [{type: 'release.delete', releaseId: releaseName}],
2068
+ transactionId,
2069
+ base: {[releaseDocId]: existing},
2070
+ working: {[releaseDocId]: existing},
2071
+ timestamp,
2072
+ grants: defaultGrants,
2073
+ }),
2074
+ ).toThrow(/Archive it first/)
2075
+ },
2076
+ )
2077
+
2078
+ it('throws if the release does not exist', () => {
2079
+ expect(() =>
2080
+ processActions({
2081
+ actions: [{type: 'release.delete', releaseId: releaseName}],
2082
+ transactionId,
2083
+ base: {},
2084
+ working: {},
2085
+ timestamp,
2086
+ grants: defaultGrants,
2087
+ }),
2088
+ ).toThrow(/does not exist/)
2089
+ })
2090
+
2091
+ it('throws PermissionActionError when the update grant is denied', () => {
2092
+ const existing = createReleaseDoc({state: 'archived'} as Partial<SanityDocument>)
2093
+ expect(() =>
2094
+ processActions({
2095
+ actions: [{type: 'release.delete', releaseId: releaseName}],
2096
+ transactionId,
2097
+ base: {[releaseDocId]: existing},
2098
+ working: {[releaseDocId]: existing},
2099
+ timestamp,
2100
+ grants: {...defaultGrants, update: alwaysDeny},
2101
+ }),
2102
+ ).toThrow(/permission to delete release/)
2103
+ })
2104
+ })
2105
+
2106
+ describe('mixed with liveEdit document actions', () => {
2107
+ const liveEditDocId = 'live-edit-doc'
2108
+ const liveEditDoc = {
2109
+ _id: liveEditDocId,
2110
+ _type: 'liveEditType',
2111
+ _createdAt: '2025-01-01T00:00:00.000Z',
2112
+ _updatedAt: '2025-01-01T00:00:00.000Z',
2113
+ _rev: 'initial',
2114
+ }
2115
+ const liveEditAction = {
2116
+ type: 'document.edit',
2117
+ documentId: liveEditDocId,
2118
+ documentType: 'liveEditType',
2119
+ liveEdit: true,
2120
+ patches: [{set: {title: 'x'}}],
2121
+ } as const
2122
+
2123
+ it('throws ActionError when a transaction mixes a release action with a liveEdit document action', () => {
2124
+ expect(() =>
2125
+ processActions({
2126
+ actions: [
2127
+ {
2128
+ type: 'release.create',
2129
+ releaseId: releaseName,
2130
+ metadata: {releaseType: 'undecided'},
2131
+ },
2132
+ liveEditAction as unknown as Action,
2133
+ ],
2134
+ transactionId,
2135
+ base: {[liveEditDocId]: liveEditDoc},
2136
+ working: {[liveEditDocId]: liveEditDoc},
2137
+ timestamp,
2138
+ grants: defaultGrants,
2139
+ }),
2140
+ ).toThrow(/Cannot combine liveEdit document actions with other actions/)
2141
+ })
2142
+
2143
+ it('throws ActionError when a transaction mixes a non-liveEdit document action with a liveEdit document action', () => {
2144
+ const otherDocId = 'other-doc'
2145
+ const otherDoc = {
2146
+ _id: otherDocId,
2147
+ _type: 'someType',
2148
+ _createdAt: '2025-01-01T00:00:00.000Z',
2149
+ _updatedAt: '2025-01-01T00:00:00.000Z',
2150
+ _rev: 'initial',
2151
+ } as SanityDocument
2152
+
2153
+ expect(() =>
2154
+ processActions({
2155
+ actions: [
2156
+ {
2157
+ type: 'document.edit',
2158
+ documentId: otherDocId,
2159
+ documentType: 'someType',
2160
+ patches: [{set: {title: 'y'}}],
2161
+ },
2162
+ liveEditAction as unknown as Action,
2163
+ ],
2164
+ transactionId,
2165
+ base: {[liveEditDocId]: liveEditDoc, [otherDocId]: otherDoc},
2166
+ working: {[liveEditDocId]: liveEditDoc, [otherDocId]: otherDoc},
2167
+ timestamp,
2168
+ grants: defaultGrants,
2169
+ }),
2170
+ ).toThrow(/Cannot combine liveEdit document actions with other actions/)
2171
+ })
2172
+ })
2173
+ })
1166
2174
  })