@sanity/sdk 2.4.0 → 2.5.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/index.d.ts +35 -90
  2. package/dist/index.js +237 -111
  3. package/dist/index.js.map +1 -1
  4. package/package.json +9 -8
  5. package/src/auth/authStore.test.ts +13 -13
  6. package/src/auth/refreshStampedToken.test.ts +16 -16
  7. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +6 -6
  8. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -4
  9. package/src/comlink/controller/actions/destroyController.test.ts +2 -2
  10. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +6 -6
  11. package/src/comlink/controller/actions/getOrCreateController.test.ts +5 -5
  12. package/src/comlink/controller/actions/getOrCreateController.ts +1 -1
  13. package/src/comlink/controller/actions/releaseChannel.test.ts +3 -2
  14. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -4
  15. package/src/comlink/node/actions/getOrCreateNode.test.ts +7 -7
  16. package/src/comlink/node/actions/releaseNode.test.ts +2 -2
  17. package/src/comlink/node/comlinkNodeStore.test.ts +4 -3
  18. package/src/config/sanityConfig.ts +8 -3
  19. package/src/document/actions.ts +11 -7
  20. package/src/document/applyDocumentActions.test.ts +9 -6
  21. package/src/document/applyDocumentActions.ts +9 -49
  22. package/src/document/documentStore.test.ts +128 -115
  23. package/src/document/documentStore.ts +40 -10
  24. package/src/document/permissions.test.ts +9 -9
  25. package/src/document/permissions.ts +17 -7
  26. package/src/document/processActions.test.ts +248 -0
  27. package/src/document/processActions.ts +173 -0
  28. package/src/document/reducers.ts +13 -6
  29. package/src/presence/presenceStore.ts +13 -7
  30. package/src/preview/previewStore.test.ts +10 -2
  31. package/src/preview/previewStore.ts +2 -1
  32. package/src/preview/subscribeToStateAndFetchBatches.test.ts +8 -5
  33. package/src/preview/subscribeToStateAndFetchBatches.ts +9 -3
  34. package/src/projection/projectionStore.test.ts +18 -2
  35. package/src/projection/projectionStore.ts +2 -1
  36. package/src/projection/subscribeToStateAndFetchBatches.test.ts +6 -5
  37. package/src/projection/subscribeToStateAndFetchBatches.ts +9 -3
  38. package/src/releases/getPerspectiveState.ts +2 -2
  39. package/src/releases/releasesStore.ts +10 -4
  40. package/src/store/createActionBinder.test.ts +8 -6
  41. package/src/store/createActionBinder.ts +44 -29
  42. package/src/store/createStateSourceAction.test.ts +12 -11
  43. package/src/store/createStateSourceAction.ts +6 -6
  44. package/src/store/createStoreInstance.test.ts +29 -16
  45. package/src/store/createStoreInstance.ts +6 -5
  46. package/src/store/defineStore.test.ts +1 -1
  47. package/src/store/defineStore.ts +12 -7
