@sanity/sdk 2.8.0 → 3.0.0-rc.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 (99) hide show
  1. package/dist/index.d.ts +228 -239
  2. package/dist/index.js +287 -454
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -4
  5. package/src/_exports/index.ts +16 -17
  6. package/src/agent/agentActions.test.ts +60 -16
  7. package/src/agent/agentActions.ts +29 -20
  8. package/src/auth/authMode.test.ts +0 -25
  9. package/src/auth/authMode.ts +3 -6
  10. package/src/auth/authStore.test.ts +129 -66
  11. package/src/auth/authStore.ts +9 -11
  12. package/src/auth/dashboardAuth.ts +2 -2
  13. package/src/auth/getOrganizationVerificationState.test.ts +10 -11
  14. package/src/auth/handleAuthCallback.test.ts +0 -12
  15. package/src/auth/handleAuthCallback.ts +9 -3
  16. package/src/auth/logout.test.ts +0 -6
  17. package/src/auth/refreshStampedToken.test.ts +121 -17
  18. package/src/auth/standaloneAuth.ts +9 -3
  19. package/src/auth/studioAuth.ts +35 -8
  20. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +9 -3
  21. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -1
  22. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +0 -2
  23. package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
  24. package/src/auth/utils.ts +33 -0
  25. package/src/client/clientStore.test.ts +14 -61
  26. package/src/client/clientStore.ts +52 -28
  27. package/src/comlink/controller/actions/destroyController.test.ts +1 -4
  28. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +1 -4
  29. package/src/comlink/controller/actions/getOrCreateController.test.ts +1 -4
  30. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  31. package/src/comlink/controller/comlinkControllerStore.test.ts +1 -4
  32. package/src/comlink/node/actions/getOrCreateNode.test.ts +1 -4
  33. package/src/comlink/node/actions/releaseNode.test.ts +1 -4
  34. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  35. package/src/comlink/node/getNodeState.test.ts +1 -1
  36. package/src/config/__tests__/handles.test.ts +12 -18
  37. package/src/config/handles.ts +7 -25
  38. package/src/config/sanityConfig.ts +99 -52
  39. package/src/datasets/datasets.test.ts +2 -2
  40. package/src/datasets/datasets.ts +4 -10
  41. package/src/document/actions.test.ts +33 -4
  42. package/src/document/actions.ts +3 -10
  43. package/src/document/applyDocumentActions.test.ts +17 -18
  44. package/src/document/applyDocumentActions.ts +9 -12
  45. package/src/document/documentStore.test.ts +303 -133
  46. package/src/document/documentStore.ts +70 -61
  47. package/src/document/permissions.test.ts +44 -8
  48. package/src/document/processActions.test.ts +77 -7
  49. package/src/document/reducers.test.ts +35 -3
  50. package/src/document/sharedListener.test.ts +13 -13
  51. package/src/document/sharedListener.ts +8 -3
  52. package/src/favorites/favorites.test.ts +10 -2
  53. package/src/presence/presenceStore.test.ts +34 -9
  54. package/src/presence/presenceStore.ts +29 -13
  55. package/src/preview/previewProjectionUtils.test.ts +192 -0
  56. package/src/preview/previewProjectionUtils.ts +88 -0
  57. package/src/preview/{previewStore.ts → types.ts} +6 -25
  58. package/src/project/project.test.ts +1 -1
  59. package/src/project/project.ts +14 -20
  60. package/src/projection/getProjectionState.test.ts +4 -2
  61. package/src/projection/getProjectionState.ts +2 -21
  62. package/src/projection/projectionQuery.ts +2 -3
  63. package/src/projection/projectionStore.test.ts +3 -3
  64. package/src/projection/resolveProjection.test.ts +2 -1
  65. package/src/projection/resolveProjection.ts +2 -18
  66. package/src/projection/subscribeToStateAndFetchBatches.test.ts +2 -2
  67. package/src/projection/subscribeToStateAndFetchBatches.ts +23 -36
  68. package/src/projection/types.ts +1 -9
  69. package/src/projects/projects.test.ts +1 -1
  70. package/src/query/queryStore.test.ts +86 -28
  71. package/src/query/queryStore.ts +23 -38
  72. package/src/releases/getPerspectiveState.test.ts +14 -13
  73. package/src/releases/getPerspectiveState.ts +6 -6
  74. package/src/releases/releasesStore.test.ts +21 -6
  75. package/src/releases/releasesStore.ts +18 -8
  76. package/src/store/createActionBinder.test.ts +114 -111
  77. package/src/store/createActionBinder.ts +52 -101
  78. package/src/store/createSanityInstance.test.ts +13 -83
  79. package/src/store/createSanityInstance.ts +2 -78
  80. package/src/store/createStateSourceAction.test.ts +2 -2
  81. package/src/store/createStateSourceAction.ts +5 -5
  82. package/src/store/createStoreInstance.test.ts +2 -4
  83. package/src/users/reducers.test.ts +1 -6
  84. package/src/users/reducers.ts +2 -2
  85. package/src/users/types.ts +4 -4
  86. package/src/users/usersStore.test.ts +12 -15
  87. package/src/utils/createFetcherStore.test.ts +1 -1
  88. package/src/utils/logger.test.ts +0 -12
  89. package/src/utils/logger.ts +3 -8
  90. package/src/preview/getPreviewState.test.ts +0 -120
  91. package/src/preview/getPreviewState.ts +0 -91
  92. package/src/preview/previewQuery.test.ts +0 -236
  93. package/src/preview/previewQuery.ts +0 -153
  94. package/src/preview/previewStore.test.ts +0 -36
  95. package/src/preview/resolvePreview.test.ts +0 -47
  96. package/src/preview/resolvePreview.ts +0 -20
  97. package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
  98. package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
  99. package/src/preview/util.ts +0 -13
