@sanity/sdk 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/_chunks-dts/utils.d.ts +171 -19
  2. package/dist/_chunks-es/_internal.js +41 -26
  3. package/dist/_chunks-es/_internal.js.map +1 -1
  4. package/dist/_chunks-es/createGroqSearchFilter.js +25 -9
  5. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  6. package/dist/_chunks-es/telemetryManager.js +25 -19
  7. package/dist/_chunks-es/telemetryManager.js.map +1 -1
  8. package/dist/_chunks-es/version.js +1 -1
  9. package/dist/_exports/_internal.d.ts +27 -11
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +723 -418
  12. package/dist/index.js.map +1 -1
  13. package/package.json +16 -16
  14. package/src/_exports/index.ts +23 -2
  15. package/src/auth/refreshStampedToken.test.ts +2 -2
  16. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +116 -0
  17. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +27 -9
  18. package/src/config/sanityConfig.ts +12 -0
  19. package/src/document/actions.test.ts +112 -1
  20. package/src/document/actions.ts +148 -1
  21. package/src/document/applyDocumentActions.ts +4 -3
  22. package/src/document/documentStore.ts +7 -6
  23. package/src/document/events.test.ts +57 -2
  24. package/src/document/events.ts +43 -24
  25. package/src/document/permissions.ts +1 -1
  26. package/src/document/processActions/create.ts +135 -0
  27. package/src/document/processActions/delete.ts +100 -0
  28. package/src/document/processActions/discard.ts +63 -0
  29. package/src/document/processActions/edit.ts +141 -0
  30. package/src/document/processActions/processActions.ts +209 -0
  31. package/src/document/processActions/publish.ts +120 -0
  32. package/src/document/processActions/releaseArchive.ts +77 -0
  33. package/src/document/processActions/releaseCreate.ts +59 -0
  34. package/src/document/processActions/releaseDelete.ts +65 -0
  35. package/src/document/processActions/releaseEdit.ts +36 -0
  36. package/src/document/processActions/releasePublish.ts +45 -0
  37. package/src/document/processActions/releaseSchedule.ts +87 -0
  38. package/src/document/processActions/releaseUtil.ts +31 -0
  39. package/src/document/processActions/shared.ts +139 -0
  40. package/src/document/processActions/unpublish.ts +85 -0
  41. package/src/document/processActions.test.ts +424 -2
  42. package/src/document/reducers.ts +41 -6
  43. package/src/releases/getPerspectiveState.test.ts +1 -1
  44. package/src/releases/releasesStore.test.ts +50 -1
  45. package/src/releases/releasesStore.ts +41 -18
  46. package/src/releases/utils/sortReleases.test.ts +2 -2
  47. package/src/releases/utils/sortReleases.ts +1 -1
  48. package/src/telemetry/environment.test.ts +119 -0
  49. package/src/telemetry/environment.ts +92 -0
  50. package/src/telemetry/{__telemetry__/sdk.telemetry.ts → events.ts} +9 -9
  51. package/src/telemetry/initTelemetry.test.ts +240 -16
  52. package/src/telemetry/initTelemetry.ts +39 -16
  53. package/src/telemetry/telemetryManager.test.ts +129 -65
  54. package/src/telemetry/telemetryManager.ts +41 -29
  55. package/src/document/processActions.ts +0 -735
  56. package/src/telemetry/devMode.test.ts +0 -60
  57. package/src/telemetry/devMode.ts +0 -41
