@sanity/sdk 2.3.1 → 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 (55) hide show
  1. package/dist/index.d.ts +173 -105
  2. package/dist/index.js +354 -122
  3. package/dist/index.js.map +1 -1
  4. package/package.json +12 -11
  5. package/src/_exports/index.ts +30 -0
  6. package/src/agent/agentActions.test.ts +81 -0
  7. package/src/agent/agentActions.ts +139 -0
  8. package/src/auth/authStore.test.ts +13 -13
  9. package/src/auth/refreshStampedToken.test.ts +16 -16
  10. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +6 -6
  11. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -4
  12. package/src/auth/utils.ts +36 -0
  13. package/src/client/clientStore.test.ts +151 -0
  14. package/src/client/clientStore.ts +39 -1
  15. package/src/comlink/controller/actions/destroyController.test.ts +2 -2
  16. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +6 -6
  17. package/src/comlink/controller/actions/getOrCreateController.test.ts +5 -5
  18. package/src/comlink/controller/actions/getOrCreateController.ts +1 -1
  19. package/src/comlink/controller/actions/releaseChannel.test.ts +3 -2
  20. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -4
  21. package/src/comlink/node/actions/getOrCreateNode.test.ts +7 -7
  22. package/src/comlink/node/actions/releaseNode.test.ts +2 -2
  23. package/src/comlink/node/comlinkNodeStore.test.ts +4 -3
  24. package/src/config/sanityConfig.ts +49 -3
  25. package/src/document/actions.test.ts +34 -0
  26. package/src/document/actions.ts +31 -7
  27. package/src/document/applyDocumentActions.test.ts +9 -6
  28. package/src/document/applyDocumentActions.ts +9 -49
  29. package/src/document/documentStore.test.ts +148 -107
  30. package/src/document/documentStore.ts +40 -10
  31. package/src/document/permissions.test.ts +9 -9
  32. package/src/document/permissions.ts +17 -7
  33. package/src/document/processActions.test.ts +345 -0
  34. package/src/document/processActions.ts +185 -2
  35. package/src/document/reducers.ts +13 -6
  36. package/src/presence/presenceStore.ts +13 -7
  37. package/src/preview/previewStore.test.ts +10 -2
  38. package/src/preview/previewStore.ts +2 -1
  39. package/src/preview/subscribeToStateAndFetchBatches.test.ts +8 -5
  40. package/src/preview/subscribeToStateAndFetchBatches.ts +9 -3
  41. package/src/projection/projectionStore.test.ts +18 -2
  42. package/src/projection/projectionStore.ts +2 -1
  43. package/src/projection/subscribeToStateAndFetchBatches.test.ts +6 -5
  44. package/src/projection/subscribeToStateAndFetchBatches.ts +9 -3
  45. package/src/query/queryStore.ts +7 -4
  46. package/src/releases/getPerspectiveState.ts +2 -2
  47. package/src/releases/releasesStore.ts +10 -4
  48. package/src/store/createActionBinder.test.ts +8 -6
  49. package/src/store/createActionBinder.ts +50 -14
  50. package/src/store/createStateSourceAction.test.ts +12 -11
  51. package/src/store/createStateSourceAction.ts +6 -6
  52. package/src/store/createStoreInstance.test.ts +29 -16
  53. package/src/store/createStoreInstance.ts +6 -5
  54. package/src/store/defineStore.test.ts +1 -1
  55. 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
 
@@ -110,6 +114,35 @@ it('creates, edits, and publishes a document', async () => {
110
114
  unsubscribe()
111
115
  })
112
116
 