@@ -51,38 +51,24 @@ interface TestDocument extends SanityDocument {
51
51
  title?: string
52
52
  }
53
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: AllTestSchemaTypes
61
- }
62
- }
63
-
64
54
  let instance: SanityInstance
65
- let instance1: SanityInstance
66
- let instance2: SanityInstance
55
+
56
+ // bindActionByResource should ensure docs from different resources are isolated
57
+ const resource = {projectId: 'p', dataset: 'd'}
58
+ const resource1 = {projectId: 'p', dataset: 'd1'}
59
+ const resource2 = {projectId: 'p', dataset: 'd2'}
67
60
 
68
61
  beforeEach(() => {
69
- instance = createSanityInstance({projectId: 'p', dataset: 'd'})
70
- // test uses two instances that share the same in-memory dataset, but separate
71
- // store instances. in real scenarios, this would be separate machines but with
72
- // the same project + dataset
73
- instance1 = createSanityInstance({projectId: 'p', dataset: 'd1'})
74
- instance2 = createSanityInstance({projectId: 'p', dataset: 'd2'})
62
+ instance = createSanityInstance()
75
63
  })
76
64
 
77
65
  afterEach(() => {
78
66
  instance?.dispose()
79
- instance1?.dispose()
80
- instance2?.dispose()
81
67
  })
82
68
 