@@ -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,139 @@
1
+ import {diffValue} from '@sanity/diff-patch'
2
+ import {type Mutation, type PatchOperations, type SanityDocument} from '@sanity/types'
3
+ import {evaluateSync, type ExprNode} from 'groq-js'
4
+
5
+ import {type Grant} from '../permissions'
6
+ import {type DocumentSet, processMutations} from '../processMutations'
7
+ import {type HttpAction} from '../reducers'
8
+
9
+ export interface ActionHandlerContext {
10
+ base: DocumentSet
11
+ working: DocumentSet
12
+ transactionId: string
13
+ timestamp: string
14
+ grants: Record<Grant, ExprNode>
15
+ outgoingActions: HttpAction[]
16
+ outgoingMutations: Mutation[]
17
+ }
18
+
19
+ export interface ActionHandlerResult {
20
+ base: DocumentSet
21
+ working: DocumentSet
22
+ }
23
+
24
+ export function checkGrant(grantExpr: ExprNode, document: SanityDocument): boolean {
25
+ const value = evaluateSync(grantExpr, {params: {document}})
26
+ return value.type === 'boolean' && value.data
27
+ }
28
+
29
+ interface ActionErrorOptions {
30
+ message: string
31
+ documentId: string
32
+ transactionId: string
33
+ }
34
+
35
+ /**
36
+ * Thrown when a precondition for an action failed.
37
+ */
38
+ export class ActionError extends Error implements ActionErrorOptions {
39
+ documentId!: string
40
+ transactionId!: string
41
+
42
+ constructor(options: ActionErrorOptions) {
43
+ super(options.message)
44
+ Object.assign(this, options)
45
+ }
46
+ }
47
+
48
+ export class PermissionActionError extends ActionError {}
49
+
50
+ interface ApplySingleDocPatchOptions {
51
+ base: DocumentSet
52
+ working: DocumentSet
53
+ documentId: string
54
+ patches: PatchOperations[] | undefined
55
+ transactionId: string
56
+ timestamp: string
57
+ grants: Record<Grant, ExprNode>
58
+ /**
59
+ * Error message thrown when the target document does not exist in either
60
+ * the base or working set.
61
+ */
62
+ notFoundMessage?: string
63
+ /**
64
+ * Error message thrown when the working document fails the `update` grant.
65
+ */
66
+ permissionMessage?: string
67
+ }
68
+
69
+ interface ApplySingleDocPatchResult {
70
+ base: DocumentSet
71
+ working: DocumentSet
72
+ /**
73
+ * Patch operations representing the minimal diff between base before and
74
+ * after the user's patches were applied. These are the patches that should
75
+ * be sent to the server (or applied to the working set as mutations).
76
+ */
77
+ diffedPatches: PatchOperations[]
78
+ /**
79
+ * Mutation envelopes for `diffedPatches` already keyed to `documentId`.
80
+ * Useful for callers that want to push them to `outgoingMutations`.
81
+ */
82
+ workingMutations: Mutation[]
83
+ }
84
+
85
+ /**
86
+ * Shared logic for applying user-provided patches to a single document that
87
+ * is identified by an exact ID (no draft/published wrapping). Used by the
88
+ * liveEdit branch of `document.edit` and by `release.edit`.
89
+ *
90
+ * Returns the updated base + working sets, plus the diffed patches in both
91
+ * raw and mutation form so the caller can decide what to send to the server.
92
+ */
93
+ export function applySingleDocPatch({
94
+ base: initialBase,
95
+ working: initialWorking,
96
+ documentId,
97
+ patches,
98
+ transactionId,
99
+ timestamp,
100
+ grants,
101
+ notFoundMessage = 'Cannot edit document because it does not exist.',
102
+ permissionMessage = `You do not have permission to edit document "${documentId}".`,
103
+ }: ApplySingleDocPatchOptions): ApplySingleDocPatchResult {
104
+ let base = initialBase
105
+ let working = initialWorking
106
+
107
+ const userPatches = patches?.map((patch) => ({patch: {id: documentId, ...patch}}))
108
+
109
+ if (!userPatches?.length) {
110
+ return {base, working, diffedPatches: [], workingMutations: []}
111
+ }
112
+
113
+ if (!working[documentId] || !base[documentId]) {
114
+ throw new ActionError({documentId, transactionId, message: notFoundMessage})
115
+ }
116
+
117
+ const baseBefore = base[documentId]
118
+ base = processMutations({documents: base, transactionId, mutations: userPatches, timestamp})
119
+ const baseAfter = base[documentId]
120
+ const diffedPatches = diffValue(baseBefore, baseAfter) as PatchOperations[]
121
+
122
+ const workingBefore = working[documentId] as SanityDocument
123
+ if (!checkGrant(grants.update, workingBefore)) {
124
+ throw new PermissionActionError({documentId, transactionId, message: permissionMessage})
125
+ }
126
+
127
+ const workingMutations: Mutation[] = diffedPatches.map((patch) => ({
128
+ patch: {id: documentId, ...patch},
129
+ }))
130
+
131
+ working = processMutations({
132
+ documents: working,
133
+ transactionId,
134
+ mutations: workingMutations,
135
+ timestamp,
136
+ })
137
+
138
+ return {base, working, diffedPatches, workingMutations}
139
+ }
@@ -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, 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)) {
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)) {
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
+ }
@@ -2,8 +2,8 @@ import {type CreateMutation, type Reference, type SanityDocument} from '@sanity/
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'
5
+ import {type Action, type DocumentAction} from './actions'
6
+ import {ActionError, processActions} from './processActions/processActions'
7
7
  import {type DocumentSet} from './processMutations'
