@sanity/sdk 2.15.0 → 2.17.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 (41) hide show
  1. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +5 -4
  2. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -1
  3. package/dist/_chunks-es/createGroqSearchFilter.js +152 -148
  4. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  5. package/dist/_chunks-es/version.js +1 -1
  6. package/dist/index.d.ts +69 -4
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +79 -32
  9. package/dist/index.js.map +1 -1
  10. package/package.json +12 -12
  11. package/src/_exports/index.ts +2 -0
  12. package/src/auth/authStore.test.ts +1 -1
  13. package/src/auth/handleAuthCallback.test.ts +1 -1
  14. package/src/auth/refreshStampedToken.test.ts +199 -477
  15. package/src/auth/refreshStampedToken.ts +67 -195
  16. package/src/client/liveEvents.test.ts +151 -0
  17. package/src/client/liveEvents.ts +107 -0
  18. package/src/document/actions.ts +36 -0
  19. package/src/document/documentStore.test.ts +132 -1
  20. package/src/document/documentStore.ts +8 -0
  21. package/src/document/events.ts +32 -0
  22. package/src/document/listen.ts +8 -0
  23. package/src/document/processActions/edit.ts +19 -14
  24. package/src/document/processActions/shared.ts +55 -8
  25. package/src/document/processActions.test.ts +145 -0
  26. package/src/document/reducers.test.ts +396 -0
  27. package/src/document/reducers.ts +62 -1
  28. package/src/document/sharedListener.test.ts +1 -0
  29. package/src/document/sharedListener.ts +2 -0
  30. package/src/query/queryStore.test.ts +232 -38
  31. package/src/query/queryStore.ts +47 -47
  32. package/src/query/reducers.test.ts +0 -42
  33. package/src/query/reducers.ts +0 -16
  34. package/src/releases/getPerspectiveState.test.ts +3 -8
  35. package/src/releases/getPerspectiveState.ts +6 -19
  36. package/src/releases/observeReleases.test.ts +127 -0
  37. package/src/releases/observeReleases.ts +102 -0
  38. package/src/releases/releasesStore.test.ts +51 -53
  39. package/src/releases/releasesStore.ts +32 -32
  40. package/src/releases/utils/sortReleases.test.ts +36 -0
  41. package/src/releases/utils/sortReleases.ts +14 -9
@@ -41,7 +41,11 @@ import {
41
41
  resolvePermissions,
42
42
  subscribeDocumentEvents,
43
43
  } from './documentStore'
44
- import {type ActionErrorEvent, type TransactionRevertedEvent} from './events'
44
+ import {
45
+ type ActionErrorEvent,
46
+ type DocumentRemotePatchesEvent,
47
+ type TransactionRevertedEvent,
48
+ } from './events'
45
49
  import {DEFAULT_MAX_BUFFER_SIZE} from './listen'
46
50
  import {type DatasetAcl} from './permissions'
47
51
  import {type DocumentSet, processMutations} from './processMutations'
@@ -281,6 +285,66 @@ it('propagates changes between two instances', async () => {
281
285
  state2Unsubscribe()
282
286
  })
283
287
 