117
+ it('creates a document with initial values', async () => {
118
+ const doc = createDocumentHandle({documentId: 'doc-with-initial', documentType: 'article'})
119
+ const documentState = getDocumentState(instance, doc)
120
+
121
+ expect(documentState.getCurrent()).toBeUndefined()
122
+
123
+ const unsubscribe = documentState.subscribe()
124
+
125
+ // Create a new document with initial field values
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
+ })
135
+ expect(appeared).toContain(getDraftId(doc.documentId))
136
+
137
+ const currentDoc = documentState.getCurrent()
138
+ expect(currentDoc?._id).toEqual(getDraftId(doc.documentId))
139
+ expect(currentDoc?.title).toEqual('Article with Initial Values')
140
+ expect(currentDoc?.['author']).toEqual('Jane Doe')
141
+ expect(currentDoc?.['count']).toEqual(42)
142
+
143
+ unsubscribe()
144
+ })
145
+
113
146
  it('edits existing documents', async () => {
114
147
  const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article'})
115
148
  const state = getDocumentState(instance, doc)
@@ -127,7 +160,9 @@ it('edits existing documents', async () => {
127
160
  title: 'existing doc',
128
161
  })
129
162
 
130
- await applyDocumentActions(instance, editDocument(doc, {set: {title: 'updated title'}}))
163
+ await applyDocumentActions(instance, {
164
+ actions: [editDocument(doc, {set: {title: 'updated title'}})],
165
+ })
131
166
  expect(state.getCurrent()).toMatchObject({
132
167
  _id: getDraftId(doc.documentId),
133
168
  title: 'updated title',
@@ -150,12 +185,11 @@ it('sets optimistic changes synchronously', async () => {
150
185
 
151
186
  // then the actions are synchronous
152
187
  expect(state1.getCurrent()).toBeNull()
153
- applyDocumentActions(instance1, createDocument(doc))
188
+ applyDocumentActions(instance1, {actions: [createDocument(doc)]})
154
189
  expect(state1.getCurrent()).toMatchObject({_id: getDraftId(doc.documentId)})
155
- const actionResult1Promise = applyDocumentActions(
156
- instance1,
157
- editDocument(doc, {set: {title: 'initial title'}}),
158
- )
190
+ const actionResult1Promise = applyDocumentActions(instance1, {
191
+ actions: [editDocument(doc, {set: {title: 'initial title'}})],
192
+ })
159
193
  expect(state1.getCurrent()?.title).toBe('initial title')
160
194
 
161
195
  // notice how state2 doesn't have the value yet because it's a different
@@ -173,10 +207,9 @@ it('sets optimistic changes synchronously', async () => {
173
207
  expect(state2.getCurrent()?.title).toBe('initial title')
174
208
 
175
209
  // synchronous for state 2
176
- const actionResult2Promise = applyDocumentActions(
177
- instance2,
178
- editDocument(doc, {set: {title: 'updated title'}}),
179
- )
210
+ const actionResult2Promise = applyDocumentActions(instance2, {
211
+ actions: [editDocument(doc, {set: {title: 'updated title'}})],
212
+ })
180
213
  expect(state2.getCurrent()?.title).toBe('updated title')
181
214
  // async for state 1
182
215
  expect(state1.getCurrent()?.title).toBe('initial title')
@@ -198,7 +231,7 @@ it('propagates changes between two instances', async () => {
198
231
  const state2Unsubscribe = state2.subscribe()
199
232
 
200
233
  // Create the document from instance1.
201
- await applyDocumentActions(instance1, createDocument(doc)).then((r) => r.submitted())
234
+ await applyDocumentActions(instance1, {actions: [createDocument(doc)]}).then((r) => r.submitted())
202
235
 
203
236
  const doc1 = state1.getCurrent()
204
237
  const doc2 = state2.getCurrent()
@@ -206,9 +239,9 @@ it('propagates changes between two instances', async () => {
206
239
  expect(doc2?._id).toEqual(getDraftId(doc.documentId))
207
240
 
208
241
  // Now, edit the document from instance2.
209
- await applyDocumentActions(instance2, editDocument(doc, {set: {title: 'Hello world!'}})).then(
210
- (r) => r.submitted(),
211
- )
242
+ await applyDocumentActions(instance2, {
243
+ actions: [editDocument(doc, {set: {title: 'Hello world!'}})],
244
+ }).then((r) => r.submitted())
212
245
 
213
246
  const updated1 = state1.getCurrent()
214
247
  const updated2 = state2.getCurrent()
@@ -230,20 +263,22 @@ it('handles concurrent edits and resolves conflicts', async () => {
230
263
  const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
231
264
 
232
265
  // Create the initial document from a one-off instance.
233
- await applyDocumentActions(oneOffInstance, [
234
- createDocument(doc),
235
- editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy dog'}}),
236
- ]).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())
237
272
 
238
273
  // Both instances now issue an edit simultaneously.
239
- const p1 = applyDocumentActions(
240
- instance1,
241
- editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy cat'}}),
242
- ).then((r) => r.submitted())
243
- const p2 = applyDocumentActions(
244
- instance2,
245
- editDocument(doc, {set: {title: 'The quick brown elephant jumps over the lazy dog'}}),
246
- ).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())
247
282
 
248
283
  // Wait for both actions to complete (or reject).
249
284
  await Promise.allSettled([p1, p2])
@@ -264,8 +299,8 @@ it('unpublishes and discards a document', async () => {
264
299
  const unsubscribe = documentState.subscribe()
265
300
 
266
301
  // Create and publish the document.
267
- await applyDocumentActions(instance, createDocument(doc))
268
- const afterPublish = await applyDocumentActions(instance, publishDocument(doc))
302
+ await applyDocumentActions(instance, {actions: [createDocument(doc)]})
303
+ const afterPublish = await applyDocumentActions(instance, {actions: [publishDocument(doc)]})
269
304
  const publishedDoc = documentState.getCurrent()
270
305
  expect(publishedDoc).toMatchObject({
271
306
  _id: getPublishedId(doc.documentId),
@@ -273,13 +308,13 @@ it('unpublishes and discards a document', async () => {
273
308
  })
274
309
 
275
310
  // Unpublish the document (which should delete the published version and create a draft).
276
- await applyDocumentActions(instance, unpublishDocument(doc))
311
+ await applyDocumentActions(instance, {actions: [unpublishDocument(doc)]})
277
312
  const afterUnpublish = documentState.getCurrent()
278
313
  // In our mock implementation the _id remains the same but the published copy is removed.
279
314
  expect(afterUnpublish?._id).toEqual(getDraftId(doc.documentId))
280
315
 
281
316
  // Discard the draft (which deletes the draft version).
282
- await applyDocumentActions(instance, discardDocument(doc))
317
+ await applyDocumentActions(instance, {actions: [discardDocument(doc)]})
283
318
  const afterDiscard = documentState.getCurrent()
284
319
  expect(afterDiscard).toBeNull()
285
320
 
@@ -292,12 +327,12 @@ it('deletes a document', async () => {
292
327
  const documentState = getDocumentState(instance, doc)
293
328
  const unsubscribe = documentState.subscribe()
294
329
 
295
- await applyDocumentActions(instance, [createDocument(doc), publishDocument(doc)])
330
+ await applyDocumentActions(instance, {actions: [createDocument(doc), publishDocument(doc)]})
296
331
  const docValue = documentState.getCurrent()
297
332
  expect(docValue).toBeDefined()
298
333
 
299
334
  // Delete the document.
300
- await applyDocumentActions(instance, deleteDocument(doc))
335
+ await applyDocumentActions(instance, {actions: [deleteDocument(doc)]})
301
336
  const afterDelete = documentState.getCurrent()
302
337
  expect(afterDelete).toBeNull()
303
338
 
@@ -312,7 +347,7 @@ it('cleans up document state when there are no subscribers', async () => {
312
347
  const unsubscribe = documentState.subscribe()
313
348
 
314
349
  // Create a document.
315
- await applyDocumentActions(instance, createDocument(doc))
350
+ await applyDocumentActions(instance, {actions: [createDocument(doc)]})
316
351
  expect(documentState.getCurrent()).toBeDefined()
317
352
 
318
353
  // Unsubscribe from the document.
@@ -337,7 +372,9 @@ it('fetches documents if there are no active subscriptions for the actions appli
337
372
  // there are no active subscriptions so applying this action will create one
338
373
  // for this action. this subscription will be removed when the outgoing
339
374
  // transaction for this action has been accepted by the server
340
- const setNewTitle = applyDocumentActions(instance, editDocument(doc, {set: {title: 'new title'}}))
375
+ const setNewTitle = applyDocumentActions(instance, {
376
+ actions: [editDocument(doc, {set: {title: 'new title'}})],
377
+ })
341
378
  expect(getCurrent()?.title).toBeUndefined()
342
379
  expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBe(false)
343
380
 
@@ -345,27 +382,25 @@ it('fetches documents if there are no active subscriptions for the actions appli
345
382
  expect(getCurrent()?.title).toBe('new title')
346
383
 
347
384
  // there is an active subscriber now so the edits are synchronous
348
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'updated title'}}))
385
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'updated title'}})]})
349
386
  expect(getCurrent()?.title).toBe('updated title')
350
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'updated title!'}}))
387
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'updated title!'}})]})
351
388
  expect(getCurrent()?.title).toBe('updated title!')
352
389
 
353
390
  expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBe(false)
354
391
 
355
392
  // await submitted in order to test that there is no subscriptions
356
- const result = await applyDocumentActions(
357
- instance,
358
- editDocument(doc, {set: {title: 'updated title'}}),
359
- )
393
+ const result = await applyDocumentActions(instance, {
394
+ actions: [editDocument(doc, {set: {title: 'updated title'}})],
395
+ })
360
396
  await result.submitted()
361
397
 
362
398
  // test that there isn't any document state
363
399
  expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBeUndefined()
364
400
 
365
- const setNewNewTitle = applyDocumentActions(
366
- instance,
367
- editDocument(doc, {set: {title: 'new new title'}}),
368
- )
401
+ const setNewNewTitle = applyDocumentActions(instance, {
402
+ actions: [editDocument(doc, {set: {title: 'new new title'}})],
403
+ })
369
404
  // now we'll have to await again
370
405
  expect(getCurrent()?.title).toBe(undefined)
371
406
 
@@ -379,13 +414,15 @@ it('batches edit transaction into one outgoing transaction', async () => {
379
414
  const unsubscribe = getDocumentState(instance, doc).subscribe()
380
415
 
381
416
  // this creates its own transaction
382
- applyDocumentActions(instance, createDocument(doc))
417
+ applyDocumentActions(instance, {actions: [createDocument(doc)]})
383
418
 
384
419
  // these get batched into one
385
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'name!'}}))
386
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'name!!'}}))
387
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'name!!!'}}))
388
- 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
+ })
389
426
  await res.submitted()
