@sanity/sdk 0.0.0-rc.6 → 0.0.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/README.md +7 -15
- package/dist/index.d.ts +562 -234
- package/dist/index.js +515 -256
- package/dist/index.js.map +1 -1
- package/package.json +12 -10
- package/src/_exports/index.ts +17 -2
- package/src/auth/dashboardUtils.test.ts +41 -0
- package/src/auth/dashboardUtils.ts +12 -0
- package/src/auth/getOrganizationVerificationState.test.ts +197 -0
- package/src/auth/getOrganizationVerificationState.ts +73 -0
- package/src/auth/handleAuthCallback.test.ts +2 -0
- package/src/auth/handleAuthCallback.ts +1 -0
- package/src/auth/logout.test.ts +1 -0
- package/src/auth/logout.ts +1 -0
- package/src/auth/refreshStampedToken.ts +1 -0
- package/src/auth/studioModeAuth.test.ts +1 -1
- package/src/auth/studioModeAuth.ts +1 -0
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +2 -0
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -0
- package/src/client/clientStore.ts +22 -18
- package/src/comlink/node/actions/releaseNode.ts +16 -14
- package/src/config/__tests__/handles.test.ts +30 -0
- package/src/config/handles.ts +67 -0
- package/src/config/sanityConfig.ts +44 -16
- package/src/document/actions.ts +188 -60
- package/src/document/applyDocumentActions.ts +12 -5
- package/src/document/documentStore.test.ts +70 -121
- package/src/document/documentStore.ts +57 -27
- package/src/document/patchOperations.test.ts +1 -1
- package/src/document/patchOperations.ts +39 -39
- package/src/document/sharedListener.ts +3 -1
- package/src/favorites/favorites.test.ts +237 -0
- package/src/favorites/favorites.ts +122 -0
- package/src/preview/resolvePreview.test.ts +3 -4
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/preview/subscribeToStateAndFetchBatches.ts +4 -2
- package/src/project/organizationVerification.test.ts +35 -0
- package/src/project/organizationVerification.ts +26 -0
- package/src/projection/getProjectionState.ts +36 -11
- package/src/projection/resolveProjection.test.ts +3 -4
- package/src/projection/resolveProjection.ts +35 -9
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +4 -2
- package/src/query/queryStore.test.ts +12 -12
- package/src/query/queryStore.ts +71 -42
- package/src/releases/getPerspectiveState.test.ts +192 -0
- package/src/releases/getPerspectiveState.ts +93 -0
- package/src/releases/releasesStore.test.ts +170 -0
- package/src/releases/releasesStore.ts +89 -0
- package/src/releases/utils/sortReleases.test.ts +336 -0
- package/src/releases/utils/sortReleases.ts +48 -0
- package/src/utils/listenQuery.test.ts +302 -0
- package/src/utils/listenQuery.ts +128 -0
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
import {type Mutation, type SanityDocument} from '@sanity/types'
|
|
15
15
|
import {evaluate, parse} from 'groq-js'
|
|
16
16
|
import {delay, first, firstValueFrom, from, Observable, of, ReplaySubject, Subject} from 'rxjs'
|
|
17
|
-
import {beforeEach, expect, it, vi} from 'vitest'
|
|
17
|
+
import {afterEach, beforeEach, expect, it, vi} from 'vitest'
|
|
18
18
|
|
|
19
19
|
import {getClientState} from '../client/clientStore'
|
|
20
|
-
import {
|
|
20
|
+
import {createDocumentHandle} from '../config/handles'
|
|
21
21
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
22
22
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
23
23
|
import {getDraftId, getPublishedId} from '../utils/ids'
|
|
@@ -45,6 +45,22 @@ import {type DocumentSet, processMutations} from './processMutations'
|
|
|
45
45
|
import {type HttpAction} from './reducers'
|
|
46
46
|
import {createFetchDocument, createSharedListener} from './sharedListener'
|
|
47
47
|
|
|
48
|
+
// Define a single generic TestDocument type
|
|
49
|
+
interface TestDocument extends SanityDocument {
|
|
50
|
+
_type: 'article'
|
|
51
|
+
title?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Scope the TestDocument type to the project/datasets used in tests
|
|
55
|
+
type AllTestSchemaTypes = TestDocument
|
|
56
|
+
|
|
57
|
+
// Augment the 'groq' module
|
|
58
|
+
declare module 'groq' {
|
|
59
|
+
interface SanitySchemas {
|
|
60
|
+
'default:default': AllTestSchemaTypes
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
let instance: SanityInstance
|
|
49
65
|
let instance1: SanityInstance
|
|
50
66
|
let instance2: SanityInstance
|
|
@@ -65,12 +81,7 @@ afterEach(() => {
|
|
|
65
81
|
})
|
|
66
82
|
|
|
67
83
|
it('creates, edits, and publishes a document', async () => {
|
|
68
|
-
|
|
69
|
-
title?: string
|
|
70
|
-
_type: 'article'
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const doc: DocumentHandle<Article> = {documentId: 'doc-single', documentType: 'article'}
|
|
84
|
+
const doc = createDocumentHandle({documentId: 'doc-single', documentType: 'article'})
|
|
74
85
|
const documentState = getDocumentState(instance, doc)
|
|
75
86
|
|
|
76
87
|
// Initially the document is undefined
|
|
@@ -100,12 +111,7 @@ it('creates, edits, and publishes a document', async () => {
|
|
|
100
111
|
})
|
|
101
112
|
|
|
102
113
|
it('edits existing documents', async () => {
|
|
103
|
-
|
|
104
|
-
_type: 'book'
|
|
105
|
-
title: string
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const doc: DocumentHandle<Book> = {documentId: 'existing-doc', documentType: 'book'}
|
|
114
|
+
const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article'})
|
|
109
115
|
const state = getDocumentState(instance, doc)
|
|
110
116
|
|
|
111
117
|
// not subscribed yet so the value is undefined
|
|
@@ -131,12 +137,7 @@ it('edits existing documents', async () => {
|
|
|
131
137
|
})
|
|
132
138
|
|
|
133
139
|
it('sets optimistic changes synchronously', async () => {
|
|
134
|
-
|
|
135
|
-
title?: string
|
|
136
|
-
_type: 'article'
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const doc: DocumentHandle<Article> = {documentId: 'optimistic', documentType: 'article'}
|
|
140
|
+
const doc = createDocumentHandle({documentId: 'optimistic', documentType: 'article'})
|
|
140
141
|
|
|
141
142
|
const state1 = getDocumentState(instance1, doc)
|
|
142
143
|
const state2 = getDocumentState(instance2, doc)
|
|
@@ -189,11 +190,7 @@ it('sets optimistic changes synchronously', async () => {
|
|
|
189
190
|
})
|
|
190
191
|
|
|
191
192
|
it('propagates changes between two instances', async () => {
|
|
192
|
-
|
|
193
|
-
_type: 'blog'
|
|
194
|
-
content?: string
|
|
195
|
-
}
|
|
196
|
-
const doc: DocumentHandle<Blog> = {documentId: 'doc-collab', documentType: 'blog'}
|
|
193
|
+
const doc = createDocumentHandle({documentId: 'doc-collab', documentType: 'article'})
|
|
197
194
|
const state1 = getDocumentState(instance1, doc)
|
|
198
195
|
const state2 = getDocumentState(instance2, doc)
|
|
199
196
|
|
|
@@ -209,26 +206,21 @@ it('propagates changes between two instances', async () => {
|
|
|
209
206
|
expect(doc2?._id).toEqual(getDraftId(doc.documentId))
|
|
210
207
|
|
|
211
208
|
// Now, edit the document from instance2.
|
|
212
|
-
await applyDocumentActions(instance2, editDocument(doc, {set: {
|
|
209
|
+
await applyDocumentActions(instance2, editDocument(doc, {set: {title: 'Hello world!'}})).then(
|
|
213
210
|
(r) => r.submitted(),
|
|
214
211
|
)
|
|
215
212
|
|
|
216
213
|
const updated1 = state1.getCurrent()
|
|
217
214
|
const updated2 = state2.getCurrent()
|
|
218
|
-
expect(updated1?.
|
|
219
|
-
expect(updated2?.
|
|
215
|
+
expect(updated1?.title).toEqual('Hello world!')
|
|
216
|
+
expect(updated2?.title).toEqual('Hello world!')
|
|
220
217
|
|
|
221
218
|
state1Unsubscribe()
|
|
222
219
|
state2Unsubscribe()
|
|
223
220
|
})
|
|
224
221
|
|
|
225
222
|
it('handles concurrent edits and resolves conflicts', async () => {
|
|
226
|
-
|
|
227
|
-
_type: 'note'
|
|
228
|
-
text?: string
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const doc: DocumentHandle<Note> = {documentId: 'doc-concurrent', documentType: 'note'}
|
|
223
|
+
const doc = createDocumentHandle({documentId: 'doc-concurrent', documentType: 'article'})
|
|
232
224
|
const state1 = getDocumentState(instance1, doc)
|
|
233
225
|
const state2 = getDocumentState(instance2, doc)
|
|
234
226
|
|
|
@@ -240,17 +232,17 @@ it('handles concurrent edits and resolves conflicts', async () => {
|
|
|
240
232
|
// Create the initial document from a one-off instance.
|
|
241
233
|
await applyDocumentActions(oneOffInstance, [
|
|
242
234
|
createDocument(doc),
|
|
243
|
-
editDocument(doc, {set: {
|
|
235
|
+
editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy dog'}}),
|
|
244
236
|
]).then((res) => res.submitted())
|
|
245
237
|
|
|
246
238
|
// Both instances now issue an edit simultaneously.
|
|
247
239
|
const p1 = applyDocumentActions(
|
|
248
240
|
instance1,
|
|
249
|
-
editDocument(doc, {set: {
|
|
241
|
+
editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy cat'}}),
|
|
250
242
|
).then((r) => r.submitted())
|
|
251
243
|
const p2 = applyDocumentActions(
|
|
252
244
|
instance2,
|
|
253
|
-
editDocument(doc, {set: {
|
|
245
|
+
editDocument(doc, {set: {title: 'The quick brown elephant jumps over the lazy dog'}}),
|
|
254
246
|
).then((r) => r.submitted())
|
|
255
247
|
|
|
256
248
|
// Wait for both actions to complete (or reject).
|
|
@@ -258,8 +250,8 @@ it('handles concurrent edits and resolves conflicts', async () => {
|
|
|
258
250
|
|
|
259
251
|
const finalDoc1 = state1.getCurrent()
|
|
260
252
|
const finalDoc2 = state2.getCurrent()
|
|
261
|
-
expect(finalDoc1?.
|
|
262
|
-
expect(finalDoc1?.
|
|
253
|
+
expect(finalDoc1?.title).toEqual(finalDoc2?.title)
|
|
254
|
+
expect(finalDoc1?.title).toBe('The quick brown elephant jumps over the lazy cat')
|
|
263
255
|
|
|
264
256
|
state1Unsubscribe()
|
|
265
257
|
state2Unsubscribe()
|
|
@@ -267,11 +259,7 @@ it('handles concurrent edits and resolves conflicts', async () => {
|
|
|
267
259
|
})
|
|
268
260
|
|
|
269
261
|
it('unpublishes and discards a document', async () => {
|
|
270
|
-
|
|
271
|
-
_type: 'post'
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const doc: DocumentHandle<Post> = {documentId: 'doc-pub-unpub', documentType: 'post'}
|
|
262
|
+
const doc = createDocumentHandle({documentId: 'doc-pub-unpub', documentType: 'article'})
|
|
275
263
|
const documentState = getDocumentState(instance, doc)
|
|
276
264
|
const unsubscribe = documentState.subscribe()
|
|
277
265
|
|
|
@@ -299,11 +287,7 @@ it('unpublishes and discards a document', async () => {
|
|
|
299
287
|
})
|
|
300
288
|
|
|
301
289
|
it('deletes a document', async () => {
|
|
302
|
-
|
|
303
|
-
_type: 'task'
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const doc: DocumentHandle<Task> = {documentId: 'doc-delete', documentType: 'task'}
|
|
290
|
+
const doc = createDocumentHandle({documentId: 'doc-delete', documentType: 'article'})
|
|
307
291
|
|
|
308
292
|
const documentState = getDocumentState(instance, doc)
|
|
309
293
|
const unsubscribe = documentState.subscribe()
|
|
@@ -321,11 +305,7 @@ it('deletes a document', async () => {
|
|
|
321
305
|
})
|
|
322
306
|
|
|
323
307
|
it('cleans up document state when there are no subscribers', async () => {
|
|
324
|
-
|
|
325
|
-
_type: 'event'
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const doc: DocumentHandle<Event> = {documentId: 'doc-cleanup', documentType: 'event'}
|
|
308
|
+
const doc = createDocumentHandle({documentId: 'doc-cleanup', documentType: 'article'})
|
|
329
309
|
const documentState = getDocumentState(instance, doc)
|
|
330
310
|
|
|
331
311
|
// Subscribe to the document state.
|
|
@@ -348,11 +328,7 @@ it('cleans up document state when there are no subscribers', async () => {
|
|
|
348
328
|
})
|
|
349
329
|
|
|
350
330
|
it('fetches documents if there are no active subscriptions for the actions applied', async () => {
|
|
351
|
-
|
|
352
|
-
_type: 'book'
|
|
353
|
-
title?: string
|
|
354
|
-
}
|
|
355
|
-
const doc: DocumentHandle<Book> = {documentId: 'existing-doc', documentType: 'book'}
|
|
331
|
+
const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article'})
|
|
356
332
|
|
|
357
333
|
const {getCurrent} = getDocumentState(instance, doc)
|
|
358
334
|
expect(getCurrent()).toBeUndefined()
|
|
@@ -398,11 +374,7 @@ it('fetches documents if there are no active subscriptions for the actions appli
|
|
|
398
374
|
})
|
|
399
375
|
|
|
400
376
|
it('batches edit transaction into one outgoing transaction', async () => {
|
|
401
|
-
|
|
402
|
-
_type: 'author'
|
|
403
|
-
name?: string
|
|
404
|
-
}
|
|
405
|
-
const doc: DocumentHandle<Author> = {documentId: crypto.randomUUID(), documentType: 'author'}
|
|
377
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
406
378
|
|
|
407
379
|
const unsubscribe = getDocumentState(instance, doc).subscribe()
|
|
408
380
|
|
|
@@ -410,10 +382,10 @@ it('batches edit transaction into one outgoing transaction', async () => {
|
|
|
410
382
|
applyDocumentActions(instance, createDocument(doc))
|
|
411
383
|
|
|
412
384
|
// these get batched into one
|
|
413
|
-
applyDocumentActions(instance, editDocument(doc, {set: {
|
|
414
|
-
applyDocumentActions(instance, editDocument(doc, {set: {
|
|
415
|
-
applyDocumentActions(instance, editDocument(doc, {set: {
|
|
416
|
-
const res = await applyDocumentActions(instance, editDocument(doc, {set: {
|
|
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!!!!'}}))
|
|
417
389
|
await res.submitted()
|
|
418
390
|
|
|
419
391
|
expect(client.action).toHaveBeenCalledTimes(2)
|
|
@@ -426,11 +398,7 @@ it('batches edit transaction into one outgoing transaction', async () => {
|
|
|
426
398
|
})
|
|
427
399
|
|
|
428
400
|
it('provides the consistency status via `getDocumentSyncStatus`', async () => {
|
|
429
|
-
|
|
430
|
-
_type: 'author'
|
|
431
|
-
name?: string
|
|
432
|
-
}
|
|
433
|
-
const doc: DocumentHandle<Author> = {documentId: crypto.randomUUID(), documentType: 'author'}
|
|
401
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
434
402
|
|
|
435
403
|
const syncStatus = getDocumentSyncStatus(instance, doc)
|
|
436
404
|
expect(syncStatus.getCurrent()).toBeUndefined()
|
|
@@ -447,10 +415,10 @@ it('provides the consistency status via `getDocumentSyncStatus`', async () => {
|
|
|
447
415
|
await createResult.submitted()
|
|
448
416
|
expect(syncStatus.getCurrent()).toBe(true)
|
|
449
417
|
|
|
450
|
-
applyDocumentActions(instance, editDocument(doc, {set: {
|
|
418
|
+
applyDocumentActions(instance, editDocument(doc, {set: {title: 'initial name'}}))
|
|
451
419
|
expect(syncStatus.getCurrent()).toBe(false)
|
|
452
420
|
|
|
453
|
-
applyDocumentActions(instance, editDocument(doc, {set: {
|
|
421
|
+
applyDocumentActions(instance, editDocument(doc, {set: {title: 'updated name'}}))
|
|
454
422
|
const publishResult = applyDocumentActions(instance, publishDocument(doc))
|
|
455
423
|
expect(syncStatus.getCurrent()).toBe(false)
|
|
456
424
|
await publishResult.then((res) => res.submitted())
|
|
@@ -476,33 +444,29 @@ it('reverts failed outgoing transaction locally', async () => {
|
|
|
476
444
|
})
|
|
477
445
|
})
|
|
478
446
|
|
|
479
|
-
|
|
480
|
-
_type: 'author'
|
|
481
|
-
name?: string
|
|
482
|
-
}
|
|
483
|
-
const doc: DocumentHandle<Author> = {documentId: crypto.randomUUID(), documentType: 'author'}
|
|
447
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
484
448
|
|
|
485
449
|
const {getCurrent, subscribe} = getDocumentState(instance, doc)
|
|
486
450
|
const unsubscribe = subscribe()
|
|
487
451
|
|
|
488
452
|
await applyDocumentActions(instance, createDocument(doc))
|
|
489
|
-
applyDocumentActions(instance, editDocument(doc, {set: {
|
|
490
|
-
applyDocumentActions(instance, editDocument(doc, {set: {
|
|
453
|
+
applyDocumentActions(instance, editDocument(doc, {set: {title: 'the'}}))
|
|
454
|
+
applyDocumentActions(instance, editDocument(doc, {set: {title: 'the quick'}}))
|
|
491
455
|
|
|
492
456
|
// this edit action is simulated to fail from the backend and will be reverted
|
|
493
457
|
const revertedActionResult = applyDocumentActions(
|
|
494
458
|
instance,
|
|
495
|
-
editDocument(doc, {set: {
|
|
459
|
+
editDocument(doc, {set: {title: 'the quick brown'}}),
|
|
496
460
|
{
|
|
497
461
|
transactionId: 'force-revert',
|
|
498
462
|
disableBatching: true,
|
|
499
463
|
},
|
|
500
464
|
)
|
|
501
465
|
|
|
502
|
-
applyDocumentActions(instance, editDocument(doc, {set: {
|
|
466
|
+
applyDocumentActions(instance, editDocument(doc, {set: {title: 'the quick brown fox'}}))
|
|
503
467
|
await applyDocumentActions(
|
|
504
468
|
instance,
|
|
505
|
-
editDocument(doc, {set: {
|
|
469
|
+
editDocument(doc, {set: {title: 'the quick brown fox jumps'}}),
|
|
506
470
|
).then((e) => e.submitted())
|
|
507
471
|
|
|
508
472
|
await expect(revertedEventPromise).resolves.toMatchObject({
|
|
@@ -517,11 +481,11 @@ it('reverts failed outgoing transaction locally', async () => {
|
|
|
517
481
|
)
|
|
518
482
|
|
|
519
483
|
// notice how `brown ` is gone
|
|
520
|
-
expect(getCurrent()?.
|
|
484
|
+
expect(getCurrent()?.title).toBe('the quick fox jumps')
|
|
521
485
|
|
|
522
486
|
// check that we can still edit after recovering from the error
|
|
523
|
-
applyDocumentActions(instance, editDocument(doc, {set: {
|
|
524
|
-
expect(getCurrent()?.
|
|
487
|
+
applyDocumentActions(instance, editDocument(doc, {set: {title: 'TEST the quick fox jumps'}}))
|
|
488
|
+
expect(getCurrent()?.title).toBe('TEST the quick fox jumps')
|
|
525
489
|
|
|
526
490
|
unsubscribe()
|
|
527
491
|
vi.mocked(client.action).mockImplementation(clientActionMockImplementation)
|
|
@@ -537,17 +501,12 @@ it('removes a queued transaction if it fails to apply', async () => {
|
|
|
537
501
|
})
|
|
538
502
|
})
|
|
539
503
|
|
|
540
|
-
|
|
541
|
-
_type: 'author'
|
|
542
|
-
name?: string
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const doc: DocumentHandle<Author> = {documentId: crypto.randomUUID(), documentType: 'author'}
|
|
504
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
546
505
|
const state = getDocumentState(instance, doc)
|
|
547
506
|
const unsubscribe = state.subscribe()
|
|
548
507
|
|
|
549
508
|
await expect(
|
|
550
|
-
applyDocumentActions(instance, editDocument(doc, {set: {
|
|
509
|
+
applyDocumentActions(instance, editDocument(doc, {set: {title: "can't set"}})),
|
|
551
510
|
).rejects.toThrowError(/Cannot edit document/)
|
|
552
511
|
|
|
553
512
|
await expect(actionErrorEventPromise).resolves.toMatchObject({
|
|
@@ -558,9 +517,9 @@ it('removes a queued transaction if it fails to apply', async () => {
|
|
|
558
517
|
|
|
559
518
|
// editing should still work after though (no crashing)
|
|
560
519
|
await applyDocumentActions(instance, createDocument(doc))
|
|
561
|
-
applyDocumentActions(instance, editDocument(doc, {set: {
|
|
520
|
+
applyDocumentActions(instance, editDocument(doc, {set: {title: 'can set!'}}))
|
|
562
521
|
|
|
563
|
-
expect(state.getCurrent()?.
|
|
522
|
+
expect(state.getCurrent()?.title).toBe('can set!')
|
|
564
523
|
|
|
565
524
|
unsubscribe()
|
|
566
525
|
})
|
|
@@ -572,10 +531,10 @@ it('returns allowed true when no permission errors occur', async () => {
|
|
|
572
531
|
client.observable.request = vi.fn().mockReturnValue(of(datasetAcl))
|
|
573
532
|
|
|
574
533
|
// Create a document and subscribe to it.
|
|
575
|
-
const doc
|
|
534
|
+
const doc = createDocumentHandle({
|
|
576
535
|
documentId: 'doc-perm-allowed',
|
|
577
536
|
documentType: 'article',
|
|
578
|
-
}
|
|
537
|
+
})
|
|
579
538
|
const state = getDocumentState(instance, doc)
|
|
580
539
|
const unsubscribe = state.subscribe()
|
|
581
540
|
await applyDocumentActions(instance, createDocument(doc)).then((r) => r.submitted())
|
|
@@ -594,7 +553,7 @@ it('returns allowed true when no permission errors occur', async () => {
|
|
|
594
553
|
})
|
|
595
554
|
|
|
596
555
|
it("should reject applying the action if a precondition isn't met", async () => {
|
|
597
|
-
const doc
|
|
556
|
+
const doc = createDocumentHandle({documentId: 'does-not-exist', documentType: 'article'})
|
|
598
557
|
|
|
599
558
|
await expect(applyDocumentActions(instance, deleteDocument(doc))).rejects.toThrow(
|
|
600
559
|
'The document you are trying to delete does not exist.',
|
|
@@ -602,7 +561,7 @@ it("should reject applying the action if a precondition isn't met", async () =>
|
|
|
602
561
|
})
|
|
603
562
|
|
|
604
563
|
it("should reject applying the action if a permission isn't met", async () => {
|
|
605
|
-
const doc
|
|
564
|
+
const doc = createDocumentHandle({documentId: 'does-not-exist', documentType: 'article'})
|
|
606
565
|
|
|
607
566
|
const datasetAcl = [{filter: 'false', permissions: ['create']}]
|
|
608
567
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
@@ -616,7 +575,7 @@ it('returns allowed false with reasons when permission errors occur', async () =
|
|
|
616
575
|
const datasetAcl = [{filter: 'false', permissions: ['create']}]
|
|
617
576
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
618
577
|
|
|
619
|
-
const doc
|
|
578
|
+
const doc = createDocumentHandle({documentId: 'doc-perm-denied', documentType: 'article'})
|
|
620
579
|
const result = await resolvePermissions(instance, createDocument(doc))
|
|
621
580
|
|
|
622
581
|
const message = 'You do not have permission to create a draft for document "doc-perm-denied".'
|
|
@@ -634,11 +593,9 @@ it('fetches dataset ACL and updates grants in the document store state', async (
|
|
|
634
593
|
{filter: '_type=="author"', permissions: ['update']},
|
|
635
594
|
]
|
|
636
595
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
637
|
-
type Book = {_type: 'book'} & SanityDocument
|
|
638
|
-
type Author = {_type: 'author'} & SanityDocument
|
|
639
596
|
|
|
640
|
-
const book
|
|
641
|
-
const author
|
|
597
|
+
const book = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'book'})
|
|
598
|
+
const author = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'author'})
|
|
642
599
|
|
|
643
600
|
expect(await resolvePermissions(instance, createDocument(book))).toEqual({allowed: true})
|
|
644
601
|
expect(await resolvePermissions(instance, createDocument(author))).toMatchObject({
|
|
@@ -648,13 +605,9 @@ it('fetches dataset ACL and updates grants in the document store state', async (
|
|
|
648
605
|
})
|
|
649
606
|
|
|
650
607
|
it('returns a promise that resolves when a document has been loaded in the store (useful for suspense)', async () => {
|
|
651
|
-
|
|
652
|
-
_type: 'book'
|
|
653
|
-
title?: string
|
|
654
|
-
}
|
|
655
|
-
const doc: DocumentHandle<Book> = {documentId: crypto.randomUUID(), documentType: 'book'}
|
|
608
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
656
609
|
|
|
657
|
-
expect(await resolveDocument
|
|
610
|
+
expect(await resolveDocument(instance, doc)).toBe(null)
|
|
658
611
|
|
|
659
612
|
// use one-off instance to create the document in the mock backend
|
|
660
613
|
const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
@@ -664,9 +617,9 @@ it('returns a promise that resolves when a document has been loaded in the store
|
|
|
664
617
|
])
|
|
665
618
|
await result.submitted() // wait till submitted to server before resolving
|
|
666
619
|
|
|
667
|
-
await expect(resolveDocument
|
|
620
|
+
await expect(resolveDocument(instance, doc)).resolves.toMatchObject({
|
|
668
621
|
_id: getDraftId(doc.documentId),
|
|
669
|
-
_type: '
|
|
622
|
+
_type: 'article',
|
|
670
623
|
title: 'initial title',
|
|
671
624
|
})
|
|
672
625
|
oneOffInstance.dispose()
|
|
@@ -676,17 +629,13 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
676
629
|
const handler = vi.fn()
|
|
677
630
|
const unsubscribe = subscribeDocumentEvents(instance, handler)
|
|
678
631
|
|
|
679
|
-
interface Author extends SanityDocument {
|
|
680
|
-
_type: 'author'
|
|
681
|
-
name?: string
|
|
682
|
-
}
|
|
683
632
|
const documentId = crypto.randomUUID()
|
|
684
|
-
const doc
|
|
633
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
685
634
|
expect(handler).toHaveBeenCalledTimes(0)
|
|
686
635
|
|
|
687
636
|
const tnx1 = await applyDocumentActions(instance, [
|
|
688
637
|
createDocument(doc),
|
|
689
|
-
editDocument(doc, {set: {
|
|
638
|
+
editDocument(doc, {set: {title: 'new name'}}),
|
|
690
639
|
publishDocument(doc),
|
|
691
640
|
]).then((e) => e.submitted())
|
|
692
641
|
expect(handler).toHaveBeenCalledTimes(4)
|
|
@@ -694,7 +643,7 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
694
643
|
const tnx2 = await applyDocumentActions(instance, [
|
|
695
644
|
unpublishDocument(doc),
|
|
696
645
|
publishDocument(doc),
|
|
697
|
-
editDocument(doc, {set: {
|
|
646
|
+
editDocument(doc, {set: {title: 'updated name'}}),
|
|
698
647
|
discardDocument(doc),
|
|
699
648
|
]).then((e) => e.submitted())
|
|
700
649
|
expect(handler).toHaveBeenCalledTimes(9)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {type Action} from '@sanity/client'
|
|
2
2
|
import {getPublishedId} from '@sanity/client/csm'
|
|
3
3
|
import {type SanityDocument} from '@sanity/types'
|
|
4
|
+
import {type SanityDocumentResult} from 'groq'
|
|
4
5
|
import {type ExprNode} from 'groq-js'
|
|
5
6
|
import {
|
|
6
7
|
catchError,
|
|
@@ -36,7 +37,7 @@ import {type DocumentAction} from './actions'
|
|
|
36
37
|
import {API_VERSION, INITIAL_OUTGOING_THROTTLE_TIME} from './documentConstants'
|
|
37
38
|
import {type DocumentEvent, getDocumentEvents} from './events'
|
|
38
39
|
import {listen, OutOfSyncError} from './listen'
|
|
39
|
-
import {type JsonMatch, jsonMatch
|
|
40
|
+
import {type JsonMatch, jsonMatch} from './patchOperations'
|
|
40
41
|
import {calculatePermissions, createGrantsLookup, type DatasetAcl, type Grant} from './permissions'
|
|
41
42
|
import {ActionError} from './processActions'
|
|
42
43
|
import {
|
|
@@ -129,37 +130,60 @@ export const documentStore = defineStore<DocumentStoreState>({
|
|
|
129
130
|
},
|
|
130
131
|
})
|
|
131
132
|
|
|
133
|
+
/**
|
|
134
|
+
* @beta
|
|
135
|
+
* Options for specifying a document and optionally a path within it.
|
|
136
|
+
*/
|
|
137
|
+
export interface DocumentOptions<
|
|
138
|
+
TPath extends string | undefined = undefined,
|
|
139
|
+
TDocumentType extends string = string,
|
|
140
|
+
TDataset extends string = string,
|
|
141
|
+
TProjectId extends string = string,
|
|
142
|
+
> extends DocumentHandle<TDocumentType, TDataset, TProjectId> {
|
|
143
|
+
path?: TPath
|
|
144
|
+
}
|
|
145
|
+
|
|
132
146
|
/** @beta */
|
|
133
147
|
export function getDocumentState<
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
TDocumentType extends string = string,
|
|
149
|
+
TDataset extends string = string,
|
|
150
|
+
TProjectId extends string = string,
|
|
136
151
|
>(
|
|
137
152
|
instance: SanityInstance,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
options: DocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
|
|
154
|
+
): StateSource<SanityDocumentResult<TDocumentType, TDataset, TProjectId> | undefined | null>
|
|
155
|
+
|
|
141
156
|
/** @beta */
|
|
142
|
-
export function getDocumentState<
|
|
157
|
+
export function getDocumentState<
|
|
158
|
+
TPath extends string = string,
|
|
159
|
+
TDocumentType extends string = string,
|
|
160
|
+
TDataset extends string = string,
|
|
161
|
+
TProjectId extends string = string,
|
|
162
|
+
>(
|
|
143
163
|
instance: SanityInstance,
|
|
144
|
-
|
|
145
|
-
): StateSource<
|
|
164
|
+
options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
|
|
165
|
+
): StateSource<
|
|
166
|
+
JsonMatch<SanityDocumentResult<TDocumentType, TDataset, TProjectId>, TPath> | undefined
|
|
167
|
+
>
|
|
168
|
+
|
|
146
169
|
/** @beta */
|
|
147
|
-
export function getDocumentState(
|
|
170
|
+
export function getDocumentState<TData>(
|
|
148
171
|
instance: SanityInstance,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
172
|
+
options: DocumentOptions<string | undefined>,
|
|
173
|
+
): StateSource<TData | undefined | null>
|
|
174
|
+
|
|
152
175
|
/** @beta */
|
|
153
176
|
export function getDocumentState(
|
|
154
177
|
...args: Parameters<typeof _getDocumentState>
|
|
155
178
|
): StateSource<unknown> {
|
|
156
179
|
return _getDocumentState(...args)
|
|
157
180
|
}
|
|
181
|
+
|
|
158
182
|
const _getDocumentState = bindActionByDataset(
|
|
159
183
|
documentStore,
|
|
160
184
|
createStateSourceAction({
|
|
161
|
-
selector: ({state: {error, documentStates}},
|
|
162
|
-
const documentId =
|
|
185
|
+
selector: ({state: {error, documentStates}}, options: DocumentOptions<string | undefined>) => {
|
|
186
|
+
const {documentId, path} = options
|
|
163
187
|
if (error) throw error
|
|
164
188
|
const draftId = getDraftId(documentId)
|
|
165
189
|
const publishedId = getPublishedId(documentId)
|
|
@@ -171,21 +195,25 @@ const _getDocumentState = bindActionByDataset(
|
|
|
171
195
|
if (path) return jsonMatch(document, path).at(0)?.value
|
|
172
196
|
return document
|
|
173
197
|
},
|
|
174
|
-
onSubscribe: (context,
|
|
175
|
-
manageSubscriberIds(context,
|
|
198
|
+
onSubscribe: (context, options: DocumentOptions<string | undefined>) =>
|
|
199
|
+
manageSubscriberIds(context, options.documentId),
|
|
176
200
|
}),
|
|
177
201
|
)
|
|
178
202
|
|
|
179
203
|
/** @beta */
|
|
180
|
-
export function resolveDocument<
|
|
204
|
+
export function resolveDocument<
|
|
205
|
+
TDocumentType extends string = string,
|
|
206
|
+
TDataset extends string = string,
|
|
207
|
+
TProjectId extends string = string,
|
|
208
|
+
>(
|
|
181
209
|
instance: SanityInstance,
|
|
182
|
-
|
|
183
|
-
): Promise<
|
|
210
|
+
docHandle: DocumentHandle<TDocumentType, TDataset, TProjectId>,
|
|
211
|
+
): Promise<SanityDocumentResult<TDocumentType, TDataset, TProjectId> | null>
|
|
184
212
|
/** @beta */
|
|
185
|
-
export function resolveDocument(
|
|
213
|
+
export function resolveDocument<TData extends SanityDocument>(
|
|
186
214
|
instance: SanityInstance,
|
|
187
|
-
|
|
188
|
-
): Promise<
|
|
215
|
+
docHandle: DocumentHandle<string, string, string>,
|
|
216
|
+
): Promise<TData | null>
|
|
189
217
|
/** @beta */
|
|
190
218
|
export function resolveDocument(
|
|
191
219
|
...args: Parameters<typeof _resolveDocument>
|
|
@@ -194,11 +222,13 @@ export function resolveDocument(
|
|
|
194
222
|
}
|
|
195
223
|
const _resolveDocument = bindActionByDataset(
|
|
196
224
|
documentStore,
|
|
197
|
-
({instance},
|
|
198
|
-
const documentId = typeof doc === 'string' ? doc : doc.documentId
|
|
225
|
+
({instance}, docHandle: DocumentHandle<string, string, string>) => {
|
|
199
226
|
return firstValueFrom(
|
|
200
|
-
getDocumentState(instance,
|
|
201
|
-
|
|
227
|
+
getDocumentState(instance, {
|
|
228
|
+
...docHandle,
|
|
229
|
+
path: undefined,
|
|
230
|
+
}).observable.pipe(filter((i) => i !== undefined)),
|
|
231
|
+
) as Promise<SanityDocument | null>
|
|
202
232
|
},
|
|
203
233
|
)
|
|
204
234
|
|
|
@@ -102,7 +102,7 @@ describe('parsePath', () => {
|
|
|
102
102
|
})
|
|
103
103
|
|
|
104
104
|
it('throws an error when bracket content is invalid', () => {
|
|
105
|
-
expect(() => parsePath('a[invalid]')).toThrowError('Invalid bracket content:
|
|
105
|
+
expect(() => parsePath('a[invalid]')).toThrowError('Invalid bracket content: "[invalid]"')
|
|
106
106
|
})
|
|
107
107
|
})
|
|
108
108
|
|