@sanity/sdk 2.11.1 → 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 (47) 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 +15 -4
  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 +355 -75
  12. package/dist/index.js.map +1 -1
  13. package/package.json +8 -8
  14. package/src/_exports/index.ts +23 -2
  15. package/src/config/sanityConfig.ts +12 -0
  16. package/src/document/actions.test.ts +112 -1
  17. package/src/document/actions.ts +148 -1
  18. package/src/document/applyDocumentActions.ts +4 -3
  19. package/src/document/documentStore.ts +6 -5
  20. package/src/document/events.test.ts +57 -2
  21. package/src/document/events.ts +43 -24
  22. package/src/document/processActions/edit.ts +9 -44
  23. package/src/document/processActions/processActions.ts +44 -3
  24. package/src/document/processActions/releaseArchive.ts +77 -0
  25. package/src/document/processActions/releaseCreate.ts +59 -0
  26. package/src/document/processActions/releaseDelete.ts +65 -0
  27. package/src/document/processActions/releaseEdit.ts +36 -0
  28. package/src/document/processActions/releasePublish.ts +45 -0
  29. package/src/document/processActions/releaseSchedule.ts +87 -0
  30. package/src/document/processActions/releaseUtil.ts +31 -0
  31. package/src/document/processActions/shared.ts +94 -2
  32. package/src/document/processActions.test.ts +423 -1
  33. package/src/document/reducers.ts +40 -5
  34. package/src/releases/getPerspectiveState.test.ts +1 -1
  35. package/src/releases/releasesStore.test.ts +50 -1
  36. package/src/releases/releasesStore.ts +41 -18
  37. package/src/releases/utils/sortReleases.test.ts +2 -2
  38. package/src/releases/utils/sortReleases.ts +1 -1
  39. package/src/telemetry/environment.test.ts +119 -0
  40. package/src/telemetry/environment.ts +92 -0
  41. package/src/telemetry/{__telemetry__/sdk.telemetry.ts → events.ts} +9 -9
  42. package/src/telemetry/initTelemetry.test.ts +240 -16
  43. package/src/telemetry/initTelemetry.ts +39 -16
  44. package/src/telemetry/telemetryManager.test.ts +129 -65
  45. package/src/telemetry/telemetryManager.ts +41 -29
  46. package/src/telemetry/devMode.test.ts +0 -60
  47. package/src/telemetry/devMode.ts +0 -41
@@ -2,7 +2,7 @@ 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'
5
+ import {type Action, type DocumentAction} from './actions'
6
6
  import {ActionError, processActions} from './processActions/processActions'
7
7
  import {type DocumentSet} from './processMutations'
8
8
 
@@ -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
  })
@@ -7,11 +7,12 @@ import {type StoreContext} from '../store/defineStore'
7
7
  import {insecureRandomId} from '../utils/ids'
8
8
  import {omitProperty} from '../utils/object'
9
9
  import {setCleanupTimeout} from '../utils/setCleanupTimeout'
10
- import {type DocumentAction} from './actions'
10
+ import {type Action} from './actions'
11
11
  import {DOCUMENT_STATE_CLEAR_DELAY} from './documentConstants'
12
12
  import {type DocumentState, type DocumentStoreState} from './documentStore'
13
13
  import {type RemoteDocument} from './listen'
14
14
  import {ActionError, processActions} from './processActions/processActions'
15
+ import {getReleaseDocumentId, isReleaseAction} from './processActions/releaseUtil'
15
16
  import {type DocumentSet} from './processMutations'
16
17
 
17
18
  const EMPTY_REVISIONS: NonNullable<Required<DocumentState['unverifiedRevisions']>> = {}
@@ -33,6 +34,14 @@ type ActionMap = {
33
34
  delete: 'sanity.action.document.delete'
34
35
  edit: 'sanity.action.document.edit'
35
36
  publish: 'sanity.action.document.publish'
37
+ releaseCreate: 'sanity.action.release.create'
38
+ releaseEdit: 'sanity.action.release.edit'
39
+ releasePublish: 'sanity.action.release.publish'
40
+ releaseSchedule: 'sanity.action.release.schedule'
41
+ releaseUnschedule: 'sanity.action.release.unschedule'
42
+ releaseArchive: 'sanity.action.release.archive'
43
+ releaseUnarchive: 'sanity.action.release.unarchive'
44
+ releaseDelete: 'sanity.action.release.delete'
36
45
  }