390
427
 
391
428
  expect(client.action).toHaveBeenCalledTimes(2)
@@ -406,7 +443,7 @@ it('provides the consistency status via `getDocumentSyncStatus`', async () => {
406
443
  const unsubscribe = syncStatus.subscribe()
407
444
  expect(syncStatus.getCurrent()).toBe(true)
408
445
 
409
- const applied = applyDocumentActions(instance, createDocument(doc))
446
+ const applied = applyDocumentActions(instance, {actions: [createDocument(doc)]})
410
447
  expect(syncStatus.getCurrent()).toBe(false)
411
448
 
412
449
  const createResult = await applied
@@ -415,11 +452,11 @@ it('provides the consistency status via `getDocumentSyncStatus`', async () => {
415
452
  await createResult.submitted()
416
453
  expect(syncStatus.getCurrent()).toBe(true)
417
454
 
418
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'initial name'}}))
455
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'initial name'}})]})
419
456
  expect(syncStatus.getCurrent()).toBe(false)
420
457
 
421
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'updated name'}}))
422
- const publishResult = applyDocumentActions(instance, publishDocument(doc))
458
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'updated name'}})]})
459
+ const publishResult = applyDocumentActions(instance, {actions: [publishDocument(doc)]})
423
460
  expect(syncStatus.getCurrent()).toBe(false)
