@sanity/sdk 0.0.0-alpha.21 → 0.0.0-alpha.23

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 (127) hide show
  1. package/dist/index.d.ts +428 -325
  2. package/dist/index.js +1618 -1553
  3. package/dist/index.js.map +1 -1
  4. package/package.json +6 -7
  5. package/src/_exports/index.ts +31 -30
  6. package/src/auth/authStore.test.ts +149 -104
  7. package/src/auth/authStore.ts +51 -100
  8. package/src/auth/handleAuthCallback.test.ts +67 -34
  9. package/src/auth/handleAuthCallback.ts +8 -7
  10. package/src/auth/logout.test.ts +61 -29
  11. package/src/auth/logout.ts +26 -28
  12. package/src/auth/refreshStampedToken.test.ts +9 -9
  13. package/src/auth/refreshStampedToken.ts +62 -56
  14. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +5 -5
  15. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +45 -47
  16. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -5
  17. package/src/auth/subscribeToStorageEventsAndSetToken.ts +22 -24
  18. package/src/client/clientStore.test.ts +131 -67
  19. package/src/client/clientStore.ts +117 -116
  20. package/src/comlink/controller/actions/destroyController.test.ts +38 -13
  21. package/src/comlink/controller/actions/destroyController.ts +11 -15
  22. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +56 -27
  23. package/src/comlink/controller/actions/getOrCreateChannel.ts +37 -35
  24. package/src/comlink/controller/actions/getOrCreateController.test.ts +27 -16
  25. package/src/comlink/controller/actions/getOrCreateController.ts +23 -22
  26. package/src/comlink/controller/actions/releaseChannel.test.ts +37 -13
  27. package/src/comlink/controller/actions/releaseChannel.ts +22 -21
  28. package/src/comlink/controller/comlinkControllerStore.test.ts +65 -36
  29. package/src/comlink/controller/comlinkControllerStore.ts +44 -5
  30. package/src/comlink/node/actions/getOrCreateNode.test.ts +31 -15
  31. package/src/comlink/node/actions/getOrCreateNode.ts +30 -29
  32. package/src/comlink/node/actions/releaseNode.test.ts +75 -55
  33. package/src/comlink/node/actions/releaseNode.ts +19 -21
  34. package/src/comlink/node/comlinkNodeStore.test.ts +6 -11
  35. package/src/comlink/node/comlinkNodeStore.ts +22 -5
  36. package/src/config/authConfig.ts +79 -0
  37. package/src/config/sanityConfig.ts +48 -0
  38. package/src/datasets/datasets.test.ts +2 -2
  39. package/src/datasets/datasets.ts +18 -5
  40. package/src/document/actions.test.ts +22 -10
  41. package/src/document/actions.ts +44 -56
  42. package/src/document/applyDocumentActions.test.ts +96 -36
  43. package/src/document/applyDocumentActions.ts +140 -99
  44. package/src/document/documentStore.test.ts +103 -155
  45. package/src/document/documentStore.ts +247 -237
  46. package/src/document/listen.ts +56 -55
  47. package/src/document/patchOperations.ts +0 -43
  48. package/src/document/permissions.test.ts +25 -12
  49. package/src/document/permissions.ts +11 -4
  50. package/src/document/processActions.test.ts +41 -8
  51. package/src/document/reducers.test.ts +87 -16
  52. package/src/document/reducers.ts +2 -2
  53. package/src/document/sharedListener.test.ts +34 -16
  54. package/src/document/sharedListener.ts +33 -11
  55. package/src/preview/getPreviewState.test.ts +40 -39
  56. package/src/preview/getPreviewState.ts +68 -56
  57. package/src/preview/previewConstants.ts +43 -0
  58. package/src/preview/previewQuery.test.ts +1 -1
  59. package/src/preview/previewQuery.ts +4 -5
  60. package/src/preview/previewStore.test.ts +13 -58
  61. package/src/preview/previewStore.ts +7 -21
  62. package/src/preview/resolvePreview.test.ts +33 -104
  63. package/src/preview/resolvePreview.ts +11 -21
  64. package/src/preview/subscribeToStateAndFetchBatches.test.ts +96 -97
  65. package/src/preview/subscribeToStateAndFetchBatches.ts +85 -81
  66. package/src/preview/util.ts +1 -0
  67. package/src/project/project.test.ts +3 -3
  68. package/src/project/project.ts +28 -5
  69. package/src/projection/getProjectionState.test.ts +69 -49
  70. package/src/projection/getProjectionState.ts +42 -50
  71. package/src/projection/projectionQuery.ts +1 -1
  72. package/src/projection/projectionStore.test.ts +13 -51
  73. package/src/projection/projectionStore.ts +6 -18
  74. package/src/projection/resolveProjection.test.ts +32 -127
  75. package/src/projection/resolveProjection.ts +15 -28
  76. package/src/projection/subscribeToStateAndFetchBatches.test.ts +105 -90
  77. package/src/projection/subscribeToStateAndFetchBatches.ts +94 -81
  78. package/src/projection/util.ts +2 -0
  79. package/src/projects/projects.test.ts +13 -4
  80. package/src/projects/projects.ts +6 -1
  81. package/src/query/queryStore.test.ts +10 -47
  82. package/src/query/queryStore.ts +151 -133
  83. package/src/query/queryStoreConstants.ts +2 -0
  84. package/src/store/createActionBinder.test.ts +153 -0
  85. package/src/store/createActionBinder.ts +176 -0
  86. package/src/store/createSanityInstance.test.ts +84 -0
  87. package/src/store/createSanityInstance.ts +124 -0
  88. package/src/store/createStateSourceAction.test.ts +196 -0
  89. package/src/store/createStateSourceAction.ts +260 -0
  90. package/src/store/createStoreInstance.test.ts +81 -0
  91. package/src/store/createStoreInstance.ts +80 -0
  92. package/src/store/createStoreState.test.ts +85 -0
  93. package/src/store/createStoreState.ts +92 -0
  94. package/src/store/defineStore.test.ts +18 -0
  95. package/src/store/defineStore.ts +81 -0
  96. package/src/users/reducers.test.ts +318 -0
  97. package/src/users/reducers.ts +88 -0
  98. package/src/users/types.ts +46 -4
  99. package/src/users/usersConstants.ts +4 -0
  100. package/src/users/usersStore.test.ts +350 -223
  101. package/src/users/usersStore.ts +285 -149
  102. package/src/utils/createFetcherStore.test.ts +6 -7
  103. package/src/utils/createFetcherStore.ts +150 -153
  104. package/src/{common/util.test.ts → utils/hashString.test.ts} +1 -1
  105. package/src/auth/fetchLoginUrls.test.ts +0 -163
  106. package/src/auth/fetchLoginUrls.ts +0 -74
  107. package/src/common/createLiveEventSubscriber.test.ts +0 -121
  108. package/src/common/createLiveEventSubscriber.ts +0 -55
  109. package/src/common/types.ts +0 -4
  110. package/src/instance/identity.test.ts +0 -46
  111. package/src/instance/identity.ts +0 -29
  112. package/src/instance/sanityInstance.test.ts +0 -77
  113. package/src/instance/sanityInstance.ts +0 -57
  114. package/src/instance/types.ts +0 -37
  115. package/src/preview/getPreviewProjection.ts +0 -45
  116. package/src/resources/README.md +0 -370
  117. package/src/resources/createAction.test.ts +0 -101
  118. package/src/resources/createAction.ts +0 -44
  119. package/src/resources/createResource.test.ts +0 -112
  120. package/src/resources/createResource.ts +0 -102
  121. package/src/resources/createStateSourceAction.test.ts +0 -114
  122. package/src/resources/createStateSourceAction.ts +0 -83
  123. package/src/resources/createStore.test.ts +0 -67
  124. package/src/resources/createStore.ts +0 -46
  125. package/src/store/createStore.test.ts +0 -108
  126. package/src/store/createStore.ts +0 -106
  127. /package/src/{common/util.ts → utils/hashString.ts} +0 -0