@@ -90,19 +90,23 @@ it('creates, edits, and publishes a document', async () => {
90
90
  const unsubscribe = documentState.subscribe()
91
91
 
92
92
  // Create a new document
93
- const {appeared} = await applyDocumentActions(instance, createDocument(doc))
93
+ const {appeared} = await applyDocumentActions(instance, {actions: [createDocument(doc)]})
94
94
  expect(appeared).toContain(getDraftId(doc.documentId))
95
95
 
96
96
  let currentDoc = documentState.getCurrent()
97
97
  expect(currentDoc?._id).toEqual(getDraftId(doc.documentId))
98
98
 
99
99
  // Edit the document – add a title
100
- await applyDocumentActions(instance, editDocument(doc, {set: {title: 'My First Article'}}))
100
+ await applyDocumentActions(instance, {
101
+ actions: [editDocument(doc, {set: {title: 'My First Article'}})],
102
+ })
101
103
  currentDoc = documentState.getCurrent()
102
104
  expect(currentDoc?.title).toEqual('My First Article')
103
105
 
104
106
  // Publish the document; the resulting transactionId is used as the new _rev
105
- const {transactionId, submitted} = await applyDocumentActions(instance, publishDocument(doc))
107
+ const {transactionId, submitted} = await applyDocumentActions(instance, {
108
+ actions: [publishDocument(doc)],
109
+ })
106
110
  await submitted()
107
111
  currentDoc = documentState.getCurrent()
108
112
 
@@ -119,14 +123,15 @@ it('creates a document with initial values', async () => {
119
123
  const unsubscribe = documentState.subscribe()
120
124
 
121
125
  // Create a new document with initial field values
122
- const {appeared} = await applyDocumentActions(
123
- instance,
124
- createDocument(doc, {
125
- title: 'Article with Initial Values',
126
- author: 'Jane Doe',
127
- count: 42,
128
- }),
129
- )
126
+ const {appeared} = await applyDocumentActions(instance, {
127
+ actions: [
128
+ createDocument(doc, {
129
+ title: 'Article with Initial Values',
130
+ author: 'Jane Doe',
131
+ count: 42,
132
+ }),
133
+ ],
134
+ })
130
135
  expect(appeared).toContain(getDraftId(doc.documentId))
131
136
 
132
137
  const currentDoc = documentState.getCurrent()
@@ -155,7 +160,9 @@ it('edits existing documents', async () => {
155
160
  title: 'existing doc',
156
161
  })
157
162
 
158
- await applyDocumentActions(instance, editDocument(doc, {set: {title: 'updated title'}}))
163
+ await applyDocumentActions(instance, {
164
+ actions: [editDocument(doc, {set: {title: 'updated title'}})],
165
+ })
159
166
  expect(state.getCurrent()).toMatchObject({
160
167
  _id: getDraftId(doc.documentId),
161
168
  title: 'updated title',
@@ -178,12 +185,11 @@ it('sets optimistic changes synchronously', async () => {
178
185
 
179
186
  // then the actions are synchronous
180
187
  expect(state1.getCurrent()).toBeNull()
181
- applyDocumentActions(instance1, createDocument(doc))
188
+ applyDocumentActions(instance1, {actions: [createDocument(doc)]})
182
189
  expect(state1.getCurrent()).toMatchObject({_id: getDraftId(doc.documentId)})
183
- const actionResult1Promise = applyDocumentActions(
184
- instance1,
185
- editDocument(doc, {set: {title: 'initial title'}}),
186
- )
190
+ const actionResult1Promise = applyDocumentActions(instance1, {
191
+ actions: [editDocument(doc, {set: {title: 'initial title'}})],
192
+ })
187
193
  expect(state1.getCurrent()?.title).toBe('initial title')
188
194
 
189
195
  // notice how state2 doesn't have the value yet because it's a different
@@ -201,10 +207,9 @@ it('sets optimistic changes synchronously', async () => {
201
207
  expect(state2.getCurrent()?.title).toBe('initial title')
202
208
 
203
209
  // synchronous for state 2
204
- const actionResult2Promise = applyDocumentActions(
205
- instance2,
206
- editDocument(doc, {set: {title: 'updated title'}}),
207
- )
210
+ const actionResult2Promise = applyDocumentActions(instance2, {
211
+ actions: [editDocument(doc, {set: {title: 'updated title'}})],
212
+ })
208
213
  expect(state2.getCurrent()?.title).toBe('updated title')
209
214
  // async for state 1
210
215
  expect(state1.getCurrent()?.title).toBe('initial title')
@@ -226,7 +231,7 @@ it('propagates changes between two instances', async () => {
226
231
  const state2Unsubscribe = state2.subscribe()
227
232
 
228
233
  // Create the document from instance1.
229
- await applyDocumentActions(instance1, createDocument(doc)).then((r) => r.submitted())
234
+ await applyDocumentActions(instance1, {actions: [createDocument(doc)]}).then((r) => r.submitted())
230
235
 
231
236
  const doc1 = state1.getCurrent()
232
237
  const doc2 = state2.getCurrent()
@@ -234,9 +239,9 @@ it('propagates changes between two instances', async () => {
234
239
  expect(doc2?._id).toEqual(getDraftId(doc.documentId))
235
240
 
236
241
  // Now, edit the document from instance2.
237
- await applyDocumentActions(instance2, editDocument(doc, {set: {title: 'Hello world!'}})).then(
238
- (r) => r.submitted(),
239
- )
242
+ await applyDocumentActions(instance2, {
243
+ actions: [editDocument(doc, {set: {title: 'Hello world!'}})],
244
+ }).then((r) => r.submitted())
240
245
 
241
246
  const updated1 = state1.getCurrent()
242
247
  const updated2 = state2.getCurrent()
@@ -258,20 +263,22 @@ it('handles concurrent edits and resolves conflicts', async () => {
258
263
  const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
259
264
 
260
265
  // Create the initial document from a one-off instance.
261
- await applyDocumentActions(oneOffInstance, [
262
- createDocument(doc),
263
- editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy dog'}}),
264
- ]).then((res) => res.submitted())
266
+ await applyDocumentActions(oneOffInstance, {
267
+ actions: [
268
+ createDocument(doc),
269
+ editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy dog'}}),
270
+ ],
271
+ }).then((res) => res.submitted())
265
272
 
266
273
  // Both instances now issue an edit simultaneously.
267
- const p1 = applyDocumentActions(
268
- instance1,
269
- editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy cat'}}),
270
- ).then((r) => r.submitted())
271
- const p2 = applyDocumentActions(
272
- instance2,
273
- editDocument(doc, {set: {title: 'The quick brown elephant jumps over the lazy dog'}}),
274
- ).then((r) => r.submitted())
274
+ const p1 = applyDocumentActions(instance1, {
275
+ actions: [editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy cat'}})],
276
+ }).then((r) => r.submitted())
277
+ const p2 = applyDocumentActions(instance2, {
278
+ actions: [
279
+ editDocument(doc, {set: {title: 'The quick brown elephant jumps over the lazy dog'}}),
280
+ ],
281
+ }).then((r) => r.submitted())
275
282
 
276
283
  // Wait for both actions to complete (or reject).
277
284
  await Promise.allSettled([p1, p2])
@@ -292,8 +299,8 @@ it('unpublishes and discards a document', async () => {
292
299
  const unsubscribe = documentState.subscribe()
293
300
 
294
301
  // Create and publish the document.
295
- await applyDocumentActions(instance, createDocument(doc))
296
- const afterPublish = await applyDocumentActions(instance, publishDocument(doc))
302
+ await applyDocumentActions(instance, {actions: [createDocument(doc)]})
303
+ const afterPublish = await applyDocumentActions(instance, {actions: [publishDocument(doc)]})
297
304
  const publishedDoc = documentState.getCurrent()
298
305
  expect(publishedDoc).toMatchObject({
299
306
  _id: getPublishedId(doc.documentId),
@@ -301,13 +308,13 @@ it('unpublishes and discards a document', async () => {
301
308
  })
302
309
 
303
310
  // Unpublish the document (which should delete the published version and create a draft).
304
- await applyDocumentActions(instance, unpublishDocument(doc))
311
+ await applyDocumentActions(instance, {actions: [unpublishDocument(doc)]})
305
312
  const afterUnpublish = documentState.getCurrent()
306
313
  // In our mock implementation the _id remains the same but the published copy is removed.
307
314
  expect(afterUnpublish?._id).toEqual(getDraftId(doc.documentId))
308
315
 
309
316
  // Discard the draft (which deletes the draft version).
310
- await applyDocumentActions(instance, discardDocument(doc))
317
+ await applyDocumentActions(instance, {actions: [discardDocument(doc)]})
311
318
  const afterDiscard = documentState.getCurrent()
312
319
  expect(afterDiscard).toBeNull()
313
320
 
@@ -320,12 +327,12 @@ it('deletes a document', async () => {
320
327
  const documentState = getDocumentState(instance, doc)
321
328
  const unsubscribe = documentState.subscribe()
322
329
 
323
- await applyDocumentActions(instance, [createDocument(doc), publishDocument(doc)])
330
+ await applyDocumentActions(instance, {actions: [createDocument(doc), publishDocument(doc)]})
324
331
  const docValue = documentState.getCurrent()
325
332
  expect(docValue).toBeDefined()
326
333
 
327
334
  // Delete the document.
328
- await applyDocumentActions(instance, deleteDocument(doc))
335
+ await applyDocumentActions(instance, {actions: [deleteDocument(doc)]})
329
336
  const afterDelete = documentState.getCurrent()
330
337
  expect(afterDelete).toBeNull()
331
338
 
@@ -340,7 +347,7 @@ it('cleans up document state when there are no subscribers', async () => {
340
347
  const unsubscribe = documentState.subscribe()
341
348
 
342
349
  // Create a document.
343
- await applyDocumentActions(instance, createDocument(doc))
350
+ await applyDocumentActions(instance, {actions: [createDocument(doc)]})
344
351
  expect(documentState.getCurrent()).toBeDefined()
345
352
 
346
353
  // Unsubscribe from the document.
@@ -365,7 +372,9 @@ it('fetches documents if there are no active subscriptions for the actions appli
365
372
  // there are no active subscriptions so applying this action will create one
366
373
  // for this action. this subscription will be removed when the outgoing
367
374
  // transaction for this action has been accepted by the server
368
- const setNewTitle = applyDocumentActions(instance, editDocument(doc, {set: {title: 'new title'}}))
375
+ const setNewTitle = applyDocumentActions(instance, {
376
+ actions: [editDocument(doc, {set: {title: 'new title'}})],
377
+ })
369
378
  expect(getCurrent()?.title).toBeUndefined()
370
379
  expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBe(false)
371
380
 
@@ -373,27 +382,25 @@ it('fetches documents if there are no active subscriptions for the actions appli
373
382
  expect(getCurrent()?.title).toBe('new title')
374
383
 
375
384
  // there is an active subscriber now so the edits are synchronous
376
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'updated title'}}))
385
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'updated title'}})]})
377
386
  expect(getCurrent()?.title).toBe('updated title')
378
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'updated title!'}}))
387
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'updated title!'}})]})
379
388
  expect(getCurrent()?.title).toBe('updated title!')
380
389
 
381
390
  expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBe(false)
382
391
 
383
392
  // await submitted in order to test that there is no subscriptions
384
- const result = await applyDocumentActions(
385
- instance,
386
- editDocument(doc, {set: {title: 'updated title'}}),
387
- )
393
+ const result = await applyDocumentActions(instance, {
394
+ actions: [editDocument(doc, {set: {title: 'updated title'}})],
395
+ })
388
396
  await result.submitted()
389
397
 
390
398
  // test that there isn't any document state
391
399
  expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBeUndefined()
392
400
 
393
- const setNewNewTitle = applyDocumentActions(
394
- instance,
395
- editDocument(doc, {set: {title: 'new new title'}}),
396
- )
401
+ const setNewNewTitle = applyDocumentActions(instance, {
402
+ actions: [editDocument(doc, {set: {title: 'new new title'}})],
403
+ })
397
404
  // now we'll have to await again
398
405
  expect(getCurrent()?.title).toBe(undefined)
399
406
 
@@ -407,13 +414,15 @@ it('batches edit transaction into one outgoing transaction', async () => {
407
414
  const unsubscribe = getDocumentState(instance, doc).subscribe()
408
415
 
409
416
  // this creates its own transaction
410
- applyDocumentActions(instance, createDocument(doc))
417
+ applyDocumentActions(instance, {actions: [createDocument(doc)]})
411
418
 
412
419
  // these get batched into one
413
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'name!'}}))
414
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'name!!'}}))
415
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'name!!!'}}))
416
- const res = await applyDocumentActions(instance, editDocument(doc, {set: {title: 'name!!!!'}}))
420
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!'}})]})
421
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!!'}})]})
422
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!!!'}})]})
423
+ const res = await applyDocumentActions(instance, {
424
+ actions: [editDocument(doc, {set: {title: 'name!!!!'}})],
425
+ })
417
426
  await res.submitted()
