@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.
Files changed (53) hide show
  1. package/README.md +7 -15
  2. package/dist/index.d.ts +562 -234
  3. package/dist/index.js +515 -256
  4. package/dist/index.js.map +1 -1
  5. package/package.json +12 -10
  6. package/src/_exports/index.ts +17 -2
  7. package/src/auth/dashboardUtils.test.ts +41 -0
  8. package/src/auth/dashboardUtils.ts +12 -0
  9. package/src/auth/getOrganizationVerificationState.test.ts +197 -0
  10. package/src/auth/getOrganizationVerificationState.ts +73 -0
  11. package/src/auth/handleAuthCallback.test.ts +2 -0
  12. package/src/auth/handleAuthCallback.ts +1 -0
  13. package/src/auth/logout.test.ts +1 -0
  14. package/src/auth/logout.ts +1 -0
  15. package/src/auth/refreshStampedToken.ts +1 -0
  16. package/src/auth/studioModeAuth.test.ts +1 -1
  17. package/src/auth/studioModeAuth.ts +1 -0
  18. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +2 -0
  19. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -0
  20. package/src/client/clientStore.ts +22 -18
  21. package/src/comlink/node/actions/releaseNode.ts +16 -14
  22. package/src/config/__tests__/handles.test.ts +30 -0
  23. package/src/config/handles.ts +67 -0
  24. package/src/config/sanityConfig.ts +44 -16
  25. package/src/document/actions.ts +188 -60
  26. package/src/document/applyDocumentActions.ts +12 -5
  27. package/src/document/documentStore.test.ts +70 -121
  28. package/src/document/documentStore.ts +57 -27
  29. package/src/document/patchOperations.test.ts +1 -1
  30. package/src/document/patchOperations.ts +39 -39
  31. package/src/document/sharedListener.ts +3 -1
  32. package/src/favorites/favorites.test.ts +237 -0
  33. package/src/favorites/favorites.ts +122 -0
  34. package/src/preview/resolvePreview.test.ts +3 -4
  35. package/src/preview/subscribeToStateAndFetchBatches.test.ts +1 -1
  36. package/src/preview/subscribeToStateAndFetchBatches.ts +4 -2
  37. package/src/project/organizationVerification.test.ts +35 -0
  38. package/src/project/organizationVerification.ts +26 -0
  39. package/src/projection/getProjectionState.ts +36 -11
  40. package/src/projection/resolveProjection.test.ts +3 -4
  41. package/src/projection/resolveProjection.ts +35 -9
  42. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  43. package/src/projection/subscribeToStateAndFetchBatches.ts +4 -2
  44. package/src/query/queryStore.test.ts +12 -12
  45. package/src/query/queryStore.ts +71 -42
  46. package/src/releases/getPerspectiveState.test.ts +192 -0
  47. package/src/releases/getPerspectiveState.ts +93 -0
  48. package/src/releases/releasesStore.test.ts +170 -0
  49. package/src/releases/releasesStore.ts +89 -0
  50. package/src/releases/utils/sortReleases.test.ts +336 -0
  51. package/src/releases/utils/sortReleases.ts +48 -0
  52. package/src/utils/listenQuery.test.ts +302 -0
  53. 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 {type DocumentHandle} from '../config/sanityConfig'
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
- interface Article extends SanityDocument {
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
- interface Book extends SanityDocument {
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
- interface Article extends SanityDocument {
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
- interface Blog extends SanityDocument {
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: {content: 'Hello world!'}})).then(
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?.content).toEqual('Hello world!')
219
- expect(updated2?.content).toEqual('Hello world!')
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
- interface Note extends SanityDocument {
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: {text: 'The quick brown fox jumps over the lazy dog'}}),
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: {text: 'The quick brown fox jumps over the lazy cat'}}),
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: {text: 'The quick brown elephant jumps over the lazy dog'}}),
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?.text).toEqual(finalDoc2?.text)
262
- expect(finalDoc1?.text).toBe('The quick brown elephant jumps over the lazy cat')
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
- interface Post extends SanityDocument {
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
- interface Task extends SanityDocument {
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
- interface Event extends SanityDocument {
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
- interface Book extends SanityDocument {
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
- interface Author extends SanityDocument {
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: {name: 'name!'}}))
414
- applyDocumentActions(instance, editDocument(doc, {set: {name: 'name!!'}}))
415
- applyDocumentActions(instance, editDocument(doc, {set: {name: 'name!!!'}}))
416
- const res = await applyDocumentActions(instance, editDocument(doc, {set: {name: 'name!!!!'}}))
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
- interface Author extends SanityDocument {
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: {name: 'initial name'}}))
418
+ applyDocumentActions(instance, editDocument(doc, {set: {title: 'initial name'}}))
451
419
  expect(syncStatus.getCurrent()).toBe(false)