424
461
  await publishResult.then((res) => res.submitted())
425
462
  expect(syncStatus.getCurrent()).toBe(true)
@@ -449,25 +486,23 @@ it('reverts failed outgoing transaction locally', async () => {
449
486
  const {getCurrent, subscribe} = getDocumentState(instance, doc)
450
487
  const unsubscribe = subscribe()
451
488
 
452
- await applyDocumentActions(instance, createDocument(doc))
453
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'the'}}))
454
- 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'}})]})
455
492
 
456
493
  // this edit action is simulated to fail from the backend and will be reverted
457
- const revertedActionResult = applyDocumentActions(
458
- instance,
459
- editDocument(doc, {set: {title: 'the quick brown'}}),
460
- {
461
- transactionId: 'force-revert',
462
- disableBatching: true,
463
- },
464
- )
494
+ const revertedActionResult = applyDocumentActions(instance, {
495
+ actions: [editDocument(doc, {set: {title: 'the quick brown'}})],
496
+ transactionId: 'force-revert',
497
+ disableBatching: true,
498
+ })
465
499
 
466
- applyDocumentActions(instance, editDocument(doc, {set: {title: 'the quick brown fox'}}))
467
- await applyDocumentActions(
468
- instance,
469
- editDocument(doc, {set: {title: 'the quick brown fox jumps'}}),
470
- ).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())
471
506
 