418
427
 
419
428
  expect(client.action).toHaveBeenCalledTimes(2)
@@ -434,7 +443,7 @@ it('provides the consistency status via `getDocumentSyncStatus`', async () => {
434
443
  const unsubscribe = syncStatus.subscribe()
435
444
  expect(syncStatus.getCurrent()).toBe(true)
436
445
 
437
- const applied = applyDocumentActions(instance, createDocument(doc))
446
+ const applied = applyDocumentActions(instance, {actions: [createDocument(doc)]})
438
447
  expect(syncStatus.getCurrent()).toBe(false)
439
448
 
440
449
  const createResult = await applied
@@ -443,11 +452,11 @@ it('provides the consistency status via `getDocumentSyncStatus`', async () => {
443
452
  await createResult.submitted()
444
453
  expect(syncStatus.getCurrent()).toBe(true)
445
454
 
446
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'initial name'}}))
455
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'initial name'}})]})
447
456
  expect(syncStatus.getCurrent()).toBe(false)
448
457
 
449
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'updated name'}}))
450
- const publishResult = applyDocumentActions(instance, publishDocument(doc))
458
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'updated name'}})]})
459
+ const publishResult = applyDocumentActions(instance, {actions: [publishDocument(doc)]})
451
460
  expect(syncStatus.getCurrent()).toBe(false)