@@ -13,16 +13,14 @@ import {
13
13
  } from '@sanity/client'
14
14
  import {type Mutation, type SanityDocument} from '@sanity/types'
15
15
  import {evaluate, parse} from 'groq-js'
16
- import {delay, first, firstValueFrom, from, map, Observable, of, ReplaySubject, Subject} from 'rxjs'
16
+ import {delay, first, firstValueFrom, from, Observable, of, ReplaySubject, Subject} from 'rxjs'
17
17
  import {beforeEach, expect, it, vi} from 'vitest'
18
18
 
19
19
  import {getClientState} from '../client/clientStore'
20
- import {createSanityInstance} from '../instance/sanityInstance'
21
- import {type SanityInstance} from '../instance/types'
22
- import {getOrCreateResource} from '../resources/createResource'
23
- import {type StateSource} from '../resources/createStateSourceAction'
20
+ import {type DocumentHandle} from '../config/sanityConfig'
21
+ import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
22
+ import {type StateSource} from '../store/createStateSourceAction'
24
23
  import {getDraftId, getPublishedId} from '../utils/ids'
25
- import {evaluateSync} from './_synchronous-groq-js.mjs'
26
24
  import {
27
25
  createDocument,
28
26
  deleteDocument,
@@ -34,7 +32,6 @@ import {
34
32
  import {applyDocumentActions} from './applyDocumentActions'
35
33
  import {diffPatch} from './diffPatch'
36
34
  import {
37
- documentStore,
38
35
  getDocumentState,
39
36
  getDocumentSyncStatus,
40
37
  getPermissionsState,
@@ -43,21 +40,37 @@ import {
43
40
  subscribeDocumentEvents,
44
41
  } from './documentStore'
45
42
  import {type ActionErrorEvent, type TransactionRevertedEvent} from './events'
46
- import {type DocumentHandle} from './patchOperations'
47
43
  import {type DatasetAcl} from './permissions'
48
44
  import {type DocumentSet, processMutations} from './processMutations'
49
45
  import {type HttpAction} from './reducers'
50
46
  import {createFetchDocument, createSharedListener} from './sharedListener'
51
47
 
52
- it('creates, edits, and publishes a document', async () => {
53
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
48
+ let instance: SanityInstance
49
+ let instance1: SanityInstance
50
+ let instance2: SanityInstance
51
+
52
+ beforeEach(() => {
53
+ instance = createSanityInstance({projectId: 'p', dataset: 'd'})
54
+ // test uses two instances that share the same in-memory dataset, but separate
55
+ // store instances. in real scenarios, this would be separate machines but with
56
+ // the same project + dataset
57
+ instance1 = createSanityInstance({projectId: 'p', dataset: 'd1'})
58
+ instance2 = createSanityInstance({projectId: 'p', dataset: 'd2'})
59
+ })
60
+
61
+ afterEach(() => {
62
+ instance?.dispose()
63
+ instance1?.dispose()
64
+ instance2?.dispose()
65
+ })
54
66
 
67
+ it('creates, edits, and publishes a document', async () => {
55
68
  interface Article extends SanityDocument {
56
69
  title?: string
57
70
  _type: 'article'
58
71
  }
59
72
 
60
- const doc: DocumentHandle<Article> = {_id: 'doc-single', _type: 'article'}
73
+ const doc: DocumentHandle<Article> = {documentId: 'doc-single', documentType: 'article'}
61
74
  const documentState = getDocumentState(instance, doc)
62
75
 
63
76
  // Initially the document is undefined
@@ -67,10 +80,10 @@ it('creates, edits, and publishes a document', async () => {
67
80
 
68
81
  // Create a new document
69
82
  const {appeared} = await applyDocumentActions(instance, createDocument(doc))
70
- expect(appeared).toContain(getDraftId(doc._id))
83
+ expect(appeared).toContain(getDraftId(doc.documentId))
71
84
 
72
85
  let currentDoc = documentState.getCurrent()
73
- expect(currentDoc?._id).toEqual(getDraftId(doc._id))
86
+ expect(currentDoc?._id).toEqual(getDraftId(doc.documentId))
74
87
 
75
88
  // Edit the document – add a title
76
89
  await applyDocumentActions(instance, editDocument(doc, {set: {title: 'My First Article'}}))
@@ -82,11 +95,8 @@ it('creates, edits, and publishes a document', async () => {
82
95
  await submitted()
83
96
  currentDoc = documentState.getCurrent()
84
97
 
85
- expect(currentDoc).toMatchObject({_id: doc._id, _rev: transactionId})
98
+ expect(currentDoc).toMatchObject({_id: doc.documentId, _rev: transactionId})
86
99
  unsubscribe()
87
-
88
- checkUnverified(instance, doc._id)
89
- instance.dispose()
90
100
  })
91
101
 
92
102
  it('edits existing documents', async () => {
@@ -95,9 +105,7 @@ it('edits existing documents', async () => {
95
105
  title: string
96
106
  }
97
107
 
98
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
99
-
100
- const doc: DocumentHandle<Book> = {_id: 'existing-doc', _type: 'book'}
108
+ const doc: DocumentHandle<Book> = {documentId: 'existing-doc', documentType: 'book'}
101
109
  const state = getDocumentState(instance, doc)
102
110
 
103
111
  // not subscribed yet so the value is undefined
@@ -109,31 +117,26 @@ it('edits existing documents', async () => {
109
117
  await firstValueFrom(state.observable.pipe(first((i) => !!i)))
110
118
 
111
119
  expect(state.getCurrent()).toMatchObject({
112
- _id: getDraftId(doc._id),
120
+ _id: getDraftId(doc.documentId),
113
121
  title: 'existing doc',
114
122
  })
115
123
 
116
124
  await applyDocumentActions(instance, editDocument(doc, {set: {title: 'updated title'}}))
117
125
  expect(state.getCurrent()).toMatchObject({
118
- _id: getDraftId(doc._id),
126
+ _id: getDraftId(doc.documentId),
119
127
  title: 'updated title',
120
128
  })
121
129
 
122
- checkUnverified(instance, doc._id)
123
130
  unsubscribe()
124
- instance.dispose()
125
131
  })
126
132
 
127
133
  it('sets optimistic changes synchronously', async () => {
128
- const instance1 = createSanityInstance({projectId: 'p', dataset: 'd'})
129
- const instance2 = createSanityInstance({projectId: 'p', dataset: 'd'})
130
-
131
134
  interface Article extends SanityDocument {
132
135
  title?: string
133
136
  _type: 'article'
134
137
  }
135
138
 
136
- const doc: DocumentHandle<Article> = {_id: 'optimistic', _type: 'article'}
139
+ const doc: DocumentHandle<Article> = {documentId: 'optimistic', documentType: 'article'}
137
140
 
138
141
  const state1 = getDocumentState(instance1, doc)
139
142
  const state2 = getDocumentState(instance2, doc)
@@ -147,7 +150,7 @@ it('sets optimistic changes synchronously', async () => {
147
150
  // then the actions are synchronous
148
151
  expect(state1.getCurrent()).toBeNull()
149
152
  applyDocumentActions(instance1, createDocument(doc))
150
- expect(state1.getCurrent()).toMatchObject({_id: getDraftId(doc._id)})
153
+ expect(state1.getCurrent()).toMatchObject({_id: getDraftId(doc.documentId)})
151
154
  const actionResult1Promise = applyDocumentActions(
152
155
  instance1,
153
156
  editDocument(doc, {set: {title: 'initial title'}}),
@@ -181,23 +184,16 @@ it('sets optimistic changes synchronously', async () => {
181
184
  await actionResult2.submitted()
182
185
  expect(state1.getCurrent()?.title).toBe('updated title')
183
186
 
184
- checkUnverified(instance1, doc._id)
185
- checkUnverified(instance2, doc._id)
186
187
  unsubscribe1()
187
188
  unsubscribe2()
188
- instance1.dispose()
189
- instance2.dispose()
190
189
  })
191
190
 
192
191
  it('propagates changes between two instances', async () => {
193
- const instance1 = createSanityInstance({projectId: 'p', dataset: 'd'})
194
- const instance2 = createSanityInstance({projectId: 'p', dataset: 'd'})
195
-
196
192
  interface Blog extends SanityDocument {
197
193
  _type: 'blog'
198
194
  content?: string
199
195
  }
200
- const doc: DocumentHandle<Blog> = {_id: 'doc-collab', _type: 'blog'}
196
+ const doc: DocumentHandle<Blog> = {documentId: 'doc-collab', documentType: 'blog'}
201
197
  const state1 = getDocumentState(instance1, doc)
202
198
  const state2 = getDocumentState(instance2, doc)
203
199
 
@@ -209,8 +205,8 @@ it('propagates changes between two instances', async () => {
209
205
 
210
206
  const doc1 = state1.getCurrent()
211
207
  const doc2 = state2.getCurrent()
212
- expect(doc1?._id).toEqual(getDraftId(doc._id))
213
- expect(doc2?._id).toEqual(getDraftId(doc._id))
208
+ expect(doc1?._id).toEqual(getDraftId(doc.documentId))
209
+ expect(doc2?._id).toEqual(getDraftId(doc.documentId))
214
210
 
215
211
  // Now, edit the document from instance2.
216
212
  await applyDocumentActions(instance2, editDocument(doc, {set: {content: 'Hello world!'}})).then(
@@ -224,32 +220,25 @@ it('propagates changes between two instances', async () => {
224
220
 
225
221
  state1Unsubscribe()
226
222
  state2Unsubscribe()
227
-
228
- checkUnverified(instance1, doc._id)
229
- checkUnverified(instance2, doc._id)
230
-
231
- instance1.dispose()
232
- instance2.dispose()
233
223
  })
234
224
 
235
225
  it('handles concurrent edits and resolves conflicts', async () => {
236
- const instance1 = createSanityInstance({projectId: 'p', dataset: 'd'})
237
- const instance2 = createSanityInstance({projectId: 'p', dataset: 'd'})
238
-
239
226
  interface Note extends SanityDocument {
240
227
  _type: 'note'
241
228
  text?: string
242
229
  }
243
230
 
244
- const doc: DocumentHandle<Note> = {_id: 'doc-concurrent', _type: 'note'}
231
+ const doc: DocumentHandle<Note> = {documentId: 'doc-concurrent', documentType: 'note'}
245
232
  const state1 = getDocumentState(instance1, doc)
246
233
  const state2 = getDocumentState(instance2, doc)
247
234
 
248
235
  const state1Unsubscribe = state1.subscribe()
249
236
  const state2Unsubscribe = state2.subscribe()
250
237
 
238
+ const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
239
+
251
240
  // Create the initial document from a one-off instance.
252
- await applyDocumentActions(createSanityInstance({projectId: 'p', dataset: 'd'}), [
241
+ await applyDocumentActions(oneOffInstance, [
253
242
  createDocument(doc),
254
243
  editDocument(doc, {set: {text: 'The quick brown fox jumps over the lazy dog'}}),
255
244
  ]).then((res) => res.submitted())
@@ -272,24 +261,17 @@ it('handles concurrent edits and resolves conflicts', async () => {
272
261
  expect(finalDoc1?.text).toEqual(finalDoc2?.text)
273
262
  expect(finalDoc1?.text).toBe('The quick brown elephant jumps over the lazy cat')
274
263
 
275
- checkUnverified(instance1, doc._id)
276
- checkUnverified(instance2, doc._id)
277
-
278
264
  state1Unsubscribe()
279
265
  state2Unsubscribe()
280
-
281
- instance1.dispose()
282
- instance2.dispose()
266
+ oneOffInstance.dispose()
283
267
  })
284
268
 
285
269
  it('unpublishes and discards a document', async () => {
286
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
287
-
288
270
  interface Post extends SanityDocument {
289
271
  _type: 'post'
290
272
  }
291
273
 
292
- const doc: DocumentHandle<Post> = {_id: 'doc-pub-unpub', _type: 'post'}
274
+ const doc: DocumentHandle<Post> = {documentId: 'doc-pub-unpub', documentType: 'post'}
293
275
  const documentState = getDocumentState(instance, doc)
294
276
  const unsubscribe = documentState.subscribe()
295
277
 
@@ -298,7 +280,7 @@ it('unpublishes and discards a document', async () => {
298
280
  const afterPublish = await applyDocumentActions(instance, publishDocument(doc))
299
281
  const publishedDoc = documentState.getCurrent()
300
282
  expect(publishedDoc).toMatchObject({
301
- _id: getPublishedId(doc._id),
283
+ _id: getPublishedId(doc.documentId),
302
284
  _rev: afterPublish.transactionId,
303
285
  })
304
286
 
@@ -306,27 +288,22 @@ it('unpublishes and discards a document', async () => {
306
288
  await applyDocumentActions(instance, unpublishDocument(doc))
307
289
  const afterUnpublish = documentState.getCurrent()
308
290
  // In our mock implementation the _id remains the same but the published copy is removed.
309
- expect(afterUnpublish?._id).toEqual(getDraftId(doc._id))
291
+ expect(afterUnpublish?._id).toEqual(getDraftId(doc.documentId))
310
292
 
311
293
  // Discard the draft (which deletes the draft version).
312
294
  await applyDocumentActions(instance, discardDocument(doc))
313
295
  const afterDiscard = documentState.getCurrent()
314
296
  expect(afterDiscard).toBeNull()
315
297
 
316
- checkUnverified(instance, doc._id)
317
298
  unsubscribe()
318
-
319
- instance.dispose()
320
299
  })
321
300
 
322
301
  it('deletes a document', async () => {
323
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
324
-
325
302
  interface Task extends SanityDocument {
326
303
  _type: 'task'
327
304
  }
328
305
 
329
- const doc: DocumentHandle<Task> = {_id: 'doc-delete', _type: 'task'}
306
+ const doc: DocumentHandle<Task> = {documentId: 'doc-delete', documentType: 'task'}
330
307
 
331
308
  const documentState = getDocumentState(instance, doc)
332
309
  const unsubscribe = documentState.subscribe()
@@ -340,20 +317,15 @@ it('deletes a document', async () => {
340
317
  const afterDelete = documentState.getCurrent()
341
318
  expect(afterDelete).toBeNull()
342
319
 
343
- checkUnverified(instance, doc._id)
344
320
  unsubscribe()
345
-
346
- instance.dispose()
347
321
  })
348
322
 
349
323
  it('cleans up document state when there are no subscribers', async () => {
350
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
351
-
352
324
  interface Event extends SanityDocument {
353
325
  _type: 'event'
354
326
  }
355
327
 
356
- const doc: DocumentHandle<Event> = {_id: 'doc-cleanup', _type: 'event'}
328
+ const doc: DocumentHandle<Event> = {documentId: 'doc-cleanup', documentType: 'event'}
357
329
  const documentState = getDocumentState(instance, doc)
358
330
 
359
331
  // Subscribe to the document state.
@@ -364,7 +336,7 @@ it('cleans up document state when there are no subscribers', async () => {
364
336
  expect(documentState.getCurrent()).toBeDefined()
365
337
 
366
338
  // Unsubscribe from the document.
367
- checkUnverified(instance, doc._id)
339
+
368
340
  unsubscribe()
369
341
 
370
342
  // Wait longer than DOCUMENT_STATE_CLEAR_DELAY (our mock sets it to 25ms)
@@ -373,26 +345,25 @@ it('cleans up document state when there are no subscribers', async () => {
373
345
  // When a new subscriber is created, if the state was cleared it should return undefined.
374
346
  const newDocumentState = getDocumentState(instance, doc)
375
347
  expect(newDocumentState.getCurrent()).toBeUndefined()
376
- instance.dispose()
377
348
  })
378
349
 
379
350
  it('fetches documents if there are no active subscriptions for the actions applied', async () => {
380
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
381
-
382
351
  interface Book extends SanityDocument {
383
352
  _type: 'book'
384
353
  title?: string
385
354
  }
386
- const doc: DocumentHandle<Book> = {_id: 'existing-doc', _type: 'book'}
355
+ const doc: DocumentHandle<Book> = {documentId: 'existing-doc', documentType: 'book'}
387
356
 
388
357
  const {getCurrent} = getDocumentState(instance, doc)
389
358
  expect(getCurrent()).toBeUndefined()
359
+ expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBeUndefined()
390
360
 
391
361
  // there are no active subscriptions so applying this action will create one
392
362
  // for this action. this subscription will be removed when the outgoing
393
363
  // transaction for this action has been accepted by the server
394
364
  const setNewTitle = applyDocumentActions(instance, editDocument(doc, {set: {title: 'new title'}}))
395
365
  expect(getCurrent()?.title).toBeUndefined()
366
+ expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBe(false)
396
367
 
397
368
  await setNewTitle
398
369
  expect(getCurrent()?.title).toBe('new title')
@@ -403,6 +374,8 @@ it('fetches documents if there are no active subscriptions for the actions appli
403
374
  applyDocumentActions(instance, editDocument(doc, {set: {title: 'updated title!'}}))
404
375
  expect(getCurrent()?.title).toBe('updated title!')
405
376
 
377
+ expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBe(false)
378
+
406
379
  // await submitted in order to test that there is no subscriptions
407
380
  const result = await applyDocumentActions(
408
381
  instance,
@@ -411,8 +384,7 @@ it('fetches documents if there are no active subscriptions for the actions appli
411
384
  await result.submitted()
412
385
 
413
386
  // test that there isn't any document state
414
- const {documentStates} = getOrCreateResource(instance, documentStore).state.get()
415
- expect(documentStates).toEqual({})
387
+ expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBeUndefined()
416
388
 
417
389
  const setNewNewTitle = applyDocumentActions(
418
390
  instance,
@@ -423,17 +395,14 @@ it('fetches documents if there are no active subscriptions for the actions appli
423
395
 
424
396
  await setNewNewTitle
425
397
  expect(getCurrent()?.title).toBe('new new title')
426
- instance.dispose()
427
398
  })
428
399
 
429
400
  it('batches edit transaction into one outgoing transaction', async () => {
430
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
431
-
432
401
  interface Author extends SanityDocument {
433
402
  _type: 'author'
434
403
  name?: string
435
404
  }
436
- const doc: DocumentHandle<Author> = {_id: crypto.randomUUID(), _type: 'author'}
405
+ const doc: DocumentHandle<Author> = {documentId: crypto.randomUUID(), documentType: 'author'}
437
406
 
438
407
  const unsubscribe = getDocumentState(instance, doc).subscribe()
439
408
 
@@ -454,19 +423,14 @@ it('batches edit transaction into one outgoing transaction', async () => {
454
423
  expect(actions.every(({actionType}) => actionType === 'sanity.action.document.edit')).toBe(true)
455
424
 
456
425
  unsubscribe()
457
- checkUnverified(instance, doc._id)
458
-
459
- instance.dispose()
460
426
  })
461
427
 
462
428
  it('provides the consistency status via `getDocumentSyncStatus`', async () => {
463
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
464
-
465
429
  interface Author extends SanityDocument {
466
430
  _type: 'author'
467
431
  name?: string
468
432
  }
469
- const doc: DocumentHandle<Author> = {_id: crypto.randomUUID(), _type: 'author'}
433
+ const doc: DocumentHandle<Author> = {documentId: crypto.randomUUID(), documentType: 'author'}
470
434
 
471
435
  const syncStatus = getDocumentSyncStatus(instance, doc)
472
436
  expect(syncStatus.getCurrent()).toBeUndefined()
@@ -503,8 +467,6 @@ it('reverts failed outgoing transaction locally', async () => {
503
467
  return await clientActionMockImplementation(...args)
504
468
  })
505
469
 
506
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
507
-
508
470
  const revertedEventPromise = new Promise<TransactionRevertedEvent>((resolve) => {
509
471
  const unsubscribe = subscribeDocumentEvents(instance, (e) => {
510
472
  if (e.type === 'reverted') {
@@ -518,7 +480,7 @@ it('reverts failed outgoing transaction locally', async () => {
518
480
  _type: 'author'
519
481
  name?: string
520
482
  }
521
- const doc: DocumentHandle<Author> = {_id: crypto.randomUUID(), _type: 'author'}
483
+ const doc: DocumentHandle<Author> = {documentId: crypto.randomUUID(), documentType: 'author'}
522
484
 
523
485
  const {getCurrent, subscribe} = getDocumentState(instance, doc)
524
486
  const unsubscribe = subscribe()
@@ -561,14 +523,11 @@ it('reverts failed outgoing transaction locally', async () => {
561
523
  applyDocumentActions(instance, editDocument(doc, {set: {name: 'TEST the quick fox jumps'}}))
562
524
  expect(getCurrent()?.name).toBe('TEST the quick fox jumps')
563
525
 
564
- checkUnverified(instance, doc._id)
565
526
  unsubscribe()
566
527
  vi.mocked(client.action).mockImplementation(clientActionMockImplementation)
567
528
  })
568
529
 
569
530
  it('removes a queued transaction if it fails to apply', async () => {
570
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
571
-
572
531
  const actionErrorEventPromise = new Promise<ActionErrorEvent>((resolve) => {
573
532
  const unsubscribe = subscribeDocumentEvents(instance, (e) => {
574
533
  if (e.type === 'error') {
@@ -583,7 +542,7 @@ it('removes a queued transaction if it fails to apply', async () => {
583
542
  name?: string
584
543
  }
585
544
 
586
- const doc: DocumentHandle<Author> = {_id: crypto.randomUUID(), _type: 'author'}
545
+ const doc: DocumentHandle<Author> = {documentId: crypto.randomUUID(), documentType: 'author'}
587
546
  const state = getDocumentState(instance, doc)
588
547
  const unsubscribe = state.subscribe()
589
548
 
@@ -592,7 +551,7 @@ it('removes a queued transaction if it fails to apply', async () => {
592
551
  ).rejects.toThrowError(/Cannot edit document/)
593
552
 
594
553
  await expect(actionErrorEventPromise).resolves.toMatchObject({
595
- documentId: doc._id,
554
+ documentId: doc.documentId,
596
555
  type: 'error',
597
556
  message: expect.stringContaining('Cannot edit document'),
598
557
  })
@@ -607,21 +566,23 @@ it('removes a queued transaction if it fails to apply', async () => {
607
566
  })
608
567
 
609
568
  it('returns allowed true when no permission errors occur', async () => {
610
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
611
569
  // Simulate a dataset ACL that allows all permissions.
612
570
  const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
613
571
  // Override the client mock to return our dataset ACL.
614
572
  client.observable.request = vi.fn().mockReturnValue(of(datasetAcl))
615
573
 
616
574
  // Create a document and subscribe to it.
617
- const doc: DocumentHandle<SanityDocument> = {_id: 'doc-perm-allowed', _type: 'article'}
575
+ const doc: DocumentHandle<SanityDocument> = {
576
+ documentId: 'doc-perm-allowed',
577
+ documentType: 'article',
578
+ }
618
579
  const state = getDocumentState(instance, doc)
619
580
  const unsubscribe = state.subscribe()
620
581
  await applyDocumentActions(instance, createDocument(doc)).then((r) => r.submitted())
621
582
 
622
583
  // Use an action that includes a patch (so that update permission check is bypassed).
623
584
  const permissionsState = getPermissionsState(instance, {
624
- documentId: doc._id,
585
+ ...doc,
625
586
  type: 'document.edit',
626
587
  patches: [{set: {title: 'New Title'}}],
627
588
  })
@@ -630,12 +591,10 @@ it('returns allowed true when no permission errors occur', async () => {
630
591
  expect(permissionsState.getCurrent()).toEqual({allowed: true})
631
592
 
632
593
  unsubscribe()
633
- instance.dispose()
634
594
  })
635
595
 
636
596
  it("should reject applying the action if a precondition isn't met", async () => {
637
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
638
- const doc: DocumentHandle = {_id: 'does-not-exist', _type: 'book'}
597
+ const doc: DocumentHandle = {documentId: 'does-not-exist', documentType: 'book'}
639
598
 
640
599
  await expect(applyDocumentActions(instance, deleteDocument(doc))).rejects.toThrow(
641
600
  'The document you are trying to delete does not exist.',
@@ -643,8 +602,7 @@ it("should reject applying the action if a precondition isn't met", async () =>
643
602
  })
644
603
 
645
604
  it("should reject applying the action if a permission isn't met", async () => {
646
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
647
- const doc: DocumentHandle = {_id: 'does-not-exist', _type: 'book'}
605
+ const doc: DocumentHandle = {documentId: 'does-not-exist', documentType: 'book'}
648
606
 
649
607
  const datasetAcl = [{filter: 'false', permissions: ['create']}]
650
608
  vi.mocked(client.request).mockResolvedValue(datasetAcl)
@@ -655,11 +613,10 @@ it("should reject applying the action if a permission isn't met", async () => {
655
613
  })
656
614
 
657
615
  it('returns allowed false with reasons when permission errors occur', async () => {
658
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
659
616
  const datasetAcl = [{filter: 'false', permissions: ['create']}]
660
617
  vi.mocked(client.request).mockResolvedValue(datasetAcl)
661
618
 
662
- const doc: DocumentHandle = {_id: 'doc-perm-denied', _type: 'article'}
619
+ const doc: DocumentHandle = {documentId: 'doc-perm-denied', documentType: 'article'}
663
620
  const result = await resolvePermissions(instance, createDocument(doc))
664
621
 
665
622
  const message = 'You do not have permission to create a draft for document "doc-perm-denied".'
@@ -668,63 +625,54 @@ it('returns allowed false with reasons when permission errors occur', async () =
668
625
  message,
669
626
  reasons: [{message, documentId: 'doc-perm-denied', type: 'access'}],
670
627
  })
671
- instance.dispose()
672
628
  })
673
629
 
674
630
  it('fetches dataset ACL and updates grants in the document store state', async () => {
675
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
676
631
  // Simulate a dataset ACL response.
677
632
  const datasetAcl = [
678
- {filter: 'true', permissions: ['read', 'update']},
679
- {filter: 'true', permissions: ['create']},
680
- {filter: 'true', permissions: ['history']},
633
+ {filter: '_type=="book"', permissions: ['read', 'update', 'create']},
634
+ {filter: '_type=="author"', permissions: ['update']},
681
635
  ]
682
636
  vi.mocked(client.request).mockResolvedValue(datasetAcl)
683
- const resource = getOrCreateResource(instance, documentStore)
684
- const grants = await firstValueFrom(
685
- resource.state.observable.pipe(
686
- map((s) => s.grants),
687
- first(Boolean),
688
- ),
689
- )
690
- // Check that each grant expression evaluates to true for a dummy document.
691
- const dummyDoc = {_id: 'dummy', _type: 'test'}
692
- for (const key of ['read', 'update', 'create', 'history'] as const) {
693
- expect(grants[key]).toBeDefined()
694
- const value = evaluateSync(grants[key], {params: {document: dummyDoc}}).get()
695
- expect(value).toBe(true)
696
- }
697
- instance.dispose()
637
+ type Book = {_type: 'book'} & SanityDocument
638
+ type Author = {_type: 'author'} & SanityDocument
639
+
640
+ const book: DocumentHandle<Book> = {documentId: crypto.randomUUID(), documentType: 'book'}
641
+ const author: DocumentHandle<Author> = {documentId: crypto.randomUUID(), documentType: 'author'}
642
+
643
+ expect(await resolvePermissions(instance, createDocument(book))).toEqual({allowed: true})
644
+ expect(await resolvePermissions(instance, createDocument(author))).toMatchObject({
645
+ allowed: false,
646
+ message: expect.stringContaining('You do not have permission to create a draft for document'),
647
+ })
698
648
  })
699
649
 
700
650
  it('returns a promise that resolves when a document has been loaded in the store (useful for suspense)', async () => {
701
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
702
-
703
651
  interface Book extends SanityDocument {
704
652
  _type: 'book'
705
653
  title?: string
706
654
  }
707
- const doc: DocumentHandle<Book> = {_id: crypto.randomUUID(), _type: 'book'}
655
+ const doc: DocumentHandle<Book> = {documentId: crypto.randomUUID(), documentType: 'book'}
708
656
 
709
657
  expect(await resolveDocument<Book>(instance, doc)).toBe(null)
710
658
 
711
659
  // use one-off instance to create the document in the mock backend
712
- const result = await applyDocumentActions(createSanityInstance({projectId: 'p', dataset: 'd'}), [
660
+ const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
661
+ const result = await applyDocumentActions(oneOffInstance, [
713
662
  createDocument(doc),
714
663
  editDocument(doc, {set: {title: 'initial title'}}),
715
664
  ])
716
665
  await result.submitted() // wait till submitted to server before resolving
717
666
 
718
667
  await expect(resolveDocument<Book>(instance, doc)).resolves.toMatchObject({
719
- _id: getDraftId(doc._id),
668
+ _id: getDraftId(doc.documentId),
720
669
  _type: 'book',
721
670
  title: 'initial title',
722
671
  })
672
+ oneOffInstance.dispose()
723
673
  })
724
674
 
725
675
  it('emits an event for each action after an outgoing transaction has been accepted', async () => {
726
- const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
727
-
728
676
  const handler = vi.fn()
729
677
  const unsubscribe = subscribeDocumentEvents(instance, handler)
730
678
 
@@ -732,7 +680,8 @@ it('emits an event for each action after an outgoing transaction has been accept
732
680
  _type: 'author'
733
681
  name?: string
734
682
  }
735
- const doc: DocumentHandle<Author> = {_id: crypto.randomUUID(), _type: 'author'}
683
+ const documentId = crypto.randomUUID()
684
+ const doc: DocumentHandle<Author> = {documentId, documentType: 'author'}
736
685
  expect(handler).toHaveBeenCalledTimes(0)
737
686
 
738
687
  const tnx1 = await applyDocumentActions(instance, [
@@ -751,14 +700,14 @@ it('emits an event for each action after an outgoing transaction has been accept
751
700
  expect(handler).toHaveBeenCalledTimes(9)
752
701
 
753
702
  expect(handler.mock.calls).toMatchObject([
754
- [{documentId: doc._id, type: 'created', outgoing: {transactionId: tnx1.transactionId}}],
755
- [{documentId: doc._id, type: 'edited', outgoing: {transactionId: tnx1.transactionId}}],
756
- [{documentId: doc._id, type: 'published', outgoing: {transactionId: tnx1.transactionId}}],
703
+ [{documentId, type: 'created', outgoing: {transactionId: tnx1.transactionId}}],
704
+ [{documentId, type: 'edited', outgoing: {transactionId: tnx1.transactionId}}],
705
+ [{documentId, type: 'published', outgoing: {transactionId: tnx1.transactionId}}],
757
706
  [{type: 'accepted', outgoing: {transactionId: tnx1.transactionId}}],
758
- [{documentId: doc._id, type: 'unpublished', outgoing: {transactionId: tnx2.transactionId}}],
759
- [{documentId: doc._id, type: 'published', outgoing: {transactionId: tnx2.transactionId}}],
760
- [{documentId: doc._id, type: 'edited', outgoing: {transactionId: tnx2.transactionId}}],
761
- [{documentId: doc._id, type: 'discarded', outgoing: {transactionId: tnx2.transactionId}}],
707
+ [{documentId, type: 'unpublished', outgoing: {transactionId: tnx2.transactionId}}],
708
+ [{documentId, type: 'published', outgoing: {transactionId: tnx2.transactionId}}],
709
+ [{documentId, type: 'edited', outgoing: {transactionId: tnx2.transactionId}}],
710
+ [{documentId, type: 'discarded', outgoing: {transactionId: tnx2.transactionId}}],
762
711
  [{type: 'accepted', outgoing: {transactionId: tnx2.transactionId}}],
763
712
  ])
764
713
 
@@ -767,13 +716,6 @@ it('emits an event for each action after an outgoing transaction has been accept
767
716
  unsubscribe()
768
717
  })
769
718
 
770
- function checkUnverified(instance: SanityInstance, docId: string) {
771
- const {state} = getOrCreateResource(instance, documentStore)
772
- const {documentStates} = state.get()
773
- expect(documentStates[getDraftId(docId)]?.unverifiedRevisions).toEqual({})
774
- expect(documentStates[getPublishedId(docId)]?.unverifiedRevisions).toEqual({})
775
- }
776
-
777
719
  vi.mock('../client/clientStore.ts', () => ({
778
720
  getClientState: vi.fn().mockReturnValue({observable: new ReplaySubject(1)}),
779
721
  }))
@@ -784,8 +726,9 @@ vi.mock('./sharedListener.ts', () => {
784
726
 
785
727
  return {
786
728
  createFetchDocument: vi.fn(),
787
- createSharedListener: vi.fn().mockReturnValue(
788
- Object.assign(
729
+ createSharedListener: vi.fn().mockReturnValue({
730
+ dispose: vi.fn(),
731
+ events: Object.assign(
789
732
  new Observable((observer) => {
790
733
  observer.next(welcomeEvent)
791
734
  return sharedListener.subscribe(observer)
@@ -796,7 +739,7 @@ vi.mock('./sharedListener.ts', () => {
796
739
  error: sharedListener.error.bind(sharedListener),
797
740
  },
798
741
  ),
799
- ),
742
+ }),
800
743
  }
801
744
  })
802
745
 
@@ -814,7 +757,12 @@ let client: SanityClient
814
757
  beforeEach(() => {
815
758
  const client$ = (getClientState as () => StateSource<SanityClient>)()
816
759
  .observable as ReplaySubject<SanityClient>
817
- const sharedListener = (createSharedListener as () => Subject<ListenEvent<SanityDocument>>)()
760
+ const sharedListener = (
761
+ createSharedListener as () => {
762
+ dispose: () => void
763
+ events: Subject<ListenEvent<SanityDocument>>
764
+ }
765
+ )()
818
766
 
819
767
  let documents: DocumentSet = {
820
768
  [getDraftId('existing-doc')]: {
@@ -1119,7 +1067,7 @@ beforeEach(() => {
1119
1067
  documents = next
1120
1068
 
1121
1069
  for (const mutationEvent of mutationEvents) {
1122
- sharedListener.next(mutationEvent)
1070
+ sharedListener.events.next(mutationEvent)
1123
1071
  }
1124
1072
  }
1125
1073