@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.
- package/dist/index.d.ts +35 -90
- package/dist/index.js +237 -111
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
- package/src/auth/authStore.test.ts +13 -13
- package/src/auth/refreshStampedToken.test.ts +16 -16
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +6 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -4
- package/src/comlink/controller/actions/destroyController.test.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +6 -6
- package/src/comlink/controller/actions/getOrCreateController.test.ts +5 -5
- package/src/comlink/controller/actions/getOrCreateController.ts +1 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +3 -2
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -4
- package/src/comlink/node/actions/getOrCreateNode.test.ts +7 -7
- package/src/comlink/node/actions/releaseNode.test.ts +2 -2
- package/src/comlink/node/comlinkNodeStore.test.ts +4 -3
- package/src/config/sanityConfig.ts +8 -3
- package/src/document/actions.ts +11 -7
- package/src/document/applyDocumentActions.test.ts +9 -6
- package/src/document/applyDocumentActions.ts +9 -49
- package/src/document/documentStore.test.ts +128 -115
- package/src/document/documentStore.ts +40 -10
- package/src/document/permissions.test.ts +9 -9
- package/src/document/permissions.ts +17 -7
- package/src/document/processActions.test.ts +248 -0
- package/src/document/processActions.ts +173 -0
- package/src/document/reducers.ts +13 -6
- package/src/presence/presenceStore.ts +13 -7
- package/src/preview/previewStore.test.ts +10 -2
- package/src/preview/previewStore.ts +2 -1
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +8 -5
- package/src/preview/subscribeToStateAndFetchBatches.ts +9 -3
- package/src/projection/projectionStore.test.ts +18 -2
- package/src/projection/projectionStore.ts +2 -1
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +6 -5
- package/src/projection/subscribeToStateAndFetchBatches.ts +9 -3
- package/src/releases/getPerspectiveState.ts +2 -2
- package/src/releases/releasesStore.ts +10 -4
- package/src/store/createActionBinder.test.ts +8 -6
- package/src/store/createActionBinder.ts +44 -29
- package/src/store/createStateSourceAction.test.ts +12 -11
- package/src/store/createStateSourceAction.ts +6 -6
- package/src/store/createStoreInstance.test.ts +29 -16
- package/src/store/createStoreInstance.ts +6 -5
- package/src/store/defineStore.test.ts +1 -1
- 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,
|
|
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,
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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,
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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,
|
|
238
|
-
(
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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,
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
395
|
-
|
|
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,
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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,
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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,
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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({
|
|
629
|
-
|
|
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
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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 {
|
|
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
|
-
[
|
|
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},
|
|
309
|
+
({instance}, options: PermissionsStateOptions) => {
|
|
280
310
|
return firstValueFrom(
|
|
281
|
-
getPermissionsState(instance,
|
|
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
|
-
|
|
443
|
-
|
|
472
|
+
key: {projectId, dataset},
|
|
473
|
+
}: StoreContext<DocumentStoreState, BoundDatasetKey>) => {
|
|
444
474
|
return getClientState(instance, {apiVersion: API_VERSION})
|
|
445
475
|
.observable.pipe(
|
|
446
476
|
switchMap((client) =>
|