452
461
  await publishResult.then((res) => res.submitted())
453
462
  expect(syncStatus.getCurrent()).toBe(true)
@@ -477,25 +486,23 @@ it('reverts failed outgoing transaction locally', async () => {
477
486
  const {getCurrent, subscribe} = getDocumentState(instance, doc)
478
487
  const unsubscribe = subscribe()
479
488
 
480
- await applyDocumentActions(instance, createDocument(doc))
481
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'the'}}))
482
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'the quick'}}))
489
+ await applyDocumentActions(instance, {actions: [createDocument(doc)]})
490
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'the'}})]})
491
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'the quick'}})]})
483
492
 
484
493
  // this edit action is simulated to fail from the backend and will be reverted
485
- const revertedActionResult = applyDocumentActions(
486
- instance,
487
- editDocument(doc, {set: {title: 'the quick brown'}}),
488
- {
489
- transactionId: 'force-revert',
490
- disableBatching: true,
491
- },
492
- )
494
+ const revertedActionResult = applyDocumentActions(instance, {
495
+ actions: [editDocument(doc, {set: {title: 'the quick brown'}})],
496
+ transactionId: 'force-revert',
497
+ disableBatching: true,
498
+ })
493
499
 
494
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'the quick brown fox'}}))
495
- await applyDocumentActions(
496
- instance,
497
- editDocument(doc, {set: {title: 'the quick brown fox jumps'}}),
498
- ).then((e) => e.submitted())
500
+ applyDocumentActions(instance, {
501
+ actions: [editDocument(doc, {set: {title: 'the quick brown fox'}})],
502
+ })
503
+ await applyDocumentActions(instance, {
504
+ actions: [editDocument(doc, {set: {title: 'the quick brown fox jumps'}})],
505
+ }).then((e) => e.submitted())
499
506
 
