@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.
- package/dist/index.d.ts +173 -105
- package/dist/index.js +354 -122
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/src/_exports/index.ts +30 -0
- package/src/agent/agentActions.test.ts +81 -0
- package/src/agent/agentActions.ts +139 -0
- 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/auth/utils.ts +36 -0
- package/src/client/clientStore.test.ts +151 -0
- package/src/client/clientStore.ts +39 -1
- 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 +49 -3
- package/src/document/actions.test.ts +34 -0
- package/src/document/actions.ts +31 -7
- package/src/document/applyDocumentActions.test.ts +9 -6
- package/src/document/applyDocumentActions.ts +9 -49
- package/src/document/documentStore.test.ts +148 -107
- 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 +345 -0
- package/src/document/processActions.ts +185 -2
- 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/query/queryStore.ts +7 -4
- 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 +50 -14
- 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
|
|
|
@@ -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,
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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,
|
|
210
|
-
(
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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,
|
|
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
|
-
|
|
358
|
-
|
|
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
|
-
|
|
367
|
-
|
|
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,
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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,
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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,
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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({
|
|
601
|
-
|
|
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
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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 {
|
|
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) =>
|