472
507
  await expect(revertedEventPromise).resolves.toMatchObject({
473
508
  type: 'reverted',
@@ -484,7 +519,9 @@ it('reverts failed outgoing transaction locally', async () => {
484
519
  expect(getCurrent()?.title).toBe('the quick fox jumps')
485
520
 
486
521
  // check that we can still edit after recovering from the error
487
- 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
+ })
488
525
  expect(getCurrent()?.title).toBe('TEST the quick fox jumps')
489
526
 
490
527
  unsubscribe()
@@ -506,7 +543,7 @@ it('removes a queued transaction if it fails to apply', async () => {
506
543
  const unsubscribe = state.subscribe()
507
544
 
508
545
  await expect(
509
- applyDocumentActions(instance, editDocument(doc, {set: {title: "can't set"}})),
546
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: "can't set"}})]}),
510
547
  ).rejects.toThrowError(/Cannot edit document/)
511
548
 
512
549
  await expect(actionErrorEventPromise).resolves.toMatchObject({
@@ -516,8 +553,8 @@ it('removes a queued transaction if it fails to apply', async () => {
516
553
  })
517
554
 
518
555
  // editing should still work after though (no crashing)
519
- await applyDocumentActions(instance, createDocument(doc))
520
- 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!'}})]})
521
558
 
522
559
  expect(state.getCurrent()?.title).toBe('can set!')
523
560
 
@@ -537,13 +574,17 @@ it('returns allowed true when no permission errors occur', async () => {
537
574
  })
538
575
  const state = getDocumentState(instance, doc)
539
576
  const unsubscribe = state.subscribe()
540
- await applyDocumentActions(instance, createDocument(doc)).then((r) => r.submitted())
577
+ await applyDocumentActions(instance, {actions: [createDocument(doc)]}).then((r) => r.submitted())
541
578
 
542
579
  // Use an action that includes a patch (so that update permission check is bypassed).
543
580
  const permissionsState = getPermissionsState(instance, {
544
- ...doc,
545
- type: 'document.edit',
546
- patches: [{set: {title: 'New Title'}}],
581
+ actions: [
582
+ {
583
+ ...doc,
584
+ type: 'document.edit',
585
+ patches: [{set: {title: 'New Title'}}],
586
+ },
587
+ ],
547
588
  })
548
589
  // Wait briefly to allow permissions calculation.
549
590
  await new Promise((resolve) => setTimeout(resolve, 10))
@@ -555,7 +596,7 @@ it('returns allowed true when no permission errors occur', async () => {
555
596
  it("should reject applying the action if a precondition isn't met", async () => {
556
597
  const doc = createDocumentHandle({documentId: 'does-not-exist', documentType: 'article'})
557
598
 
558
- await expect(applyDocumentActions(instance, deleteDocument(doc))).rejects.toThrow(
599
+ await expect(applyDocumentActions(instance, {actions: [deleteDocument(doc)]})).rejects.toThrow(
559
600
  'The document you are trying to delete does not exist.',
560
601
  )
561
602
  })
@@ -566,7 +607,7 @@ it("should reject applying the action if a permission isn't met", async () => {
566
607
  const datasetAcl = [{filter: 'false', permissions: ['create']}]
567
608
  vi.mocked(client.request).mockResolvedValue(datasetAcl)
568
609
 
569
- await expect(applyDocumentActions(instance, createDocument(doc))).rejects.toThrow(
610
+ await expect(applyDocumentActions(instance, {actions: [createDocument(doc)]})).rejects.toThrow(
570
611
  'You do not have permission to create a draft for document "does-not-exist".',
571
612
  )
572
613
  })
@@ -576,7 +617,7 @@ it('returns allowed false with reasons when permission errors occur', async () =
576
617
  vi.mocked(client.request).mockResolvedValue(datasetAcl)
577
618
 
578
619
  const doc = createDocumentHandle({documentId: 'doc-perm-denied', documentType: 'article'})
579
- const result = await resolvePermissions(instance, createDocument(doc))
620
+ const result = await resolvePermissions(instance, {actions: [createDocument(doc)]})
580
621
 
581
622
  const message = 'You do not have permission to create a draft for document "doc-perm-denied".'
582
623
  expect(result).toMatchObject({
@@ -597,8 +638,10 @@ it('fetches dataset ACL and updates grants in the document store state', async (
597
638
  const book = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'book'})
598
639
  const author = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'author'})
599
640
 
600
- expect(await resolvePermissions(instance, createDocument(book))).toEqual({allowed: true})
601
- 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({
602
645
  allowed: false,
603
646
  message: expect.stringContaining('You do not have permission to create a draft for document'),
604
647
  })
@@ -611,10 +654,9 @@ it('returns a promise that resolves when a document has been loaded in the store
611
654
 
612
655
  // use one-off instance to create the document in the mock backend
613
656
  const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
614
- const result = await applyDocumentActions(oneOffInstance, [
615
- createDocument(doc),
616
- editDocument(doc, {set: {title: 'initial title'}}),
617
- ])
657
+ const result = await applyDocumentActions(oneOffInstance, {
658
+ actions: [createDocument(doc), editDocument(doc, {set: {title: 'initial title'}})],
659
+ })
618
660
  await result.submitted() // wait till submitted to server before resolving
619
661
 
620
662
  await expect(resolveDocument(instance, doc)).resolves.toMatchObject({
@@ -633,19 +675,23 @@ it('emits an event for each action after an outgoing transaction has been accept
633
675
  const doc = createDocumentHandle({documentId, documentType: 'article'})
634
676
  expect(handler).toHaveBeenCalledTimes(0)
635
677
 
636
- const tnx1 = await applyDocumentActions(instance, [
637
- createDocument(doc),
638
- editDocument(doc, {set: {title: 'new name'}}),
639
- publishDocument(doc),
640
- ]).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())
641
685
  expect(handler).toHaveBeenCalledTimes(4)
642
686
 
643
- const tnx2 = await applyDocumentActions(instance, [
644
- unpublishDocument(doc),
645
- publishDocument(doc),
646
- editDocument(doc, {set: {title: 'updated name'}}),
647
- discardDocument(doc),
648
- ]).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())
649
695
  expect(handler).toHaveBeenCalledTimes(9)
650
696
 
651
697
  expect(handler.mock.calls).toMatchObject([
@@ -660,7 +706,7 @@ it('emits an event for each action after an outgoing transaction has been accept
660
706
  [{type: 'accepted', outgoing: {transactionId: tnx2.transactionId}}],
661
707
  ])
662
708
 
663
- await applyDocumentActions(instance, deleteDocument(doc))
709
+ await applyDocumentActions(instance, {actions: [deleteDocument(doc)]})
664
710
 
665
711
  unsubscribe()
666
712
  })
@@ -905,12 +951,7 @@ beforeEach(() => {
905
951
  continue
906
952
  }
907
953
  default: {
908
- throw new Error(
909
- `Unsupported action for mock backend: ${
910
- // @ts-expect-error unexpected input
911
- i.actionType
912
- }`,
913
- )
954
+ throw new Error(`Unsupported action for mock backend: ${i.actionType}`)
914
955
  }
915
956
  }
916
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) =>