500
507
  await expect(revertedEventPromise).resolves.toMatchObject({
501
508
  type: 'reverted',
@@ -512,7 +519,9 @@ it('reverts failed outgoing transaction locally', async () => {
512
519
  expect(getCurrent()?.title).toBe('the quick fox jumps')
513
520
 
514
521
  // check that we can still edit after recovering from the error
515
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'TEST the quick fox jumps'}}))
522
+ applyDocumentActions(instance, {
523
+ actions: [editDocument(doc, {set: {title: 'TEST the quick fox jumps'}})],
524
+ })
516
525
  expect(getCurrent()?.title).toBe('TEST the quick fox jumps')
517
526
 
518
527
  unsubscribe()
@@ -534,7 +543,7 @@ it('removes a queued transaction if it fails to apply', async () => {
534
543
  const unsubscribe = state.subscribe()
535
544
 
536
545
  await expect(
537
- applyDocumentActions(instance, editDocument(doc, {set: {title: "can't set"}})),
546
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: "can't set"}})]}),
538
547
  ).rejects.toThrowError(/Cannot edit document/)
539
548
 
540
549
  await expect(actionErrorEventPromise).resolves.toMatchObject({
@@ -544,8 +553,8 @@ it('removes a queued transaction if it fails to apply', async () => {
544
553
  })
545
554
 
546
555
  // editing should still work after though (no crashing)
547
- await applyDocumentActions(instance, createDocument(doc))
548
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'can set!'}}))
556
+ await applyDocumentActions(instance, {actions: [createDocument(doc)]})
557
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'can set!'}})]})
549
558
 