83
69
  it('creates, edits, and publishes a document', async () => {
84
- const doc = createDocumentHandle({documentId: 'doc-single', documentType: 'article'})
85
- const documentState = getDocumentState(instance, doc)
70
+ const doc = createDocumentHandle({documentId: 'doc-single', documentType: 'article', resource})
71
+ const documentState = getDocumentState<TestDocument>(instance, doc)
86
72
 
87
73
  // Initially the document is undefined
88
74
  expect(documentState.getCurrent()).toBeUndefined()
@@ -90,7 +76,10 @@ it('creates, edits, and publishes a document', async () => {
90
76
  const unsubscribe = documentState.subscribe()
91
77
 
92
78
  // Create a new document
93
- const {appeared} = await applyDocumentActions(instance, {actions: [createDocument(doc)]})
79
+ const {appeared} = await applyDocumentActions(instance, {
80
+ actions: [createDocument(doc)],
81
+ resource,
82
+ })
94
83
  expect(appeared).toContain(getDraftId(doc.documentId))
95
84
 
96
85
  let currentDoc = documentState.getCurrent()
@@ -99,6 +88,7 @@ it('creates, edits, and publishes a document', async () => {
99
88
  // Edit the document – add a title
100
89
  await applyDocumentActions(instance, {
101
90
  actions: [editDocument(doc, {set: {title: 'My First Article'}})],
91
+ resource,
102
92
  })
103
93
  currentDoc = documentState.getCurrent()
104
94
  expect(currentDoc?.title).toEqual('My First Article')
@@ -106,6 +96,7 @@ it('creates, edits, and publishes a document', async () => {
106
96
  // Publish the document; the resulting transactionId is used as the new _rev
107
97
  const {transactionId, submitted} = await applyDocumentActions(instance, {
108
98
  actions: [publishDocument(doc)],
99
+ resource,
109
100
  })
110
101
  await submitted()
111
102
  currentDoc = documentState.getCurrent()
@@ -115,8 +106,12 @@ it('creates, edits, and publishes a document', async () => {
115
106
  })
116
107
 
117
108
  it('creates a document with initial values', async () => {
118
- const doc = createDocumentHandle({documentId: 'doc-with-initial', documentType: 'article'})
119
- const documentState = getDocumentState(instance, doc)
109
+ const doc = createDocumentHandle({
110
+ documentId: 'doc-with-initial',
111
+ documentType: 'article',
112
+ resource,
113
+ })
114
+ const documentState = getDocumentState<TestDocument>(instance, doc)
120
115
 
121
116
  expect(documentState.getCurrent()).toBeUndefined()
122
117
 
@@ -131,6 +126,7 @@ it('creates a document with initial values', async () => {
131
126
  count: 42,
132
127
  }),
133
128
  ],
129
+ resource,
134
130
  })
135
131
  expect(appeared).toContain(getDraftId(doc.documentId))
136
132
 
@@ -144,8 +140,8 @@ it('creates a document with initial values', async () => {
144
140
  })
145
141
 
146
142
  it('edits existing documents', async () => {
147
- const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article'})
148
- const state = getDocumentState(instance, doc)
143
+ const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article', resource})
144
+ const state = getDocumentState<TestDocument>(instance, doc)
149
145
 
150
146
  // not subscribed yet so the value is undefined
151
147
  expect(state.getCurrent()).toBeUndefined()
@@ -162,6 +158,7 @@ it('edits existing documents', async () => {
162
158
 
163
159
  await applyDocumentActions(instance, {
164
160
  actions: [editDocument(doc, {set: {title: 'updated title'}})],
161
+ resource,
165
162
  })
166
163
  expect(state.getCurrent()).toMatchObject({
167
164
  _id: getDraftId(doc.documentId),
@@ -172,23 +169,33 @@ it('edits existing documents', async () => {
172
169
  })
173
170
 
174
171
  it('sets optimistic changes synchronously', async () => {
175
- const doc = createDocumentHandle({documentId: 'optimistic', documentType: 'article'})
172
+ const doc1 = createDocumentHandle({
173
+ documentId: 'optimistic',
174
+ documentType: 'article',
175
+ resource: resource1,
176
+ })
177
+ const doc2 = createDocumentHandle({
178
+ documentId: 'optimistic',
179
+ documentType: 'article',
180
+ resource: resource2,
181
+ })
176
182
 
177
- const state1 = getDocumentState(instance1, doc)
178
- const state2 = getDocumentState(instance2, doc)
183
+ const state1 = getDocumentState<TestDocument>(instance, doc1)
184
+ const state2 = getDocumentState<TestDocument>(instance, doc2)
179
185
 
180
186
  const unsubscribe1 = state1.subscribe()
181
187
  const unsubscribe2 = state2.subscribe()
182
188
 
183
189
  // wait until the value is primed in the store
184
- await resolveDocument(instance1, doc)
190
+ await resolveDocument(instance, doc1)
185
191
 
186
192
  // then the actions are synchronous
187
193
  expect(state1.getCurrent()).toBeNull()
188
- applyDocumentActions(instance1, {actions: [createDocument(doc)]})
189
- expect(state1.getCurrent()).toMatchObject({_id: getDraftId(doc.documentId)})
190
- const actionResult1Promise = applyDocumentActions(instance1, {
191
- actions: [editDocument(doc, {set: {title: 'initial title'}})],
194
+ applyDocumentActions(instance, {actions: [createDocument(doc1)], resource: resource1})
195
+ expect(state1.getCurrent()).toMatchObject({_id: getDraftId(doc1.documentId)})
196
+ const actionResult1Promise = applyDocumentActions(instance, {
197
+ actions: [editDocument(doc1, {set: {title: 'initial title'}})],
198
+ resource: resource1,
192
199
  })
193
200
  expect(state1.getCurrent()?.title).toBe('initial title')
194
201
 
@@ -207,8 +214,9 @@ it('sets optimistic changes synchronously', async () => {
207
214
  expect(state2.getCurrent()?.title).toBe('initial title')
208
215
 
209
216
  // synchronous for state 2
210
- const actionResult2Promise = applyDocumentActions(instance2, {
211
- actions: [editDocument(doc, {set: {title: 'updated title'}})],
217
+ const actionResult2Promise = applyDocumentActions(instance, {
218
+ actions: [editDocument(doc2, {set: {title: 'updated title'}})],
219
+ resource: resource2,
212
220
  })
213
221
  expect(state2.getCurrent()?.title).toBe('updated title')
214
222
  // async for state 1
@@ -223,24 +231,37 @@ it('sets optimistic changes synchronously', async () => {
223
231
  })
224
232
 
225
233
  it('propagates changes between two instances', async () => {
226
- const doc = createDocumentHandle({documentId: 'doc-collab', documentType: 'article'})
227
- const state1 = getDocumentState(instance1, doc)
228
- const state2 = getDocumentState(instance2, doc)
234
+ const doc1 = createDocumentHandle({
235
+ documentId: 'doc-collab',
236
+ documentType: 'article',
237
+ resource: resource1,
238
+ })
239
+ const doc2 = createDocumentHandle({
240
+ documentId: 'doc-collab',
241
+ documentType: 'article',
242
+ resource: resource2,
243
+ })
244
+ const state1 = getDocumentState<TestDocument>(instance, doc1)
245
+ const state2 = getDocumentState<TestDocument>(instance, doc2)
229
246
 
230
247
  const state1Unsubscribe = state1.subscribe()
231
248
  const state2Unsubscribe = state2.subscribe()
232
249
 
233
- // Create the document from instance1.
234
- await applyDocumentActions(instance1, {actions: [createDocument(doc)]}).then((r) => r.submitted())
250
+ // Create the document from the store instance.
251
+ await applyDocumentActions(instance, {
252
+ actions: [createDocument(doc1)],
253
+ resource: resource1,
254
+ }).then((r) => r.submitted())
235
255
 
236
- const doc1 = state1.getCurrent()
237
- const doc2 = state2.getCurrent()
238
- expect(doc1?._id).toEqual(getDraftId(doc.documentId))
239
- expect(doc2?._id).toEqual(getDraftId(doc.documentId))
256
+ const docState1 = state1.getCurrent()
257
+ const docState2 = state2.getCurrent()
258
+ expect(docState1?._id).toEqual(getDraftId(doc1.documentId))
259
+ expect(docState2?._id).toEqual(getDraftId(doc1.documentId))
240
260
 
241
- // Now, edit the document from instance2.
242
- await applyDocumentActions(instance2, {
243
- actions: [editDocument(doc, {set: {title: 'Hello world!'}})],
261
+ // Now, edit the document from the other store instance.
262
+ await applyDocumentActions(instance, {
263
+ actions: [editDocument(doc2, {set: {title: 'Hello world!'}})],
264
+ resource: resource2,
244
265
  }).then((r) => r.submitted())
245
266
 
246
267
  const updated1 = state1.getCurrent()
@@ -253,31 +274,43 @@ it('propagates changes between two instances', async () => {
253
274
  })
254
275
 
255
276
  it('handles concurrent edits and resolves conflicts', async () => {
256
- const doc = createDocumentHandle({documentId: 'doc-concurrent', documentType: 'article'})
257
- const state1 = getDocumentState(instance1, doc)
258
- const state2 = getDocumentState(instance2, doc)
277
+ const doc1 = createDocumentHandle({
278
+ documentId: 'doc-concurrent',
279
+ documentType: 'article',
280
+ resource: resource1,
281
+ })
282
+ const doc2 = createDocumentHandle({
283
+ documentId: 'doc-concurrent',
284
+ documentType: 'article',
285
+ resource: resource2,
286
+ })
287
+ const state1 = getDocumentState<TestDocument>(instance, doc1)
288
+ const state2 = getDocumentState<TestDocument>(instance, doc2)
259
289
 
260
290
  const state1Unsubscribe = state1.subscribe()
261
291
  const state2Unsubscribe = state2.subscribe()
262
292
 
263
- const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
293
+ const oneOffInstance = createSanityInstance()
264
294
 
265
295
  // Create the initial document from a one-off instance.
266
296
  await applyDocumentActions(oneOffInstance, {
267
297
  actions: [
268
- createDocument(doc),
269
- editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy dog'}}),
298
+ createDocument(doc1),
299
+ editDocument(doc1, {set: {title: 'The quick brown fox jumps over the lazy dog'}}),
270
300
  ],
301
+ resource,
271
302
  }).then((res) => res.submitted())
272
303
 
273
- // Both instances now issue an edit simultaneously.
274
- const p1 = applyDocumentActions(instance1, {
275
- actions: [editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy cat'}})],
304
+ // Both store instances now issue an edit simultaneously.
305
+ const p1 = applyDocumentActions(instance, {
306
+ actions: [editDocument(doc1, {set: {title: 'The quick brown fox jumps over the lazy cat'}})],
307
+ resource: resource1,
276
308
  }).then((r) => r.submitted())
277
- const p2 = applyDocumentActions(instance2, {
309
+ const p2 = applyDocumentActions(instance, {
278
310
  actions: [
279
- editDocument(doc, {set: {title: 'The quick brown elephant jumps over the lazy dog'}}),
311
+ editDocument(doc2, {set: {title: 'The quick brown elephant jumps over the lazy dog'}}),
280
312
  ],
313
+ resource: resource2,
281
314
  }).then((r) => r.submitted())
282
315
 
283
316
  // Wait for both actions to complete (or reject).
@@ -294,13 +327,16 @@ it('handles concurrent edits and resolves conflicts', async () => {
294
327
  })
295
328
 
296
329
  it('unpublishes and discards a document', async () => {
297
- const doc = createDocumentHandle({documentId: 'doc-pub-unpub', documentType: 'article'})
298
- const documentState = getDocumentState(instance, doc)
330
+ const doc = createDocumentHandle({documentId: 'doc-pub-unpub', documentType: 'article', resource})
331
+ const documentState = getDocumentState<TestDocument>(instance, doc)
299
332
  const unsubscribe = documentState.subscribe()
300
333
 
301
334
  // Create and publish the document.
302
- await applyDocumentActions(instance, {actions: [createDocument(doc)]})
303
- const afterPublish = await applyDocumentActions(instance, {actions: [publishDocument(doc)]})
335
+ await applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
336
+ const afterPublish = await applyDocumentActions(instance, {
337
+ actions: [publishDocument(doc)],
338
+ resource,
339
+ })
304
340
  const publishedDoc = documentState.getCurrent()
305
341
  expect(publishedDoc).toMatchObject({
306
342
  _id: getPublishedId(doc.documentId),
@@ -308,13 +344,13 @@ it('unpublishes and discards a document', async () => {
308
344
  })
309
345
 
310
346
  // Unpublish the document (which should delete the published version and create a draft).
311
- await applyDocumentActions(instance, {actions: [unpublishDocument(doc)]})
347
+ await applyDocumentActions(instance, {actions: [unpublishDocument(doc)], resource})
312
348
  const afterUnpublish = documentState.getCurrent()
313
349
  // In our mock implementation the _id remains the same but the published copy is removed.
314
350
  expect(afterUnpublish?._id).toEqual(getDraftId(doc.documentId))
315
351
 
316
352
  // Discard the draft (which deletes the draft version).
317
- await applyDocumentActions(instance, {actions: [discardDocument(doc)]})
353
+ await applyDocumentActions(instance, {actions: [discardDocument(doc)], resource})
318
354
  const afterDiscard = documentState.getCurrent()
319
355
  expect(afterDiscard).toBeNull()
320
356
 
@@ -322,17 +358,20 @@ it('unpublishes and discards a document', async () => {
322
358
  })
323
359
 
324
360
  it('deletes a document', async () => {
325
- const doc = createDocumentHandle({documentId: 'doc-delete', documentType: 'article'})
361
+ const doc = createDocumentHandle({documentId: 'doc-delete', documentType: 'article', resource})
326
362
 
327
- const documentState = getDocumentState(instance, doc)
363
+ const documentState = getDocumentState<TestDocument>(instance, doc)
328
364
  const unsubscribe = documentState.subscribe()
329
365
 
330
- await applyDocumentActions(instance, {actions: [createDocument(doc), publishDocument(doc)]})
366
+ await applyDocumentActions(instance, {
367
+ actions: [createDocument(doc), publishDocument(doc)],
368
+ resource,
369
+ })
331
370
  const docValue = documentState.getCurrent()
332
371
  expect(docValue).toBeDefined()
333
372
 
334
373
  // Delete the document.
335
- await applyDocumentActions(instance, {actions: [deleteDocument(doc)]})
374
+ await applyDocumentActions(instance, {actions: [deleteDocument(doc)], resource})
336
375
  const afterDelete = documentState.getCurrent()
337
376
  expect(afterDelete).toBeNull()
338
377
 
@@ -340,14 +379,14 @@ it('deletes a document', async () => {
340
379
  })
341
380
 
342
381
  it('cleans up document state when there are no subscribers', async () => {
343
- const doc = createDocumentHandle({documentId: 'doc-cleanup', documentType: 'article'})
344
- const documentState = getDocumentState(instance, doc)
382
+ const doc = createDocumentHandle({documentId: 'doc-cleanup', documentType: 'article', resource})
383
+ const documentState = getDocumentState<TestDocument>(instance, doc)
345
384
 
346
385
  // Subscribe to the document state.
347
386
  const unsubscribe = documentState.subscribe()
348
387
 
349
388
  // Create a document.
350
- await applyDocumentActions(instance, {actions: [createDocument(doc)]})
389
+ await applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
351
390
  expect(documentState.getCurrent()).toBeDefined()
352
391
 
353
392
  // Unsubscribe from the document.
@@ -358,14 +397,14 @@ it('cleans up document state when there are no subscribers', async () => {
358
397
  await new Promise((resolve) => setTimeout(resolve, 30))
359
398
 
360
399
  // When a new subscriber is created, if the state was cleared it should return undefined.
361
- const newDocumentState = getDocumentState(instance, doc)
400
+ const newDocumentState = getDocumentState<TestDocument>(instance, doc)
362
401
  expect(newDocumentState.getCurrent()).toBeUndefined()
363
402
  })
364
403
 
365
404
  it('fetches documents if there are no active subscriptions for the actions applied', async () => {
366
- const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article'})
405
+ const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article', resource})
367
406
 
368
- const {getCurrent} = getDocumentState(instance, doc)
407
+ const {getCurrent} = getDocumentState<TestDocument>(instance, doc)
369
408
  expect(getCurrent()).toBeUndefined()
370
409
  expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBeUndefined()
371
410
 
@@ -374,6 +413,7 @@ it('fetches documents if there are no active subscriptions for the actions appli
374
413
  // transaction for this action has been accepted by the server
375
414
  const setNewTitle = applyDocumentActions(instance, {
376
415
  actions: [editDocument(doc, {set: {title: 'new title'}})],
416
+ resource,
377
417
  })
378
418
  expect(getCurrent()?.title).toBeUndefined()
379
419
  expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBe(false)
@@ -382,9 +422,15 @@ it('fetches documents if there are no active subscriptions for the actions appli
382
422
  expect(getCurrent()?.title).toBe('new title')
383
423
 
384
424
  // there is an active subscriber now so the edits are synchronous
385
- applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'updated title'}})]})
425
+ applyDocumentActions(instance, {
426
+ actions: [editDocument(doc, {set: {title: 'updated title'}})],
427
+ resource,
428
+ })
386
429
  expect(getCurrent()?.title).toBe('updated title')
387
- applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'updated title!'}})]})
430
+ applyDocumentActions(instance, {
431
+ actions: [editDocument(doc, {set: {title: 'updated title!'}})],
432
+ resource,
433
+ })
388
434
  expect(getCurrent()?.title).toBe('updated title!')
389
435
 
390
436
  expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBe(false)
@@ -392,6 +438,7 @@ it('fetches documents if there are no active subscriptions for the actions appli
392
438
  // await submitted in order to test that there is no subscriptions
393
439
  const result = await applyDocumentActions(instance, {
394
440
  actions: [editDocument(doc, {set: {title: 'updated title'}})],
441
+ resource,
395
442
  })
396
443
  await result.submitted()
397
444
 
@@ -400,6 +447,7 @@ it('fetches documents if there are no active subscriptions for the actions appli
400
447
 
401
448
  const setNewNewTitle = applyDocumentActions(instance, {
402
449
  actions: [editDocument(doc, {set: {title: 'new new title'}})],
450
+ resource,
403
451
  })
404
452
  // now we'll have to await again
405
453
  expect(getCurrent()?.title).toBe(undefined)
@@ -409,19 +457,27 @@ it('fetches documents if there are no active subscriptions for the actions appli
409
457
  })
410
458
 
411
459
  it('batches edit transaction into one outgoing transaction', async () => {
412
- const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
460
+ const doc = createDocumentHandle({
461
+ documentId: crypto.randomUUID(),
462
+ documentType: 'article',
463
+ resource,
464
+ })
413
465
 
414
- const unsubscribe = getDocumentState(instance, doc).subscribe()
466
+ const unsubscribe = getDocumentState<TestDocument>(instance, doc).subscribe()
415
467
 
416
468
  // this creates its own transaction
417
- applyDocumentActions(instance, {actions: [createDocument(doc)]})
469
+ applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
418
470
 
419
471
  // these get batched into one
420
- applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!'}})]})
421
- applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!!'}})]})
422
- applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!!!'}})]})
472
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!'}})], resource})
473
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!!'}})], resource})
474
+ applyDocumentActions(instance, {
475
+ actions: [editDocument(doc, {set: {title: 'name!!!'}})],
476
+ resource,
477
+ })
423
478
  const res = await applyDocumentActions(instance, {
424
479
  actions: [editDocument(doc, {set: {title: 'name!!!!'}})],
480
+ resource,
425
481
  })
426
482
  await res.submitted()
427
483
 
@@ -435,7 +491,11 @@ it('batches edit transaction into one outgoing transaction', async () => {
435
491
  })
436
492
 
437
493
  it('provides the consistency status via `getDocumentSyncStatus`', async () => {
438
- const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
494
+ const doc = createDocumentHandle({
495
+ documentId: crypto.randomUUID(),
496
+ documentType: 'article',
497
+ resource,
498
+ })
439
499
 
440
500
  const syncStatus = getDocumentSyncStatus(instance, doc)
441
501
  expect(syncStatus.getCurrent()).toBeUndefined()
@@ -443,7 +503,7 @@ it('provides the consistency status via `getDocumentSyncStatus`', async () => {
443
503
  const unsubscribe = syncStatus.subscribe()
444
504
  expect(syncStatus.getCurrent()).toBe(true)
445
505
 
446
- const applied = applyDocumentActions(instance, {actions: [createDocument(doc)]})
506
+ const applied = applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
447
507
  expect(syncStatus.getCurrent()).toBe(false)
448
508
 
449
509
  const createResult = await applied
@@ -452,11 +512,17 @@ it('provides the consistency status via `getDocumentSyncStatus`', async () => {
452
512
  await createResult.submitted()
453
513
  expect(syncStatus.getCurrent()).toBe(true)
454
514
 
455
- applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'initial name'}})]})
515
+ applyDocumentActions(instance, {
516
+ actions: [editDocument(doc, {set: {title: 'initial name'}})],
517
+ resource,
518
+ })
456
519
  expect(syncStatus.getCurrent()).toBe(false)
457
520
 
458
- applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'updated name'}})]})
459
- const publishResult = applyDocumentActions(instance, {actions: [publishDocument(doc)]})
521
+ applyDocumentActions(instance, {
522
+ actions: [editDocument(doc, {set: {title: 'updated name'}})],
523
+ resource,
524
+ })
525
+ const publishResult = applyDocumentActions(instance, {actions: [publishDocument(doc)], resource})
460
526
  expect(syncStatus.getCurrent()).toBe(false)
461
527
  await publishResult.then((res) => res.submitted())
462
528
  expect(syncStatus.getCurrent()).toBe(true)
@@ -473,35 +539,48 @@ it('reverts failed outgoing transaction locally', async () => {
473
539
  })
474
540
 
475
541
  const revertedEventPromise = new Promise<TransactionRevertedEvent>((resolve) => {
476
- const unsubscribe = subscribeDocumentEvents(instance, (e) => {
477
- if (e.type === 'reverted') {
478
- resolve(e)
479
- unsubscribe()
480
- }
542
+ const unsubscribe = subscribeDocumentEvents(instance, {
543
+ resource,
544
+ eventHandler: (e) => {
545
+ if (e.type === 'reverted') {
546
+ resolve(e)
547
+ unsubscribe()
548
+ }
549
+ },
481
550
  })
482
551
  })
483
552
 
484
- const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
553
+ const doc = createDocumentHandle({
554
+ documentId: crypto.randomUUID(),
555
+ documentType: 'article',
556
+ resource,
557
+ })
485
558
 
486
- const {getCurrent, subscribe} = getDocumentState(instance, doc)
559
+ const {getCurrent, subscribe} = getDocumentState<TestDocument>(instance, doc)
487
560
  const unsubscribe = subscribe()
488
561
 
489
- await applyDocumentActions(instance, {actions: [createDocument(doc)]})
490
- applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'the'}})]})
491
- applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'the quick'}})]})
562
+ await applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
563
+ applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'the'}})], resource})
564
+ applyDocumentActions(instance, {
565
+ actions: [editDocument(doc, {set: {title: 'the quick'}})],
566
+ resource,
567
+ })
492
568
 
493
569
  // this edit action is simulated to fail from the backend and will be reverted
494
570
  const revertedActionResult = applyDocumentActions(instance, {
495
571
  actions: [editDocument(doc, {set: {title: 'the quick brown'}})],
496
572
  transactionId: 'force-revert',
497
573
  disableBatching: true,
574
+ resource,
498
575
  })
499
576
 
500
577
  applyDocumentActions(instance, {
501
578
  actions: [editDocument(doc, {set: {title: 'the quick brown fox'}})],
579
+ resource,
502
580
  })
503
581
  await applyDocumentActions(instance, {
504
582
  actions: [editDocument(doc, {set: {title: 'the quick brown fox jumps'}})],
583
+ resource,
505
584
  }).then((e) => e.submitted())
506
585
 
507
586
  await expect(revertedEventPromise).resolves.toMatchObject({
@@ -521,6 +600,7 @@ it('reverts failed outgoing transaction locally', async () => {
521
600
  // check that we can still edit after recovering from the error
522
601
  applyDocumentActions(instance, {
523
602
  actions: [editDocument(doc, {set: {title: 'TEST the quick fox jumps'}})],
603
+ resource,
524
604
  })
525
605
  expect(getCurrent()?.title).toBe('TEST the quick fox jumps')
526
606
 
@@ -530,20 +610,30 @@ it('reverts failed outgoing transaction locally', async () => {
530
610
 
531
611
  it('removes a queued transaction if it fails to apply', async () => {
532
612
  const actionErrorEventPromise = new Promise<ActionErrorEvent>((resolve) => {
533
- const unsubscribe = subscribeDocumentEvents(instance, (e) => {
534
- if (e.type === 'error') {
535
- resolve(e)
536
- unsubscribe()
537
- }
613
+ const unsubscribe = subscribeDocumentEvents(instance, {
614
+ resource,
615
+ eventHandler: (e) => {
616
+ if (e.type === 'error') {
617
+ resolve(e)
618
+ unsubscribe()
619
+ }
620
+ },
538
621
  })
539
622
  })
540
623
 
541
- const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
542
- const state = getDocumentState(instance, doc)
624
+ const doc = createDocumentHandle({
625
+ documentId: crypto.randomUUID(),
626
+ documentType: 'article',
627
+ resource,
628
+ })
629
+ const state = getDocumentState<TestDocument>(instance, doc)
543
630
  const unsubscribe = state.subscribe()
544
631
 
545
632
  await expect(
546
- applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: "can't set"}})]}),
633
+ applyDocumentActions(instance, {
634
+ actions: [editDocument(doc, {set: {title: "can't set"}})],
635
+ resource,
636
+ }),
547
637
  ).rejects.toThrowError(/Cannot edit document/)
548
638
 
549
639
  await expect(actionErrorEventPromise).resolves.toMatchObject({
@@ -553,8 +643,11 @@ it('removes a queued transaction if it fails to apply', async () => {
553
643
  })
554
644
 
555
645
  // editing should still work after though (no crashing)
556
- await applyDocumentActions(instance, {actions: [createDocument(doc)]})
557
- applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'can set!'}})]})
646
+ await applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
647
+ applyDocumentActions(instance, {
648
+ actions: [editDocument(doc, {set: {title: 'can set!'}})],
649
+ resource,
650
+ })
558
651
 
559
652
  expect(state.getCurrent()?.title).toBe('can set!')
560
653
 
@@ -571,13 +664,17 @@ it('returns allowed true when no permission errors occur', async () => {
571
664
  const doc = createDocumentHandle({
572
665
  documentId: 'doc-perm-allowed',
573
666
  documentType: 'article',
667
+ resource,
574
668
  })
575
- const state = getDocumentState(instance, doc)
669
+ const state = getDocumentState<TestDocument>(instance, doc)
576
670
  const unsubscribe = state.subscribe()
577
- await applyDocumentActions(instance, {actions: [createDocument(doc)]}).then((r) => r.submitted())
671
+ await applyDocumentActions(instance, {actions: [createDocument(doc)], resource}).then((r) =>
672
+ r.submitted(),
673
+ )
578
674
 
579
675
  // Use an action that includes a patch (so that update permission check is bypassed).
580
676
  const permissionsState = getPermissionsState(instance, {
677
+ resource,
581
678
  actions: [
582
679
  {
583
680
  ...doc,
@@ -594,30 +691,42 @@ it('returns allowed true when no permission errors occur', async () => {
594
691
  })
595
692
 
596
693
  it("should reject applying the action if a precondition isn't met", async () => {
597
- const doc = createDocumentHandle({documentId: 'does-not-exist', documentType: 'article'})
694
+ const doc = createDocumentHandle({
695
+ documentId: 'does-not-exist',
696
+ documentType: 'article',
697
+ resource,
698
+ })
598
699
 
599
- await expect(applyDocumentActions(instance, {actions: [deleteDocument(doc)]})).rejects.toThrow(
600
- 'The document you are trying to delete does not exist.',
601
- )
700
+ await expect(
701
+ applyDocumentActions(instance, {actions: [deleteDocument(doc)], resource}),
702
+ ).rejects.toThrow('The document you are trying to delete does not exist.')
602
703
  })
603
704
 
604
705
  it("should reject applying the action if a permission isn't met", async () => {
605
- const doc = createDocumentHandle({documentId: 'does-not-exist', documentType: 'article'})
706
+ const doc = createDocumentHandle({
707
+ documentId: 'does-not-exist',
708
+ documentType: 'article',
709
+ resource,
710
+ })
606
711
 
607
712
  const datasetAcl = [{filter: 'false', permissions: ['create']}]
608
713
  vi.mocked(client.request).mockResolvedValue(datasetAcl)
609
714
 
610
- await expect(applyDocumentActions(instance, {actions: [createDocument(doc)]})).rejects.toThrow(
611
- 'You do not have permission to create a draft for document "does-not-exist".',
612
- )
715
+ await expect(
716
+ applyDocumentActions(instance, {actions: [createDocument(doc)], resource}),
717
+ ).rejects.toThrow('You do not have permission to create a draft for document "does-not-exist".')
613
718
  })
614
719
 
615
720
  it('returns allowed false with reasons when permission errors occur', async () => {
616
721
  const datasetAcl = [{filter: 'false', permissions: ['create']}]
617
722
  vi.mocked(client.request).mockResolvedValue(datasetAcl)
618
723
 
619
- const doc = createDocumentHandle({documentId: 'doc-perm-denied', documentType: 'article'})
620
- const result = await resolvePermissions(instance, {actions: [createDocument(doc)]})
724
+ const doc = createDocumentHandle({
725
+ documentId: 'doc-perm-denied',
726
+ documentType: 'article',
727
+ resource,
728
+ })
729
+ const result = await resolvePermissions(instance, {actions: [createDocument(doc)], resource})
621
730
 
622
731
  const message = 'You do not have permission to create a draft for document "doc-perm-denied".'
623
732
  expect(result).toMatchObject({
@@ -635,27 +744,86 @@ it('fetches dataset ACL and updates grants in the document store state', async (
635
744
  ]
636
745
  vi.mocked(client.request).mockResolvedValue(datasetAcl)
637
746
 
638
- const book = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'book'})
639
- const author = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'author'})
747
+ const book = createDocumentHandle({
748
+ documentId: crypto.randomUUID(),
749
+ documentType: 'book',
750
+ resource,
751
+ })
752
+ const author = createDocumentHandle({
753
+ documentId: crypto.randomUUID(),
754
+ documentType: 'author',
755
+ resource,
756
+ })
640
757
 
641
- expect(await resolvePermissions(instance, {actions: [createDocument(book)]})).toEqual({
758
+ expect(await resolvePermissions(instance, {actions: [createDocument(book)], resource})).toEqual({
642
759
  allowed: true,
643
760
  })
644
- expect(await resolvePermissions(instance, {actions: [createDocument(author)]})).toMatchObject({
761
+ expect(
762
+ await resolvePermissions(instance, {actions: [createDocument(author)], resource}),
763
+ ).toMatchObject({
645
764
  allowed: false,
646
765
  message: expect.stringContaining('You do not have permission to create a draft for document'),
647
766
  })
648
767
  })
649
768
 
769
+ it('fetches ACL for MediaLibraryResource', async () => {
770
+ const mediaLibraryInstance = createSanityInstance()
771
+
772
+ const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
773
+ vi.mocked(client.request).mockResolvedValue(datasetAcl)
774
+
775
+ const doc = createDocumentHandle({
776
+ documentId: crypto.randomUUID(),
777
+ documentType: 'article',
778
+ resource,
779
+ })
780
+ const mediaLibraryResource = {mediaLibraryId: 'test-media-library'}
781
+
782
+ const result = await resolvePermissions(mediaLibraryInstance, {
783
+ actions: [createDocument(doc)],
784
+ resource: mediaLibraryResource,
785
+ })
786
+
787
+ expect(result).toEqual({allowed: true})
788
+ mediaLibraryInstance.dispose()
789
+ })
790
+
791
+ it('fetches ACL for CanvasResource', async () => {
792
+ const canvasInstance = createSanityInstance()
793
+
794
+ const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
795
+ vi.mocked(client.request).mockResolvedValue(datasetAcl)
796
+
797
+ const doc = createDocumentHandle({
798
+ documentId: crypto.randomUUID(),
799
+ documentType: 'article',
800
+ resource,
801
+ })
802
+ const canvasResource = {canvasId: 'test-canvas'}
803
+
804
+ const result = await resolvePermissions(canvasInstance, {
805
+ actions: [createDocument(doc)],
806
+ resource: canvasResource,
807
+ })
808
+
809
+ expect(result).toEqual({allowed: true})
810
+ canvasInstance.dispose()
811
+ })
812
+
650
813
  it('returns a promise that resolves when a document has been loaded in the store (useful for suspense)', async () => {
651
- const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
814
+ const doc = createDocumentHandle({
815
+ documentId: crypto.randomUUID(),
816
+ documentType: 'article',
817
+ resource,
818
+ })
652
819
 
653
820
  expect(await resolveDocument(instance, doc)).toBe(null)
654
821
 
655
822
  // use one-off instance to create the document in the mock backend
656
- const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
823
+ const oneOffInstance = createSanityInstance()
657
824
  const result = await applyDocumentActions(oneOffInstance, {
658
825
  actions: [createDocument(doc), editDocument(doc, {set: {title: 'initial title'}})],
826
+ resource,
659
827
  })
660
828
  await result.submitted() // wait till submitted to server before resolving
661
829
 
@@ -669,10 +837,10 @@ it('returns a promise that resolves when a document has been loaded in the store
669
837
 
670
838
  it('emits an event for each action after an outgoing transaction has been accepted', async () => {
671
839
  const handler = vi.fn()
672
- const unsubscribe = subscribeDocumentEvents(instance, handler)
840
+ const unsubscribe = subscribeDocumentEvents(instance, {resource, eventHandler: handler})
673
841
 
674
842
  const documentId = crypto.randomUUID()
675
- const doc = createDocumentHandle({documentId, documentType: 'article'})
843
+ const doc = createDocumentHandle({documentId, documentType: 'article', resource})
676
844
  expect(handler).toHaveBeenCalledTimes(0)
677
845
 
678
846
  const tnx1 = await applyDocumentActions(instance, {
@@ -681,6 +849,7 @@ it('emits an event for each action after an outgoing transaction has been accept
681
849
  editDocument(doc, {set: {title: 'new name'}}),
682
850
  publishDocument(doc),
683
851
  ],
852
+ resource,
684
853
  }).then((e) => e.submitted())
685
854
  expect(handler).toHaveBeenCalledTimes(4)
686
855
 
@@ -691,6 +860,7 @@ it('emits an event for each action after an outgoing transaction has been accept
691
860
  editDocument(doc, {set: {title: 'updated name'}}),
692
861
  discardDocument(doc),
693
862
  ],
863
+ resource,
694
864
  }).then((e) => e.submitted())
695
865
  expect(handler).toHaveBeenCalledTimes(9)
696
866
 
@@ -706,7 +876,7 @@ it('emits an event for each action after an outgoing transaction has been accept
706
876
  [{type: 'accepted', outgoing: {transactionId: tnx2.transactionId}}],
707
877
  ])
708
878
 
709
- await applyDocumentActions(instance, {actions: [deleteDocument(doc)]})
879
+ await applyDocumentActions(instance, {actions: [deleteDocument(doc)], resource})
710
880
 
711
881
  unsubscribe()
712
882
  })
@@ -859,8 +1029,8 @@ beforeEach(() => {
859
1029
  continue
860
1030
  }
861
1031
  case 'sanity.action.document.edit': {
862
- const source = next[i.draftId] ?? next[i.publishedId]
863
- if (!source) {
1032
+ const documentSource = next[i.draftId] ?? next[i.publishedId]
1033
+ if (!documentSource) {
864
1034
  throw new Error(
865
1035
  `Could not find a document to edit from \`draftId\` \`${i.draftId}\` or \`publishedId\` ${i.publishedId}`,
866
1036
  )
@@ -869,7 +1039,7 @@ beforeEach(() => {
869
1039
  next = processMutations({
870
1040
  documents: next,
871
1041
  mutations: [
872
- {createIfNotExists: {...source, _id: i.draftId}},
1042
+ {createIfNotExists: {...documentSource, _id: i.draftId}},
873
1043
  {patch: {id: i.draftId, ...i.patch}},
874
1044
  ],
875
1045
  transactionId,