8
8
 
9
9
  // Helper: Create a sample document that conforms to SanityDocument.
@@ -1420,4 +1420,426 @@ describe('processActions', () => {
1420
1420
  })
1421
1421
  })
1422
1422
  })
1423
+
1424
+ describe('release actions', () => {
1425
+ const releaseName = 'my-release'
1426
+ const releaseDocId = `_.releases.${releaseName}`
1427
+
1428
+ const createReleaseDoc = (overrides: Partial<SanityDocument> = {}): SanityDocument => ({
1429
+ _id: releaseDocId,
1430
+ _type: 'system.release',
1431
+ _createdAt: '2025-01-01T00:00:00.000Z',
1432
+ _updatedAt: '2025-01-01T00:00:00.000Z',
1433
+ _rev: 'initial',
1434
+ name: releaseName,
1435
+ state: 'active',
1436
+ metadata: {releaseType: 'undecided'},
1437
+ ...overrides,
1438
+ })
1439
+
1440
+ describe('release.create', () => {
1441
+ it('inserts an optimistic release doc and emits a release.create action', () => {
1442
+ const base: DocumentSet = {}
1443
+ const working: DocumentSet = {}
1444
+ const actions: Action[] = [
1445
+ {
1446
+ type: 'release.create',
1447
+ releaseId: releaseName,
1448
+ metadata: {title: 'My release', releaseType: 'asap'},
1449
+ },
1450
+ ]
1451
+
1452
+ const result = processActions({
1453
+ actions,
1454
+ transactionId,
1455
+ base,
1456
+ working,
1457
+ timestamp,
1458
+ grants: defaultGrants,
1459
+ })
1460
+
1461
+ const doc = result.working[releaseDocId]
1462
+ expect(doc).toBeDefined()
1463
+ expect(doc?._type).toBe('system.release')
1464
+ expect(doc?.['name']).toBe(releaseName)
1465
+ expect(doc?.['state']).toBe('active')
1466
+ expect(doc?.['metadata']).toEqual({title: 'My release', releaseType: 'asap'})
1467
+
1468
+ expect(result.outgoingActions).toEqual([
1469
+ {
1470
+ actionType: 'sanity.action.release.create',
1471
+ releaseId: releaseName,
1472
+ metadata: {title: 'My release', releaseType: 'asap'},
1473
+ },
1474
+ ])
1475
+ })
1476
+
1477
+ it('throws if the release already exists in base or working', () => {
1478
+ const existing = createReleaseDoc()
1479
+ expect(() =>
1480
+ processActions({
1481
+ actions: [
1482
+ {
1483
+ type: 'release.create',
1484
+ releaseId: releaseName,
1485
+ metadata: {releaseType: 'undecided'},
1486
+ },
1487
+ ],
1488
+ transactionId,
1489
+ base: {[releaseDocId]: existing},
1490
+ working: {[releaseDocId]: existing},
1491
+ timestamp,
1492
+ grants: defaultGrants,
1493
+ }),
1494
+ ).toThrow(/already exists/)
1495
+ })
1496
+
1497
+ it('throws PermissionActionError when the create grant is denied', () => {
1498
+ expect(() =>
1499
+ processActions({
1500
+ actions: [
1501
+ {
1502
+ type: 'release.create',
1503
+ releaseId: releaseName,
1504
+ metadata: {releaseType: 'undecided'},
1505
+ },
1506
+ ],
1507
+ transactionId,
1508
+ base: {},
1509
+ working: {},
1510
+ timestamp,
1511
+ grants: {...defaultGrants, create: alwaysDeny},
1512
+ }),
1513
+ ).toThrow(/permission to create release/)
1514
+ })
1515
+ })
1516
+
1517
+ describe('release.edit', () => {
1518
+ it('applies the patch optimistically and forwards the original patch to the outgoing action', () => {
1519
+ const existing = createReleaseDoc({
1520
+ metadata: {releaseType: 'undecided'},
1521
+ } as Partial<SanityDocument>)
1522
+ const patch = {set: {'metadata.title': 'Updated title'}}
1523
+
1524
+ const result = processActions({
1525
+ actions: [{type: 'release.edit', releaseId: releaseName, patch}],
1526
+ transactionId,
1527
+ base: {[releaseDocId]: existing},
1528
+ working: {[releaseDocId]: existing},
1529
+ timestamp,
1530
+ grants: defaultGrants,
1531
+ })
1532
+
1533
+ const updated = result.working[releaseDocId] as SanityDocument & {
1534
+ metadata: {title?: string; releaseType: string}
1535
+ }
1536
+ expect(updated.metadata.title).toBe('Updated title')
1537
+ // releaseType wasn't unset
1538
+ expect(updated.metadata.releaseType).toBe('undecided')
1539
+
1540
+ expect(result.outgoingActions).toEqual([
1541
+ {
1542
+ actionType: 'sanity.action.release.edit',
1543
+ releaseId: releaseName,
1544
+ patch,
1545
+ },
1546
+ ])
1547
+ })
1548
+
1549
+ it('throws when editing a release that does not exist', () => {
1550
+ expect(() =>
1551
+ processActions({
1552
+ actions: [
1553
+ {
1554
+ type: 'release.edit',
1555
+ releaseId: releaseName,
1556
+ patch: {set: {'metadata.title': 'x'}},
1557
+ },
1558
+ ],
1559
+ transactionId,
1560
+ base: {},
1561
+ working: {},
1562
+ timestamp,
1563
+ grants: defaultGrants,
1564
+ }),
1565
+ ).toThrow(/does not exist/)
1566
+ })
1567
+
1568
+ it('throws PermissionActionError when the working release fails the update grant', () => {
1569
+ const existing = createReleaseDoc()
1570
+ expect(() =>
1571
+ processActions({
1572
+ actions: [
1573
+ {
1574
+ type: 'release.edit',
1575
+ releaseId: releaseName,
1576
+ patch: {set: {'metadata.title': 'x'}},
1577
+ },
1578
+ ],
1579
+ transactionId,
1580
+ base: {[releaseDocId]: existing},
1581
+ working: {[releaseDocId]: existing},
1582
+ timestamp,
1583
+ grants: {...defaultGrants, update: alwaysDeny},
1584
+ }),
1585
+ ).toThrow(/permission to edit release/)
1586
+ })
1587
+ })
1588
+
1589
+ describe('fire-and-forget actions', () => {
1590
+ const existing = createReleaseDoc()
1591
+ const cases: Array<{
1592
+ name: string
1593
+ action: Action
1594
+ expectedOutgoing: Record<string, unknown>
1595
+ }> = [
1596
+ {
1597
+ name: 'release.publish',
1598
+ action: {type: 'release.publish', releaseId: releaseName},
1599
+ expectedOutgoing: {actionType: 'sanity.action.release.publish', releaseId: releaseName},
1600
+ },
1601
+ {
1602
+ name: 'release.schedule',
1603
+ action: {
1604
+ type: 'release.schedule',
1605
+ releaseId: releaseName,
1606
+ publishAt: '2026-01-01T00:00:00.000Z',
1607
+ },
1608
+ expectedOutgoing: {
1609
+ actionType: 'sanity.action.release.schedule',
1610
+ releaseId: releaseName,
1611
+ publishAt: '2026-01-01T00:00:00.000Z',
1612
+ },
1613
+ },
1614
+ {
1615
+ name: 'release.unschedule',
1616
+ action: {type: 'release.unschedule', releaseId: releaseName},
1617
+ expectedOutgoing: {
1618
+ actionType: 'sanity.action.release.unschedule',
1619
+ releaseId: releaseName,
1620
+ },
1621
+ },
1622
+ {
1623
+ name: 'release.archive',
1624
+ action: {type: 'release.archive', releaseId: releaseName},
1625
+ expectedOutgoing: {actionType: 'sanity.action.release.archive', releaseId: releaseName},
1626
+ },
1627
+ {
1628
+ name: 'release.unarchive',
1629
+ action: {type: 'release.unarchive', releaseId: releaseName},
1630
+ expectedOutgoing: {actionType: 'sanity.action.release.unarchive', releaseId: releaseName},
1631
+ },
1632
+ ]
1633
+
1634
+ it.each(cases)(
1635
+ '$name does not mutate local state and emits the matching outgoing action',
1636
+ ({action, expectedOutgoing}) => {
1637
+ const base: DocumentSet = {[releaseDocId]: existing}
1638
+ const working: DocumentSet = {[releaseDocId]: existing}
1639
+
1640
+ const result = processActions({
1641
+ actions: [action],
1642
+ transactionId,
1643
+ base,
1644
+ working,
1645
+ timestamp,
1646
+ grants: defaultGrants,
1647
+ })
1648
+
1649
+ // working and base reference the same doc instance — no optimistic
1650
+ // mutation was applied
1651
+ expect(result.working[releaseDocId]).toBe(existing)
1652
+ expect(result.outgoingActions).toEqual([expectedOutgoing])
1653
+ expect(result.outgoingMutations).toEqual([])
1654
+ },
1655
+ )
1656
+
1657
+ it.each(cases)('$name throws if the release does not exist', ({action}) => {
1658
+ expect(() =>
1659
+ processActions({
1660
+ actions: [action],
1661
+ transactionId,
1662
+ base: {},
1663
+ working: {},
1664
+ timestamp,
1665
+ grants: defaultGrants,
1666
+ }),
1667
+ ).toThrow(ActionError)
1668
+ })
1669
+
1670
+ it.each(cases)(
1671
+ '$name throws PermissionActionError when update grant is denied',
1672
+ ({action}) => {
1673
+ const base: DocumentSet = {[releaseDocId]: existing}
1674
+ const working: DocumentSet = {[releaseDocId]: existing}
1675
+ expect(() =>
1676
+ processActions({
1677
+ actions: [action],
1678
+ transactionId,
1679
+ base,
1680
+ working,
1681
+ timestamp,
1682
+ grants: {...defaultGrants, update: alwaysDeny},
1683
+ }),
1684
+ ).toThrow(/permission to/)
1685
+ },
1686
+ )
1687
+ })
1688
+
1689
+ describe('release.schedule publishAt validation', () => {
1690
+ const existing = createReleaseDoc()
1691
+
1692
+ it.each(['not-a-date', '', '2026-13-40T99:99:99Z'])(
1693
+ 'throws ActionError when publishAt is not a valid timestamp (%s)',
1694
+ (publishAt) => {
1695
+ expect(() =>
1696
+ processActions({
1697
+ actions: [{type: 'release.schedule', releaseId: releaseName, publishAt}],
1698
+ transactionId,
1699
+ base: {[releaseDocId]: existing},
1700
+ working: {[releaseDocId]: existing},
1701
+ timestamp,
1702
+ grants: defaultGrants,
1703
+ }),
1704
+ ).toThrow(/must be a valid ISO 8601 timestamp/)
1705
+ },
1706
+ )
1707
+ })
1708
+
1709
+ describe('release.delete', () => {
1710
+ it.each(['archived', 'published'] as const)(
1711
+ 'optimistically removes the local release when state is %s',
1712
+ (state) => {
1713
+ const existing = createReleaseDoc({state} as Partial<SanityDocument>)
1714
+ const result = processActions({
1715
+ actions: [{type: 'release.delete', releaseId: releaseName}],
1716
+ transactionId,
1717
+ base: {[releaseDocId]: existing},
1718
+ working: {[releaseDocId]: existing},
1719
+ timestamp,
1720
+ grants: defaultGrants,
1721
+ })
1722
+
1723
+ // processMutations sets deleted entries to null
1724
+ expect(result.working[releaseDocId]).toBeNull()
1725
+ expect(result.outgoingMutations).toEqual([{delete: {id: releaseDocId}}])
1726
+ expect(result.outgoingActions).toEqual([
1727
+ {actionType: 'sanity.action.release.delete', releaseId: releaseName},
1728
+ ])
1729
+ },
1730
+ )
1731
+
1732
+ it.each(['active', 'scheduled', 'archiving', 'publishing'] as const)(
1733
+ 'throws when state is %s (server only accepts archived/published)',
1734
+ (state) => {
1735
+ const existing = createReleaseDoc({state} as Partial<SanityDocument>)
1736
+ expect(() =>
1737
+ processActions({
1738
+ actions: [{type: 'release.delete', releaseId: releaseName}],
1739
+ transactionId,
1740
+ base: {[releaseDocId]: existing},
1741
+ working: {[releaseDocId]: existing},
1742
+ timestamp,
1743
+ grants: defaultGrants,
1744
+ }),
1745
+ ).toThrow(/Archive it first/)
1746
+ },
1747
+ )
1748
+
1749
+ it('throws if the release does not exist', () => {
1750
+ expect(() =>
1751
+ processActions({
1752
+ actions: [{type: 'release.delete', releaseId: releaseName}],
1753
+ transactionId,
1754
+ base: {},
1755
+ working: {},
1756
+ timestamp,
1757
+ grants: defaultGrants,
1758
+ }),
1759
+ ).toThrow(/does not exist/)
1760
+ })
1761
+
1762
+ it('throws PermissionActionError when the update grant is denied', () => {
1763
+ const existing = createReleaseDoc({state: 'archived'} as Partial<SanityDocument>)
1764
+ expect(() =>
1765
+ processActions({
1766
+ actions: [{type: 'release.delete', releaseId: releaseName}],
1767
+ transactionId,
1768
+ base: {[releaseDocId]: existing},
1769
+ working: {[releaseDocId]: existing},
1770
+ timestamp,
1771
+ grants: {...defaultGrants, update: alwaysDeny},
1772
+ }),
1773
+ ).toThrow(/permission to delete release/)
1774
+ })
1775
+ })
1776
+
1777
+ describe('mixed with liveEdit document actions', () => {
1778
+ const liveEditDocId = 'live-edit-doc'
1779
+ const liveEditDoc = {
1780
+ _id: liveEditDocId,
1781
+ _type: 'liveEditType',
1782
+ _createdAt: '2025-01-01T00:00:00.000Z',
1783
+ _updatedAt: '2025-01-01T00:00:00.000Z',
1784
+ _rev: 'initial',
1785
+ }
1786
+ const liveEditAction = {
1787
+ type: 'document.edit',
1788
+ documentId: liveEditDocId,
1789
+ documentType: 'liveEditType',
1790
+ liveEdit: true,
1791
+ patches: [{set: {title: 'x'}}],
1792
+ } as const
1793
+
1794
+ it('throws ActionError when a transaction mixes a release action with a liveEdit document action', () => {
1795
+ expect(() =>
1796
+ processActions({
1797
+ actions: [
1798
+ {
1799
+ type: 'release.create',
1800
+ releaseId: releaseName,
1801
+ metadata: {releaseType: 'undecided'},
1802
+ },
1803
+ liveEditAction as unknown as Action,
1804
+ ],
1805
+ transactionId,
1806
+ base: {[liveEditDocId]: liveEditDoc},
1807
+ working: {[liveEditDocId]: liveEditDoc},
1808
+ timestamp,
1809
+ grants: defaultGrants,
1810
+ }),
1811
+ ).toThrow(/Cannot combine liveEdit document actions with other actions/)
1812
+ })
1813
+
1814
+ it('throws ActionError when a transaction mixes a non-liveEdit document action with a liveEdit document action', () => {
1815
+ const otherDocId = 'other-doc'
1816
+ const otherDoc = {
1817
+ _id: otherDocId,
1818
+ _type: 'someType',
1819
+ _createdAt: '2025-01-01T00:00:00.000Z',
1820
+ _updatedAt: '2025-01-01T00:00:00.000Z',
1821
+ _rev: 'initial',
1822
+ } as SanityDocument
1823
+
1824
+ expect(() =>
1825
+ processActions({
1826
+ actions: [
1827
+ {
1828
+ type: 'document.edit',
1829
+ documentId: otherDocId,
1830
+ documentType: 'someType',
1831
+ patches: [{set: {title: 'y'}}],
1832
+ },
1833
+ liveEditAction as unknown as Action,
1834
+ ],
1835
+ transactionId,
1836
+ base: {[liveEditDocId]: liveEditDoc, [otherDocId]: otherDoc},
1837
+ working: {[liveEditDocId]: liveEditDoc, [otherDocId]: otherDoc},
1838
+ timestamp,
1839
+ grants: defaultGrants,
1840
+ }),
1841
+ ).toThrow(/Cannot combine liveEdit document actions with other actions/)
1842
+ })
1843
+ })
1844
+ })
1423
1845
  })