@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.
- package/dist/_chunks-dts/utils.d.ts +171 -19
- package/dist/_chunks-es/_internal.js +41 -26
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +15 -4
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/telemetryManager.js +25 -19
- package/dist/_chunks-es/telemetryManager.js.map +1 -1
- package/dist/_chunks-es/version.js +1 -1
- package/dist/_exports/_internal.d.ts +27 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +355 -75
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/_exports/index.ts +23 -2
- package/src/config/sanityConfig.ts +12 -0
- package/src/document/actions.test.ts +112 -1
- package/src/document/actions.ts +148 -1
- package/src/document/applyDocumentActions.ts +4 -3
- package/src/document/documentStore.ts +6 -5
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +43 -24
- package/src/document/processActions/edit.ts +9 -44
- package/src/document/processActions/processActions.ts +44 -3
- package/src/document/processActions/releaseArchive.ts +77 -0
- package/src/document/processActions/releaseCreate.ts +59 -0
- package/src/document/processActions/releaseDelete.ts +65 -0
- package/src/document/processActions/releaseEdit.ts +36 -0
- package/src/document/processActions/releasePublish.ts +45 -0
- package/src/document/processActions/releaseSchedule.ts +87 -0
- package/src/document/processActions/releaseUtil.ts +31 -0
- package/src/document/processActions/shared.ts +94 -2
- package/src/document/processActions.test.ts +423 -1
- package/src/document/reducers.ts +40 -5
- package/src/releases/getPerspectiveState.test.ts +1 -1
- package/src/releases/releasesStore.test.ts +50 -1
- package/src/releases/releasesStore.ts +41 -18
- package/src/releases/utils/sortReleases.test.ts +2 -2
- package/src/releases/utils/sortReleases.ts +1 -1
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/{__telemetry__/sdk.telemetry.ts → events.ts} +9 -9
- package/src/telemetry/initTelemetry.test.ts +240 -16
- package/src/telemetry/initTelemetry.ts +39 -16
- package/src/telemetry/telemetryManager.test.ts +129 -65
- package/src/telemetry/telemetryManager.ts +41 -29
- package/src/telemetry/devMode.test.ts +0 -60
- 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
|
})
|
package/src/document/reducers.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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({
|