37
46
 
38
47
  type OptimisticLock = {
@@ -40,6 +49,14 @@ type OptimisticLock = {
40
49
  ifPublishedRevisionId?: string
41
50
  }
42
51
 
52
+ interface ReleaseMetadataPayload {
53
+ title?: string
54
+ description?: string
55
+ intendedPublishAt?: string
56
+ releaseType?: 'asap' | 'scheduled' | 'undecided'
57
+ cardinality?: 'one' | 'many'
58
+ }
59
+
43
60
  export type HttpAction =
44
61
  | {actionType: ActionMap['create']; publishedId: string; attributes: SanityDocumentLike}
45
62
  | {actionType: ActionMap['discard']; versionId: string; purge?: boolean}
@@ -47,6 +64,18 @@ export type HttpAction =
47
64
  | {actionType: ActionMap['delete']; publishedId: string; includeDrafts?: string[]}
48
65
  | {actionType: ActionMap['edit']; draftId: string; publishedId: string; patch: PatchOperations}
49
66
  | ({actionType: ActionMap['publish']; draftId: string; publishedId: string} & OptimisticLock)
67
+ | {
68
+ actionType: ActionMap['releaseCreate']
69
+ releaseId: string
70
+ metadata?: ReleaseMetadataPayload
71
+ }
72
+ | {actionType: ActionMap['releaseEdit']; releaseId: string; patch: PatchOperations}
73
+ | {actionType: ActionMap['releasePublish']; releaseId: string}
74
+ | {actionType: ActionMap['releaseSchedule']; releaseId: string; publishAt: string}
75
+ | {actionType: ActionMap['releaseUnschedule']; releaseId: string}
76
+ | {actionType: ActionMap['releaseArchive']; releaseId: string}
77
+ | {actionType: ActionMap['releaseUnarchive']; releaseId: string}
78
+ | {actionType: ActionMap['releaseDelete']; releaseId: string}
50
79
 
51
80
  /**
52
81
  * Represents a transaction that is queued to be applied but has not yet been
@@ -63,7 +92,7 @@ export interface QueuedTransaction {
63
92
  * actions don't mention draft IDs and is meant to abstract away the draft
64
93
  * model from users.
65
94
  */
66
- actions: DocumentAction[]
95
+ actions: Action[]
67
96
  /**
68
97
  * An optional flag set to disable this transaction from being batched with
69
98
  * other transactions.
@@ -272,7 +301,9 @@ export function batchAppliedTransactions([curr, ...rest]: AppliedTransaction[]):
272
301
  if (next.disableBatching) return editAction
273
302
 
274
303
  // Don't batch a liveEdit edit with a non-liveEdit edit — they route to different APIs
275
- if (!!action.liveEdit !== !!next.actions[0]?.liveEdit) return editAction
304
+ const nextFirst = next.actions[0]
305
+ const nextLiveEdit = nextFirst && 'liveEdit' in nextFirst ? nextFirst.liveEdit : false
306
+ if (!!action.liveEdit !== !!nextLiveEdit) return editAction
276
307
 
277
308
  return {
278
309
  disableBatching: false,
@@ -586,9 +617,13 @@ export function manageSubscriberIds(
586
617
 
587
618
  // document handles are passed in via the public facing API, but we also need to
588
619
  // pull the correct document ids from action bodies, which have similar but not
589
- // identical shapes to the document handles.
590
- function getDocumentIdsFromHandleLikes(handles: DocumentHandleLike[]): string[] {
620
+ // identical shapes to the document handles. release actions also flow through
621
+ // here, and resolve to the underlying release document id.
622
+ function getDocumentIdsFromHandleLikes(handles: (DocumentHandleLike | Action)[]): string[] {
591
623
  return handles.flatMap((handle) => {
624
+ if ('type' in handle && isReleaseAction(handle)) {
625
+ return [getReleaseDocumentId(handle.releaseId)]
626
+ }
592
627
  const idsForDocument = []
593
628
  if (!handle.documentId) return []
594
629
  if (handle.liveEdit) {
@@ -1,3 +1,4 @@
1
+ import {type ReleaseDocument} from '@sanity/client'
1
2
  import {filter, firstValueFrom, of, Subject, take} from 'rxjs'
2
3
  import {describe, expect, it, vi} from 'vitest'
3
4
 
@@ -6,7 +7,6 @@ import {getQueryState} from '../query/queryStore'
6
7
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
7
8
  import {type StateSource} from '../store/createStateSourceAction'
8
9
  import {getPerspectiveState} from './getPerspectiveState'
9
- import {type ReleaseDocument} from './releasesStore'
10
10
 
11
11
  vi.mock('../query/queryStore')
12
12
 
@@ -1,10 +1,11 @@
1
+ import {type ReleaseDocument} from '@sanity/client'
1
2
  import {NEVER, Observable, type Observer, of, Subject} from 'rxjs'
2
3
  import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
3
4
 
4
5
  import {getQueryState, resolveQuery} from '../query/queryStore'
5
6
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
6
7
  import {type StateSource} from '../store/createStateSourceAction'
7
- import {getActiveReleasesState, type ReleaseDocument} from './releasesStore'
8
+ import {getActiveReleasesState, getAllReleasesState} from './releasesStore'
8
9
 
9
10
  // Mock dependencies
10
11
  vi.mock('../query/queryStore')
@@ -167,6 +168,54 @@ describe('releasesStore', () => {
167
168
  expect(state.getCurrent()).toEqual([])
168
169
  })
169
170
 
171
+ it('exposes archived/published releases through getAllReleasesState but filters them out of getActiveReleasesState', async () => {
172
+ const subject = new Subject<ReleaseDocument[]>()
173
+ vi.mocked(getQueryState).mockReturnValue({
174
+ subscribe: () => () => {},
175
+ getCurrent: () => undefined,
176
+ observable: subject.asObservable(),
177
+ } as StateSource<ReleaseDocument[] | undefined>)
178
+
179
+ const active = getActiveReleasesState(instance, {
180
+ resource: {projectId: 'test', dataset: 'test'},
181
+ })
182
+ const all = getAllReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
183
+
184
+ const releases: ReleaseDocument[] = [
185
+ {
186
+ _id: 'r-active',
187
+ _type: 'system.release',
188
+ name: 'r-active',
189
+ state: 'active',
190
+ metadata: {releaseType: 'asap'},
191
+ } as ReleaseDocument,
192
+ {
193
+ _id: 'r-archived',
194
+ _type: 'system.release',
195
+ name: 'r-archived',
196
+ state: 'archived',
197
+ metadata: {releaseType: 'asap'},
198
+ } as ReleaseDocument,
199
+ {
200
+ _id: 'r-published',
201
+ _type: 'system.release',
202
+ name: 'r-published',
203
+ state: 'published',
204
+ metadata: {releaseType: 'asap'},
205
+ } as ReleaseDocument,
206
+ ]
207
+
208
+ subject.next(releases)
209
+ await new Promise((resolve) => setTimeout(resolve, 0))
210
+
211
+ const activeNames = active.getCurrent()?.map((r) => r.name) ?? []
212
+ const allNames = all.getCurrent()?.map((r) => r.name) ?? []
213
+
214
+ expect(activeNames).toEqual(['r-active'])
215
+ expect(allNames).toEqual(expect.arrayContaining(['r-active', 'r-archived', 'r-published']))
216
+ expect(allNames).toHaveLength(3)
217
+ })
218
+
170
219
  it('should not crash when the releases query errors', async () => {
171
220
  const subject = new Subject<ReleaseDocument[]>()
172
221
  vi.mocked(getQueryState).mockReturnValue({