288
+ it('emits remote-patches events distinguishing own and foreign transactions', async () => {
289
+ const doc = createDocumentHandle({documentId: 'doc-remote-patches', documentType: 'article'})
290
+ const state1 = getDocumentState(instance1, doc)
291
+ const state2 = getDocumentState(instance2, doc)
292
+
293
+ const state1Unsubscribe = state1.subscribe()
294
+ const state2Unsubscribe = state2.subscribe()
295
+
296
+ await applyDocumentActions(instance1, {actions: [createDocument(doc)], resource: source1}).then(
297
+ (r) => r.submitted(),
298
+ )
299
+
300
+ const events1: DocumentRemotePatchesEvent[] = []
301
+ const events2: DocumentRemotePatchesEvent[] = []
302
+ const unsubscribeEvents1 = subscribeDocumentEvents(instance1, {
303
+ resource: source1,
304
+ eventHandler: (e) => {
305
+ if (e.type === 'remote-patches') events1.push(e)
306
+ },
307
+ })
308
+ const unsubscribeEvents2 = subscribeDocumentEvents(instance2, {
309
+ resource: source2,
310
+ eventHandler: (e) => {
311
+ if (e.type === 'remote-patches') events2.push(e)
312
+ },
313
+ })
314
+
315
+ // edit from instance2: an own transaction for instance2, a foreign one for instance1
316
+ const result = await applyDocumentActions(instance2, {
317
+ actions: [editDocument(doc, {set: {title: 'Hello world!'}})],
318
+ resource: source2,
319
+ }).then((r) => r.submitted())
320
+
321
+ const draftId = getDraftId(DocumentId(doc.documentId))
322
+
323
+ expect(events1).toHaveLength(1)
324
+ expect(events1[0]).toMatchObject({
325
+ type: 'remote-patches',
326
+ documentId: draftId,
327
+ transactionId: result.transactionId,
328
+ origin: 'remote',
329
+ })
330
+ expect(events1[0].patches).toEqual(
331
+ expect.arrayContaining([{set: expect.objectContaining({title: 'Hello world!'})}]),
332
+ )
333
+
334
+ expect(events2).toHaveLength(1)
335
+ expect(events2[0]).toMatchObject({
336
+ type: 'remote-patches',
337
+ documentId: draftId,
338
+ transactionId: result.transactionId,
339
+ origin: 'local',
340
+ })
341
+
342
+ unsubscribeEvents1()
343
+ unsubscribeEvents2()
344
+ state1Unsubscribe()
345
+ state2Unsubscribe()
346
+ })
347
+
284
348
  it('handles concurrent edits and resolves conflicts', async () => {
285
349
  const doc = createDocumentHandle({documentId: 'doc-concurrent', documentType: 'article'})
286
350
  const state1 = getDocumentState(instance1, doc)
@@ -325,6 +389,73 @@ it('handles concurrent edits and resolves conflicts', async () => {
325
389
  oneOffInstance.dispose()
326
390
  })
327
391
 
392
+ it('interleaves concurrent keyed-array edits when operations are preserved', async () => {
393
+ const doc = createDocumentHandle({documentId: 'doc-preserve-ops', documentType: 'article'})
394
+ const state1 = getDocumentState(instance1, doc)
395
+ const state2 = getDocumentState(instance2, doc)
396
+
397
+ const state1Unsubscribe = state1.subscribe()
398
+ const state2Unsubscribe = state2.subscribe()
399
+
400
+ const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
401
+ await applyDocumentActions(oneOffInstance, {
402
+ actions: [
403
+ createDocument(doc),
404
+ editDocument(doc, {
405
+ set: {
406
+ items: [
407
+ {_key: 'a', value: 'first'},
408
+ {_key: 'b', value: 'second'},
409
+ ],
410
+ },
411
+ }),
412
+ ],
413
+ resource,
414
+ }).then((r) => r.submitted())
415
+
416
+ // both instances insert into the same keyed array simultaneously, each
417
+ // preserving their operational patches instead of re-diffing snapshots
418
+ const p1 = applyDocumentActions(instance1, {
419
+ actions: [
420
+ editDocument(
421
+ doc,
422
+ {insert: {after: 'items[_key=="a"]', items: [{_key: 'c', value: 'from instance1'}]}},
423
+ {preserveOperations: true},
424
+ ),
425
+ ],
426
+ resource: source1,
427
+ }).then((r) => r.submitted())
428
+ const p2 = applyDocumentActions(instance2, {
429
+ actions: [
430
+ editDocument(
431
+ doc,
432
+ {insert: {after: 'items[_key=="b"]', items: [{_key: 'd', value: 'from instance2'}]}},
433
+ {preserveOperations: true},
434
+ ),
435
+ ],
436
+ resource: source2,
437
+ }).then((r) => r.submitted())
438
+
439
+ await Promise.allSettled([p1, p2])
440
+
441
+ const finalDoc1 = state1.getCurrent()
442
+ const finalDoc2 = state2.getCurrent()
443
+
444
+ // neither edit overwrote the other: both keyed inserts landed and both
445
+ // instances converged on the same interleaved result
446
+ expect(finalDoc1?.['items']).toEqual(finalDoc2?.['items'])
447
+ expect(finalDoc1?.['items']).toEqual([
448
+ {_key: 'a', value: 'first'},
449
+ {_key: 'c', value: 'from instance1'},
450
+ {_key: 'b', value: 'second'},
451
+ {_key: 'd', value: 'from instance2'},
452
+ ])
453
+
454
+ state1Unsubscribe()
455
+ state2Unsubscribe()
456
+ oneOffInstance.dispose()
457
+ })
458
+
328
459
  it('unpublishes and discards a document', async () => {
329
460
  const documentId = DocumentId('doc-pub-unpub')
330
461
  const doc = createDocumentHandle({documentId, documentType: 'article'})
@@ -133,6 +133,14 @@ export interface DocumentState {
133
133
  * local transactions on top of this new remote.
134
134
  */
135
135
  unverifiedRevisions?: {[TTransactionId in string]?: UnverifiedDocumentRevision}
136
+ /**
137
+ * transaction IDs recently submitted by this client for this document,
138
+ * newest last. used to label `remote-patches` events with the correct
139
+ * `origin` even after the corresponding `unverifiedRevisions` entry has
140
+ * been pruned (e.g. by a sync event that raced the listener echo). capped
141
+ * to the most recent entries.
142
+ */
143
+ recentOwnTransactionIds?: string[]
136
144
  }
137
145
 
138
146
  export const documentStore = defineStore<DocumentStoreState, BoundResourceKey>({
@@ -1,4 +1,5 @@
1
1
  import {type MultipleMutationResult, type SanityClient} from '@sanity/client'
2
+ import {type PatchOperations} from '@sanity/types'
2
3
 
3
4
  import {type DocumentAction, type ReleaseAction} from './actions'
4
5
  import {getReleaseDocumentId, isReleaseAction} from './processActions/releaseUtil'
@@ -21,6 +22,7 @@ export type DocumentEvent =
21
22
  | DocumentPublishedEvent
22
23
  | DocumentUnpublishedEvent
23
24
  | DocumentDiscardedEvent
25
+ | DocumentRemotePatchesEvent
24
26
 
25
27
  /**
26
28
  * @beta
@@ -119,6 +121,36 @@ export interface DocumentDiscardedEvent {
119
121
  outgoing: OutgoingTransaction
120
122
  }
121
123
 
124
+ /**
125
+ * @beta
126
+ * Event emitted when patches for a document are observed through the listener.
127
+ *
128
+ * Unlike the whole-document snapshots the store keeps, these are the raw patch
129
+ * operations from the transaction that produced the change, preserving the
130
+ * operational intent of the edit (e.g. keyed array inserts/unsets). Consumers
131
+ * that maintain their own local state, like collaborative text editors, can
132
+ * apply these directly instead of diffing document snapshots.
133
+ *
134
+ * `origin` is `'local'` when the transaction was submitted by this document
135
+ * store instance (an own edit observed back through the listener) and
136
+ * `'remote'` when it came from another client.
137
+ */
138
+ export interface DocumentRemotePatchesEvent {
139
+ type: 'remote-patches'
140
+ /** The ID of the document version the patches apply to (e.g. a draft ID). */
141
+ documentId: string
142
+ /** The transaction ID that carried these patches. */
143
+ transactionId: string
144
+ /** The revision the document was at before this transaction. */
145
+ previousRev?: string
146
+ /** The timestamp of the transaction. */
147
+ timestamp: string
148
+ /** The patch operations, rooted at the document. */
149
+ patches: PatchOperations[]
150
+ /** Whether the transaction originated from this client or another one. */
151
+ origin: 'local' | 'remote'
152
+ }
153
+
122
154
  // Release actions that write a mutation to the local release doc map onto
123
155
  // the regular per-document events with `documentId = '_.releases.<releaseId>'`.
124
156
  // The other release actions (publish/schedule/unschedule/archive/unarchive)
@@ -30,6 +30,13 @@ export interface RemoteDocument {
30
30
  revision?: string
31
31
  previousRev?: string
32
32
  timestamp: string
33
+ /**
34
+ * The raw mutations from the listener event that produced this document.
35
+ * Only present for `'mutation'` events. These carry the operational intent
36
+ * of the remote change (e.g. keyed patches) before it is collapsed into a
37
+ * whole document snapshot.
38
+ */
39
+ mutations?: Mutation[]
33
40
  }
34
41
 
35
42
  export interface SyncEvent {
@@ -251,6 +258,7 @@ export const listen = (
251
258
  document: document ?? null,
252
259
  revision: transactionId,
253
260
  timestamp,
261
+ mutations: next.mutations as Mutation[],
254
262
  ...(previousRev && {previousRev}),
255
263
  }
256
264
  }),
@@ -4,13 +4,14 @@ import {type Mutation, type PatchOperations, type SanityDocument} from '@sanity/
4
4
 
5
5
  import {isReleasePerspective} from '../../releases/utils/isReleasePerspective'
6
6
  import {type EditDocumentAction} from '../actions'
7
- import {getId, processMutations} from '../processMutations'
7
+ import {getId} from '../processMutations'
8
8
  import {
9
9
  ActionError,
10
10
  type ActionHandlerContext,
11
11
  type ActionHandlerResult,
12
12
  applySingleDocPatch,
13
13
  checkGrant,
14
+ createMutationApplier,
14
15
  PermissionActionError,
15
16
  } from './shared'
16
17
 
@@ -33,6 +34,7 @@ export function handleEdit(
33
34
  timestamp,
34
35
  grants,
35
36
  identity,
37
+ preserveOperations: action.preserveOperations,
36
38
  })
37
39
  // liveEdit documents use the mutation endpoint directly -- we don't send actions
38
40
  outgoingMutations.push(...result.workingMutations)
@@ -71,6 +73,13 @@ export function handleEdit(
71
73
  })
72
74
  }
73
75
 
76
+ const applyMutations = createMutationApplier({
77
+ documentId,
78
+ transactionId,
79
+ timestamp,
80
+ preserveOperations: action.preserveOperations,
81
+ })
82
+
74
83
  const baseMutations: Mutation[] = []
75
84
  // don't create a draft from the published version in a release perspective
76
85
  if (!isReleasePerspective(action.perspective) && !base[draftId] && base[publishedId]) {
@@ -84,16 +93,17 @@ export function handleEdit(
84
93
  baseMutations.push(...userPatches)
85
94
  }
86
95
 
87
- base = processMutations({
88
- documents: base,
89
- transactionId,
90
- mutations: baseMutations,
91
- timestamp,
92
- })
96
+ base = applyMutations(base, baseMutations, 'base')
93
97
  // this one will always be defined because a patch mutation will never
94
98
  // delete an input document
95
99
  const baseAfter = base[patchDocumentId] as SanityDocument
96
- const patches = diffValue(baseBefore, baseAfter)
100
+ // when the caller asked us to preserve their operations, forward their
101
+ // patches verbatim instead of re-deriving them from a snapshot diff. this
102
+ // keeps keyed array operations addressable so concurrent edits from other
103
+ // clients interleave instead of being overwritten
104
+ const patches = action.preserveOperations
105
+ ? (action.patches ?? [])
106
+ : diffValue(baseBefore, baseAfter)
97
107
 
98
108
  const workingMutations: Mutation[] = []
99
109
  if (!isReleasePerspective(action.perspective) && !working[draftId] && working[publishedId]) {
@@ -121,12 +131,7 @@ export function handleEdit(
121
131
  }
122
132
  workingMutations.push(...patches.map((patch) => ({patch: {id: patchDocumentId, ...patch}})))
123
133
 
124
- working = processMutations({
125
- documents: working,
126
- transactionId,
127
- mutations: workingMutations,
128
- timestamp,
129
- })
134
+ working = applyMutations(working, workingMutations, 'working')
130
135
 
131
136
  outgoingMutations.push(...workingMutations)
132
137
  outgoingActions.push(
@@ -57,6 +57,41 @@ export class ActionError extends Error implements ActionErrorOptions {
57
57
 
58
58
  export class PermissionActionError extends ActionError {}
59
59
 
60
+ /**
61
+ * Creates a `processMutations` wrapper that surfaces application failures as
62
+ * `ActionError`s when the caller asked to preserve their patch operations.
63
+ * With preserved operations, patches can legitimately fail to apply (e.g.
64
+ * re-applied onto a diverged document during a rebase), so wrapping lets a
65
+ * rebase skip the transaction instead of failing the store. Without
66
+ * `preserveOperations`, errors are rethrown untouched.
67
+ */
68
+ export function createMutationApplier(options: {
69
+ documentId: string
70
+ transactionId: string
71
+ timestamp: string
72
+ preserveOperations: boolean | undefined
73
+ }): (
74
+ documents: DocumentSet,
75
+ mutations: Mutation[],
76
+ documentSetName: 'base' | 'working',
77
+ ) => DocumentSet {
78
+ const {documentId, transactionId, timestamp, preserveOperations} = options
79
+ return (documents, mutations, documentSetName) => {
80
+ try {
81
+ return processMutations({documents, transactionId, mutations, timestamp})
82
+ } catch (error) {
83
+ if (!preserveOperations) throw error
84
+ throw new ActionError({
85
+ documentId,
86
+ transactionId,
87
+ message: `Failed to apply patches to the ${documentSetName} document: ${
88
+ error instanceof Error ? error.message : 'Unknown error'
89
+ }`,
90
+ })
91
+ }
92
+ }
93
+ }
94
+
60
95
  interface ApplySingleDocPatchOptions {
61
96
  base: DocumentSet
62
97
  working: DocumentSet
@@ -75,6 +110,13 @@ interface ApplySingleDocPatchOptions {
75
110
  * Error message thrown when the working document fails the `update` grant.
76
111
  */
77
112
  permissionMessage?: string
113
+ /**
114
+ * When `true`, the given patches are used verbatim instead of being
115
+ * re-derived by diffing the base document before and after application.
116
+ * Patch application failures are surfaced as `ActionError`s so a rebase
117
+ * can skip the transaction instead of failing the store.
118
+ */
119
+ preserveOperations?: boolean
78
120
  }
79
121
 
80
122
  interface ApplySingleDocPatchResult {
@@ -112,6 +154,7 @@ export function applySingleDocPatch({
112
154
  identity,
113
155
  notFoundMessage = 'Cannot edit document because it does not exist.',
114
156
  permissionMessage = `You do not have permission to edit document "${documentId}".`,
157
+ preserveOperations,
115
158
  }: ApplySingleDocPatchOptions): ApplySingleDocPatchResult {
116
159
  let base = initialBase
117
160
  let working = initialWorking
@@ -126,10 +169,19 @@ export function applySingleDocPatch({
126
169
  throw new ActionError({documentId, transactionId, message: notFoundMessage})
127
170
  }
128
171
 
172
+ const applyMutations = createMutationApplier({
173
+ documentId,
174
+ transactionId,
175
+ timestamp,
176
+ preserveOperations,
177
+ })
178
+
129
179
  const baseBefore = base[documentId]
130
- base = processMutations({documents: base, transactionId, mutations: userPatches, timestamp})
180
+ base = applyMutations(base, userPatches, 'base')
131
181
  const baseAfter = base[documentId]
132
- const diffedPatches = diffValue(baseBefore, baseAfter) as PatchOperations[]
182
+ const diffedPatches = preserveOperations
183
+ ? (patches as PatchOperations[])
184
+ : (diffValue(baseBefore, baseAfter) as PatchOperations[])
133
185
 
134
186
  const workingBefore = working[documentId] as SanityDocument
135
187
  if (!checkGrant(grants.update, workingBefore, identity)) {
@@ -140,12 +192,7 @@ export function applySingleDocPatch({
140
192
  patch: {id: documentId, ...patch},
141
193
  }))
142
194
 
143
- working = processMutations({
144
- documents: working,
145
- transactionId,
146
- mutations: workingMutations,
147
- timestamp,
148
- })
195
+ working = applyMutations(working, workingMutations, 'working')
149
196
 
150
197
  return {base, working, diffedPatches, workingMutations}
151
198
  }
@@ -527,6 +527,151 @@ describe('processActions', () => {
527
527
  })
528
528
  })
529
529
 
530
+ describe('document.edit with preserveOperations', () => {
531
+ const createKeyedDoc = (): SanityDocument => ({
532
+ _id: 'drafts.doc1',
533
+ _type: 'article',
534
+ _createdAt: '2025-01-01T00:00:00.000Z',
535
+ _updatedAt: '2025-01-01T00:00:00.000Z',
536
+ _rev: 'initial',
537
+ items: [
538
+ {_key: 'a', value: 'first'},
539
+ {_key: 'b', value: 'second'},
540
+ ],
541
+ })
542
+
543
+ it('forwards the given patches verbatim instead of re-diffing', () => {
544
+ const draft = createKeyedDoc()
545
+ const base: DocumentSet = {'drafts.doc1': draft}
546
+ const working: DocumentSet = {'drafts.doc1': draft}
547
+ const patches = [
548
+ {insert: {after: 'items[_key=="a"]', items: [{_key: 'c', value: 'in between'}]}},
549
+ ]
550
+ const actions: DocumentAction[] = [
551
+ {
552
+ documentId: 'doc1',
553
+ documentType: 'article',
554
+ type: 'document.edit',
555
+ patches,
556
+ preserveOperations: true,
557
+ },
558
+ ]
559
+ const result = processActions({
560
+ actions,
561
+ transactionId,
562
+ base,
563
+ working,
564
+ timestamp,
565
+ grants: defaultGrants,
566
+ })
567
+
568
+ // the outgoing action carries the caller's keyed patch untouched
569
+ expect(result.outgoingActions).toEqual([
570
+ {
571
+ actionType: 'sanity.action.document.edit',
572
+ draftId: 'drafts.doc1',
573
+ publishedId: 'doc1',
574
+ patch: patches[0],
575
+ },
576
+ ])
577
+
578
+ // and it still applies to the local working copy
579
+ expect(result.working['drafts.doc1']?.['items']).toEqual([
580
+ {_key: 'a', value: 'first'},
581
+ {_key: 'c', value: 'in between'},
582
+ {_key: 'b', value: 'second'},
583
+ ])
584
+ })
585
+
586
+ it('applies keyed unsets and sets to the local working copy', () => {
587
+ const draft = createKeyedDoc()
588
+ const base: DocumentSet = {'drafts.doc1': draft}
589
+ const working: DocumentSet = {'drafts.doc1': draft}
590
+ const actions: DocumentAction[] = [
591
+ {
592
+ documentId: 'doc1',
593
+ documentType: 'article',
594
+ type: 'document.edit',
595
+ patches: [
596
+ {unset: ['items[_key=="a"]']},
597
+ {set: {'items[_key=="b"].value': 'updated second'}},
598
+ ],
599
+ preserveOperations: true,
600
+ },
601
+ ]
602
+ const result = processActions({
603
+ actions,
604
+ transactionId,
605
+ base,
606
+ working,
607
+ timestamp,
608
+ grants: defaultGrants,
609
+ })
610
+
611
+ expect(result.working['drafts.doc1']?.['items']).toEqual([
612
+ {_key: 'b', value: 'updated second'},
613
+ ])
614
+ expect(result.outgoingActions.map((action) => 'patch' in action && action.patch)).toEqual([
615
+ {unset: ['items[_key=="a"]']},
616
+ {set: {'items[_key=="b"].value': 'updated second'}},
617
+ ])
618
+ })
619
+
620
+ it('surfaces patch application failures as ActionError', () => {
621
+ const draft = {...createKeyedDoc(), count: 5}
622
+ const base: DocumentSet = {'drafts.doc1': draft}
623
+ const working: DocumentSet = {'drafts.doc1': draft}
624
+ const actions: DocumentAction[] = [
625
+ {
626
+ documentId: 'doc1',
627
+ documentType: 'article',
628
+ type: 'document.edit',
629
+ // diff-match-patch on a non-string value cannot apply
630
+ patches: [{diffMatchPatch: {count: '@@ -1,3 +1,3 @@\n-foo\n+bar\n'}}],
631
+ preserveOperations: true,
632
+ },
633
+ ]
634
+ expect(() =>
635
+ processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
636
+ ).toThrow(ActionError)
637
+ expect(() =>
638
+ processActions({actions, transactionId, base, working, timestamp, grants: defaultGrants}),
639
+ ).toThrow(/Failed to apply patches to the base document/)
640
+ })
641
+
642
+ it('forwards patches verbatim through the liveEdit mutation path', () => {
643
+ const liveDoc = {...createKeyedDoc(), _id: 'live-doc'}
644
+ const base: DocumentSet = {'live-doc': liveDoc}
645
+ const working: DocumentSet = {'live-doc': liveDoc}
646
+ const patches = [{insert: {before: 'items[_key=="a"]', items: [{_key: 'z', value: 'start'}]}}]
647
+ const actions: DocumentAction[] = [
648
+ {
649
+ documentId: 'live-doc',
650
+ documentType: 'liveArticle',
651
+ type: 'document.edit',
652
+ liveEdit: true,
653
+ patches,
654
+ preserveOperations: true,
655
+ },
656
+ ]
657
+ const result = processActions({
658
+ actions,
659
+ transactionId,
660
+ base,
661
+ working,
662
+ timestamp,
663
+ grants: defaultGrants,
664
+ })
665
+
666
+ expect(result.outgoingMutations).toEqual([{patch: {id: 'live-doc', ...patches[0]}}])
667
+ expect(result.working['live-doc']?.['items']).toEqual([
668
+ {_key: 'z', value: 'start'},
669
+ {_key: 'a', value: 'first'},
670
+ {_key: 'b', value: 'second'},
671
+ ])
672
+ })
673
+ })
674
+
530
675
  describe('document.publish', () => {
531
676
  it('should publish a draft document', () => {
532
677
  const draft = createDoc('drafts.doc1', 'Draft Title', '1')