550
559
  expect(state.getCurrent()?.title).toBe('can set!')
551
560
 
@@ -565,13 +574,17 @@ it('returns allowed true when no permission errors occur', async () => {
565
574
  })
566
575
  const state = getDocumentState(instance, doc)
567
576
  const unsubscribe = state.subscribe()
568
- await applyDocumentActions(instance, createDocument(doc)).then((r) => r.submitted())
577
+ await applyDocumentActions(instance, {actions: [createDocument(doc)]}).then((r) => r.submitted())
569
578
 
570
579
  // Use an action that includes a patch (so that update permission check is bypassed).
571
580
  const permissionsState = getPermissionsState(instance, {
572
- ...doc,
573
- type: 'document.edit',
574
- patches: [{set: {title: 'New Title'}}],
581
+ actions: [
582
+ {
583
+ ...doc,
584
+ type: 'document.edit',
585
+ patches: [{set: {title: 'New Title'}}],
586
+ },
587
+ ],
575
588
  })
576
589
  // Wait briefly to allow permissions calculation.
577
590
  await new Promise((resolve) => setTimeout(resolve, 10))
@@ -583,7 +596,7 @@ it('returns allowed true when no permission errors occur', async () => {
583
596
  it("should reject applying the action if a precondition isn't met", async () => {
584
597
  const doc = createDocumentHandle({documentId: 'does-not-exist', documentType: 'article'})
585
598
 
586
- await expect(applyDocumentActions(instance, deleteDocument(doc))).rejects.toThrow(
599
+ await expect(applyDocumentActions(instance, {actions: [deleteDocument(doc)]})).rejects.toThrow(
587
600
  'The document you are trying to delete does not exist.',
588
601
  )
589
602
  })
@@ -594,7 +607,7 @@ it("should reject applying the action if a permission isn't met", async () => {
594
607
  const datasetAcl = [{filter: 'false', permissions: ['create']}]
595
608
  vi.mocked(client.request).mockResolvedValue(datasetAcl)
596
609
 
597
- await expect(applyDocumentActions(instance, createDocument(doc))).rejects.toThrow(
610
+ await expect(applyDocumentActions(instance, {actions: [createDocument(doc)]})).rejects.toThrow(
598
611
  'You do not have permission to create a draft for document "does-not-exist".',
599
612
  )
600
613
  })
@@ -604,7 +617,7 @@ it('returns allowed false with reasons when permission errors occur', async () =
604
617
  vi.mocked(client.request).mockResolvedValue(datasetAcl)
605
618
 
606
619
  const doc = createDocumentHandle({documentId: 'doc-perm-denied', documentType: 'article'})
607
- const result = await resolvePermissions(instance, createDocument(doc))
620
+ const result = await resolvePermissions(instance, {actions: [createDocument(doc)]})
608
621
 
609
622
  const message = 'You do not have permission to create a draft for document "doc-perm-denied".'
610
623
  expect(result).toMatchObject({
@@ -625,8 +638,10 @@ it('fetches dataset ACL and updates grants in the document store state', async (
625
638
  const book = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'book'})
626
639
  const author = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'author'})
627
640
 
628
- expect(await resolvePermissions(instance, createDocument(book))).toEqual({allowed: true})
629
- expect(await resolvePermissions(instance, createDocument(author))).toMatchObject({
641
+ expect(await resolvePermissions(instance, {actions: [createDocument(book)]})).toEqual({
642
+ allowed: true,
643
+ })
644
+ expect(await resolvePermissions(instance, {actions: [createDocument(author)]})).toMatchObject({
630
645
  allowed: false,
631
646
  message: expect.stringContaining('You do not have permission to create a draft for document'),
632
647
  })
@@ -639,10 +654,9 @@ it('returns a promise that resolves when a document has been loaded in the store
639
654
 
640
655
  // use one-off instance to create the document in the mock backend
641
656
  const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
642
- const result = await applyDocumentActions(oneOffInstance, [
643
- createDocument(doc),
644
- editDocument(doc, {set: {title: 'initial title'}}),
645
- ])
657
+ const result = await applyDocumentActions(oneOffInstance, {
658
+ actions: [createDocument(doc), editDocument(doc, {set: {title: 'initial title'}})],
659
+ })
646
660
  await result.submitted() // wait till submitted to server before resolving
647
661
 
648
662
  await expect(resolveDocument(instance, doc)).resolves.toMatchObject({
@@ -661,19 +675,23 @@ it('emits an event for each action after an outgoing transaction has been accept
661
675
  const doc = createDocumentHandle({documentId, documentType: 'article'})
662
676
  expect(handler).toHaveBeenCalledTimes(0)
663
677
 
664
- const tnx1 = await applyDocumentActions(instance, [
665
- createDocument(doc),
666
- editDocument(doc, {set: {title: 'new name'}}),
667
- publishDocument(doc),
668
- ]).then((e) => e.submitted())
678
+ const tnx1 = await applyDocumentActions(instance, {
679
+ actions: [
680
+ createDocument(doc),
681
+ editDocument(doc, {set: {title: 'new name'}}),
682
+ publishDocument(doc),
683
+ ],
684
+ }).then((e) => e.submitted())
669
685
  expect(handler).toHaveBeenCalledTimes(4)
670
686
 
671
- const tnx2 = await applyDocumentActions(instance, [
672
- unpublishDocument(doc),
673
- publishDocument(doc),
674
- editDocument(doc, {set: {title: 'updated name'}}),
675
- discardDocument(doc),
676
- ]).then((e) => e.submitted())
687
+ const tnx2 = await applyDocumentActions(instance, {
688
+ actions: [
689
+ unpublishDocument(doc),
690
+ publishDocument(doc),
691
+ editDocument(doc, {set: {title: 'updated name'}}),
692
+ discardDocument(doc),
693
+ ],
694
+ }).then((e) => e.submitted())
677
695
  expect(handler).toHaveBeenCalledTimes(9)
678
696
 
679
697
  expect(handler.mock.calls).toMatchObject([
@@ -688,7 +706,7 @@ it('emits an event for each action after an outgoing transaction has been accept
688
706
  [{type: 'accepted', outgoing: {transactionId: tnx2.transactionId}}],
689
707
  ])
690
708
 
691
- await applyDocumentActions(instance, deleteDocument(doc))
709
+ await applyDocumentActions(instance, {actions: [deleteDocument(doc)]})
692
710
 
693
711
  unsubscribe()
694
712
  })
@@ -933,12 +951,7 @@ beforeEach(() => {
933
951
  continue
934
952
  }
935
953
  default: {
936
- throw new Error(
937
- `Unsupported action for mock backend: ${
938
- // @ts-expect-error unexpected input
939
- i.actionType
940
- }`,
941
- )
954
+ throw new Error(`Unsupported action for mock backend: ${i.actionType}`)
942
955
  }
943
956
  }
944
957
  }
@@ -28,7 +28,11 @@ import {
28
28
 
29
29
  import {getClientState} from '../client/clientStore'
30
30
  import {type DocumentHandle} from '../config/sanityConfig'
31
- import {bindActionByDataset, type StoreAction} from '../store/createActionBinder'
31
+ import {
32
+ bindActionByDataset,
33
+ type BoundDatasetKey,
34
+ type StoreAction,
35
+ } from '../store/createActionBinder'
32
36
  import {type SanityInstance} from '../store/createSanityInstance'
33
37
  import {createStateSourceAction, type StateSource} from '../store/createStateSourceAction'
34
38
  import {defineStore, type StoreContext} from '../store/defineStore'
@@ -103,7 +107,7 @@ export interface DocumentState {
103
107
  unverifiedRevisions?: {[TTransactionId in string]?: UnverifiedDocumentRevision}
104
108
  }
105
109
 
106
- export const documentStore = defineStore<DocumentStoreState>({
110
+ export const documentStore = defineStore<DocumentStoreState, BoundDatasetKey>({
107
111
  name: 'Document',
108
112
  getInitialState: (instance) => ({
109
113
  documentStates: {},
@@ -183,8 +187,21 @@ const _getDocumentState = bindActionByDataset(
183
187
  documentStore,
184
188
  createStateSourceAction({
185
189
  selector: ({state: {error, documentStates}}, options: DocumentOptions<string | undefined>) => {
186
- const {documentId, path} = options
190
+ const {documentId, path, liveEdit} = options
187
191
  if (error) throw error
192
+
193
+ if (liveEdit) {
194
+ // For liveEdit documents, only look at the single document
195
+ const document = documentStates[documentId]?.local
196
+ if (document === undefined) return undefined
197
+ if (!path) return document
198
+ const result = jsonMatch(document, path).next()
199
+ if (result.done) return undefined
200
+ const {value} = result.value
201
+ return value
202
+ }
203
+
204
+ // Standard draft/published logic
188
205
  const draftId = getDraftId(documentId)
189
206
  const publishedId = getPublishedId(documentId)
190
207
  const draft = documentStates[draftId]?.local
@@ -200,7 +217,7 @@ const _getDocumentState = bindActionByDataset(
200
217
  return value
201
218
  },
202
219
  onSubscribe: (context, options: DocumentOptions<string | undefined>) =>
203
- manageSubscriberIds(context, options.documentId),
220
+ manageSubscriberIds(context, options.documentId, {expandDraftPublished: !options.liveEdit}),
204
221
  }),
205
222
  )
206
223
 
@@ -246,6 +263,15 @@ export const getDocumentSyncStatus = bindActionByDataset(
246
263
  ) => {
247
264
  const documentId = typeof doc === 'string' ? doc : doc.documentId
248
265
  if (error) throw error
266
+
267
+ if (doc.liveEdit) {
268
+ // For liveEdit documents, only check the single document
269
+ const document = documents[documentId]
270
+ if (document === undefined) return undefined
271
+ return !queued.length && !applied.length && !outgoing
272
+ }
273
+
274
+ // Standard draft/published logic
249
275
  const draftId = getDraftId(documentId)
250
276
  const publishedId = getPublishedId(documentId)
251
277
 
@@ -259,16 +285,20 @@ export const getDocumentSyncStatus = bindActionByDataset(
259
285
  }),
260
286
  )
261
287
 
288
+ type PermissionsStateOptions = {
289
+ actions: DocumentAction[]
290
+ }
291
+
262
292
  /** @beta */
263
293
  export const getPermissionsState = bindActionByDataset(
264
294
  documentStore,
265
295
  createStateSourceAction({
266
296
  selector: calculatePermissions,
267
- onSubscribe: (context, actions) =>
297
+ onSubscribe: (context, {actions}: PermissionsStateOptions) =>
268
298
  manageSubscriberIds(context, getDocumentIdsFromActions(actions)),
269
299
  }) as StoreAction<
270
300
  DocumentStoreState,
271
- [DocumentAction | DocumentAction[]],
301
+ [PermissionsStateOptions],
272
302
  StateSource<ReturnType<typeof calculatePermissions>>
273
303
  >,
274
304
  )
@@ -276,9 +306,9 @@ export const getPermissionsState = bindActionByDataset(
276
306
  /** @beta */
277
307
  export const resolvePermissions = bindActionByDataset(
278
308
  documentStore,
279
- ({instance}, actions: DocumentAction | DocumentAction[]) => {
309
+ ({instance}, options: PermissionsStateOptions) => {
280
310
  return firstValueFrom(
281
- getPermissionsState(instance, actions).observable.pipe(filter((i) => i !== undefined)),
311
+ getPermissionsState(instance, options).observable.pipe(filter((i) => i !== undefined)),
282
312
  )
283
313
  },
284
314
  )
@@ -439,8 +469,8 @@ const subscribeToSubscriptionsAndListenToDocuments = (
439
469
  const subscribeToClientAndFetchDatasetAcl = ({
440
470
  instance,
441
471
  state,
442
- }: StoreContext<DocumentStoreState>) => {
443
- const {projectId, dataset} = instance.config
472
+ key: {projectId, dataset},
473
+ }: StoreContext<DocumentStoreState, BoundDatasetKey>) => {
444
474
  return getClientState(instance, {apiVersion: API_VERSION})
445
475
  .observable.pipe(
446
476
  switchMap((client) =>