452
420
 
453
- applyDocumentActions(instance, editDocument(doc, {set: {name: 'updated name'}}))
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
- interface Author extends SanityDocument {
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: {name: 'the'}}))
490
- applyDocumentActions(instance, editDocument(doc, {set: {name: 'the quick'}}))
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: {name: 'the quick brown'}}),
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: {name: 'the quick brown fox'}}))
466
+ applyDocumentActions(instance, editDocument(doc, {set: {title: 'the quick brown fox'}}))
503
467
  await applyDocumentActions(
504
468
  instance,
505
- editDocument(doc, {set: {name: 'the quick brown fox jumps'}}),
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()?.name).toBe('the quick fox jumps')
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: {name: 'TEST the quick fox jumps'}}))
524
- expect(getCurrent()?.name).toBe('TEST the quick fox jumps')
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
- interface Author extends SanityDocument {
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: {name: "can't 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: {name: 'can set!'}}))
520
+ applyDocumentActions(instance, editDocument(doc, {set: {title: 'can set!'}}))
562
521
 
563
- expect(state.getCurrent()?.name).toBe('can set!')
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: DocumentHandle<SanityDocument> = {
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: DocumentHandle = {documentId: 'does-not-exist', documentType: 'book'}
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: DocumentHandle = {documentId: 'does-not-exist', documentType: 'book'}
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: DocumentHandle = {documentId: 'doc-perm-denied', documentType: 'article'}
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: DocumentHandle<Book> = {documentId: crypto.randomUUID(), documentType: 'book'}
641
- const author: DocumentHandle<Author> = {documentId: crypto.randomUUID(), documentType: '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
- interface Book extends SanityDocument {
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<Book>(instance, doc)).toBe(null)
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<Book>(instance, doc)).resolves.toMatchObject({
620
+ await expect(resolveDocument(instance, doc)).resolves.toMatchObject({
668
621
  _id: getDraftId(doc.documentId),
669
- _type: 'book',
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: DocumentHandle<Author> = {documentId, documentType: 'author'}
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: {name: 'new name'}}),
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: {name: 'updated name'}}),
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, type JsonMatchPath} from './patchOperations'
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
- TDocument extends SanityDocument,
135
- TPath extends JsonMatchPath<TDocument>,
148
+ TDocumentType extends string = string,
149
+ TDataset extends string = string,
150
+ TProjectId extends string = string,
136
151
  >(
137
152
  instance: SanityInstance,
138
- doc: string | DocumentHandle<TDocument>,
139
- path: TPath,
140
- ): StateSource<JsonMatch<TDocument, TPath> | undefined>
153
+ options: DocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
154
+ ): StateSource<SanityDocumentResult<TDocumentType, TDataset, TProjectId> | undefined | null>
155
+
141
156
  /** @beta */
142
- export function getDocumentState<TDocument extends SanityDocument>(
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
- doc: string | DocumentHandle<TDocument>,
145
- ): StateSource<TDocument | null>
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
- doc: string | DocumentHandle,
150
- path?: string,
151
- ): StateSource<unknown>
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}}, doc: string | DocumentHandle, path?: string) => {
162
- const documentId = typeof doc === 'string' ? doc : doc.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, doc: string | DocumentHandle) =>
175
- manageSubscriberIds(context, typeof doc === 'string' ? doc : doc.documentId),
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<TDocument extends SanityDocument>(
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
- doc: string | DocumentHandle<TDocument>,
183
- ): Promise<TDocument | null>
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
- doc: string | DocumentHandle,
188
- ): Promise<SanityDocument | null>
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}, doc: string | DocumentHandle) => {
198
- const documentId = typeof doc === 'string' ? doc : doc.documentId
225
+ ({instance}, docHandle: DocumentHandle<string, string, string>) => {
199
226
  return firstValueFrom(
200
- getDocumentState(instance, documentId).observable.pipe(filter((i) => i !== undefined)),
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: [invalid]')
105
+ expect(() => parsePath('a[invalid]')).toThrowError('Invalid bracket content: "[invalid]"')
106
106
  })
107
107
  })
108
108