@sanity/sdk 2.11.1 → 2.13.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 +175 -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 +465 -131
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- 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.test.ts +24 -0
- package/src/document/applyDocumentActions.ts +17 -5
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.test.ts +69 -0
- package/src/document/documentStore.ts +42 -10
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +43 -24
- package/src/document/listen.ts +1 -1
- package/src/document/permissions.test.ts +79 -0
- package/src/document/permissions.ts +8 -7
- package/src/document/processActions/create.ts +7 -4
- package/src/document/processActions/delete.ts +4 -4
- package/src/document/processActions/discard.ts +2 -2
- package/src/document/processActions/edit.ts +13 -47
- package/src/document/processActions/processActions.ts +53 -3
- package/src/document/processActions/publish.ts +4 -4
- 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 +37 -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 +108 -4
- package/src/document/processActions/unpublish.ts +3 -3
- package/src/document/processActions.test.ts +423 -1
- package/src/document/reducers.ts +44 -8
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- 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
|
@@ -16,7 +16,7 @@ export function handleUnpublish(
|
|
|
16
16
|
action: UnpublishDocumentAction,
|
|
17
17
|
ctx: ActionHandlerContext,
|
|
18
18
|
): ActionHandlerResult {
|
|
19
|
-
const {transactionId, timestamp, grants, outgoingActions, outgoingMutations} = ctx
|
|
19
|
+
const {transactionId, timestamp, grants, identity, outgoingActions, outgoingMutations} = ctx
|
|
20
20
|
let {base, working} = ctx
|
|
21
21
|
|
|
22
22
|
const documentId = getId(action.documentId)
|
|
@@ -48,7 +48,7 @@ export function handleUnpublish(
|
|
|
48
48
|
{createIfNotExists: newDraftFromPublished},
|
|
49
49
|
]
|
|
50
50
|
|
|
51
|
-
if (!checkGrant(grants.update, sourceDoc)) {
|
|
51
|
+
if (!checkGrant(grants.update, sourceDoc, identity)) {
|
|
52
52
|
throw new PermissionActionError({
|
|
53
53
|
documentId,
|
|
54
54
|
transactionId,
|
|
@@ -56,7 +56,7 @@ export function handleUnpublish(
|
|
|
56
56
|
})
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
if (!working[draftId] && !checkGrant(grants.create, newDraftFromPublished)) {
|
|
59
|
+
if (!working[draftId] && !checkGrant(grants.create, newDraftFromPublished, identity)) {
|
|
60
60
|
throw new PermissionActionError({
|
|
61
61
|
documentId,
|
|
62
62
|
transactionId,
|
|
@@ -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,18 +7,19 @@ 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']>> = {}
|
|
18
19
|
|
|
19
20
|
export type SyncTransactionState = Pick<
|
|
20
21
|
DocumentStoreState,
|
|
21
|
-
'queued' | 'applied' | 'documentStates' | 'outgoing' | 'grants'
|
|
22
|
+
'queued' | 'applied' | 'documentStates' | 'outgoing' | 'grants' | 'identity'
|
|
22
23
|
>
|
|
23
24
|
|
|
24
25
|
type DocumentHandleLike = Pick<DocumentHandle, 'perspective'> & {
|
|
@@ -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.
|
|
@@ -203,6 +232,7 @@ export function applyFirstQueuedTransaction(prev: SyncTransactionState): SyncTra
|
|
|
203
232
|
base: working,
|
|
204
233
|
timestamp,
|
|
205
234
|
grants: prev.grants,
|
|
235
|
+
identity: prev.identity,
|
|
206
236
|
})
|
|
207
237
|
const applied: AppliedTransaction = {
|
|
208
238
|
...queued,
|
|
@@ -272,7 +302,9 @@ export function batchAppliedTransactions([curr, ...rest]: AppliedTransaction[]):
|
|
|
272
302
|
if (next.disableBatching) return editAction
|
|
273
303
|
|
|
274
304
|
// Don't batch a liveEdit edit with a non-liveEdit edit — they route to different APIs
|
|
275
|
-
|
|
305
|
+
const nextFirst = next.actions[0]
|
|
306
|
+
const nextLiveEdit = nextFirst && 'liveEdit' in nextFirst ? nextFirst.liveEdit : false
|
|
307
|
+
if (!!action.liveEdit !== !!nextLiveEdit) return editAction
|
|
276
308
|
|
|
277
309
|
return {
|
|
278
310
|
disableBatching: false,
|
|
@@ -368,7 +400,7 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
368
400
|
|
|
369
401
|
for (const t of prev.applied) {
|
|
370
402
|
try {
|
|
371
|
-
const next = processActions({...t, working, grants: prev.grants})
|
|
403
|
+
const next = processActions({...t, working, grants: prev.grants, identity: prev.identity})
|
|
372
404
|
working = next.working
|
|
373
405
|
nextApplied.push({...t, ...next})
|
|
374
406
|
} catch (error) {
|
|
@@ -475,7 +507,7 @@ export function applyRemoteDocument(
|
|
|
475
507
|
// transaction again through the listener and this same flow will run then
|
|
476
508
|
for (const curr of prev.applied) {
|
|
477
509
|
try {
|
|
478
|
-
const next = processActions({...curr, working, grants: prev.grants})
|
|
510
|
+
const next = processActions({...curr, working, grants: prev.grants, identity: prev.identity})
|
|
479
511
|
working = next.working
|
|
480
512
|
// next includes an updated `previous` set and `working` set and updates
|
|
481
513
|
// the `outgoingAction` and `outgoingMutations`. the `base` set from the
|
|
@@ -586,9 +618,13 @@ export function manageSubscriberIds(
|
|
|
586
618
|
|
|
587
619
|
// document handles are passed in via the public facing API, but we also need to
|
|
588
620
|
// pull the correct document ids from action bodies, which have similar but not
|
|
589
|
-
// identical shapes to the document handles.
|
|
590
|
-
|
|
621
|
+
// identical shapes to the document handles. release actions also flow through
|
|
622
|
+
// here, and resolve to the underlying release document id.
|
|
623
|
+
function getDocumentIdsFromHandleLikes(handles: (DocumentHandleLike | Action)[]): string[] {
|
|
591
624
|
return handles.flatMap((handle) => {
|
|
625
|
+
if ('type' in handle && isReleaseAction(handle)) {
|
|
626
|
+
return [getReleaseDocumentId(handle.releaseId)]
|
|
627
|
+
}
|
|
592
628
|
const idsForDocument = []
|
|
593
629
|
if (!handle.documentId) return []
|
|
594
630
|
if (handle.liveEdit) {
|