@sanity/sdk 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks-dts/utils.d.ts +2450 -0
- package/dist/_chunks-es/_internal.js +129 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1537 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +87 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_chunks-es/version.js +7 -0
- package/dist/_chunks-es/version.js.map +1 -0
- package/dist/_exports/_internal.d.ts +64 -0
- package/dist/_exports/_internal.js +20 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +2 -2343
- package/dist/index.js +465 -1813
- package/dist/index.js.map +1 -1
- package/package.json +17 -12
- package/src/_exports/_internal.ts +14 -0
- package/src/_exports/index.ts +18 -1
- package/src/auth/authStore.test.ts +150 -1
- package/src/auth/authStore.ts +11 -11
- package/src/auth/dashboardAuth.ts +2 -2
- package/src/auth/handleAuthCallback.ts +9 -3
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -1
- package/src/auth/refreshStampedToken.test.ts +118 -1
- package/src/auth/refreshStampedToken.ts +3 -2
- package/src/auth/standaloneAuth.ts +9 -3
- package/src/auth/studioAuth.ts +34 -7
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +10 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +5 -1
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
- package/src/auth/utils.ts +33 -0
- package/src/client/clientStore.test.ts +44 -30
- package/src/client/clientStore.ts +49 -48
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/sanityConfig.ts +78 -12
- package/src/document/actions.ts +18 -11
- package/src/document/applyDocumentActions.test.ts +7 -6
- package/src/document/applyDocumentActions.ts +10 -4
- package/src/document/documentStore.test.ts +542 -188
- package/src/document/documentStore.ts +142 -76
- package/src/document/events.ts +7 -2
- package/src/document/permissions.test.ts +18 -16
- package/src/document/permissions.ts +35 -11
- package/src/document/processActions.test.ts +359 -32
- package/src/document/processActions.ts +106 -78
- package/src/document/reducers.test.ts +117 -29
- package/src/document/reducers.ts +47 -40
- package/src/document/sharedListener.ts +16 -6
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +9 -2
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +96 -0
- package/src/presence/presenceStore.ts +96 -24
- package/src/preview/getPreviewState.test.ts +115 -98
- package/src/preview/getPreviewState.ts +38 -60
- package/src/preview/previewProjectionUtils.test.ts +179 -0
- package/src/preview/previewProjectionUtils.ts +93 -0
- package/src/preview/resolvePreview.test.ts +42 -25
- package/src/preview/resolvePreview.ts +33 -10
- package/src/preview/{previewStore.ts → types.ts} +8 -17
- package/src/projection/getProjectionState.test.ts +16 -16
- package/src/projection/getProjectionState.ts +6 -5
- package/src/projection/projectionQuery.ts +2 -3
- package/src/projection/projectionStore.test.ts +2 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +12 -11
- package/src/projection/types.ts +1 -1
- package/src/query/queryStore.test.ts +12 -12
- package/src/query/queryStore.ts +12 -11
- package/src/query/reducers.ts +3 -3
- package/src/releases/getPerspectiveState.ts +7 -6
- package/src/releases/releasesStore.test.ts +20 -5
- package/src/releases/releasesStore.ts +20 -8
- package/src/store/createActionBinder.test.ts +31 -31
- package/src/store/createActionBinder.ts +43 -38
- package/src/store/createSanityInstance.ts +2 -3
- package/src/store/createStateSourceAction.test.ts +62 -0
- package/src/store/createStateSourceAction.ts +34 -39
- package/src/telemetry/__telemetry__/sdk.telemetry.ts +42 -0
- package/src/telemetry/devMode.test.ts +52 -0
- package/src/telemetry/devMode.ts +40 -0
- package/src/telemetry/initTelemetry.test.ts +225 -0
- package/src/telemetry/initTelemetry.ts +205 -0
- package/src/telemetry/telemetryManager.test.ts +263 -0
- package/src/telemetry/telemetryManager.ts +187 -0
- package/src/users/reducers.ts +3 -4
- package/src/users/usersStore.test.ts +1 -0
- package/src/users/usersStore.ts +5 -1
- package/src/utils/createFetcherStore.test.ts +6 -4
- package/src/utils/createFetcherStore.ts +8 -5
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +24 -0
- package/src/preview/previewQuery.test.ts +0 -236
- package/src/preview/previewQuery.ts +0 -153
- package/src/preview/previewStore.test.ts +0 -36
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
- package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
- package/src/preview/util.ts +0 -13
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type BaseActionOptions,
|
|
3
|
+
type BaseMutationOptions,
|
|
3
4
|
type FilteredResponseQueryOptions,
|
|
4
5
|
type ListenEvent,
|
|
5
6
|
type MultipleActionResult,
|
|
7
|
+
type MultipleMutationResult,
|
|
6
8
|
type MutationEvent,
|
|
7
9
|
type RawQueryResponse,
|
|
8
10
|
type ResponseQueryOptions,
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
type WelcomeEvent,
|
|
13
15
|
} from '@sanity/client'
|
|
14
16
|
import {diffValue} from '@sanity/diff-patch'
|
|
17
|
+
import {DocumentId, getDraftId, getPublishedId} from '@sanity/id-utils'
|
|
15
18
|
import {type Mutation, type SanityDocument} from '@sanity/types'
|
|
16
19
|
import {evaluate, parse} from 'groq-js'
|
|
17
20
|
import {delay, first, firstValueFrom, from, Observable, of, ReplaySubject, Subject} from 'rxjs'
|
|
@@ -21,7 +24,6 @@ import {getClientState} from '../client/clientStore'
|
|
|
21
24
|
import {createDocumentHandle} from '../config/handles'
|
|
22
25
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
23
26
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
24
|
-
import {getDraftId, getPublishedId} from '../utils/ids'
|
|
25
27
|
import {
|
|
26
28
|
createDocument,
|
|
27
29
|
deleteDocument,
|
|
@@ -65,6 +67,10 @@ let instance: SanityInstance
|
|
|
65
67
|
let instance1: SanityInstance
|
|
66
68
|
let instance2: SanityInstance
|
|
67
69
|
|
|
70
|
+
const resource = {projectId: 'p', dataset: 'd'}
|
|
71
|
+
const source1 = {projectId: 'p', dataset: 'd1'}
|
|
72
|
+
const source2 = {projectId: 'p', dataset: 'd2'}
|
|
73
|
+
|
|
68
74
|
beforeEach(() => {
|
|
69
75
|
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
70
76
|
// test uses two instances that share the same in-memory dataset, but separate
|
|
@@ -81,8 +87,9 @@ afterEach(() => {
|
|
|
81
87
|
})
|
|
82
88
|
|
|
83
89
|
it('creates, edits, and publishes a document', async () => {
|
|
84
|
-
const
|
|
85
|
-
const
|
|
90
|
+
const documentId = DocumentId('doc-single')
|
|
91
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
92
|
+
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
86
93
|
|
|
87
94
|
// Initially the document is undefined
|
|
88
95
|
expect(documentState.getCurrent()).toBeUndefined()
|
|
@@ -90,15 +97,19 @@ it('creates, edits, and publishes a document', async () => {
|
|
|
90
97
|
const unsubscribe = documentState.subscribe()
|
|
91
98
|
|
|
92
99
|
// Create a new document
|
|
93
|
-
const {appeared} = await applyDocumentActions(instance, {
|
|
94
|
-
|
|
100
|
+
const {appeared} = await applyDocumentActions(instance, {
|
|
101
|
+
actions: [createDocument(doc)],
|
|
102
|
+
resource,
|
|
103
|
+
})
|
|
104
|
+
expect(appeared).toContain(getDraftId(documentId))
|
|
95
105
|
|
|
96
106
|
let currentDoc = documentState.getCurrent()
|
|
97
|
-
expect(currentDoc?._id).toEqual(getDraftId(
|
|
107
|
+
expect(currentDoc?._id).toEqual(getDraftId(documentId))
|
|
98
108
|
|
|
99
109
|
// Edit the document – add a title
|
|
100
110
|
await applyDocumentActions(instance, {
|
|
101
111
|
actions: [editDocument(doc, {set: {title: 'My First Article'}})],
|
|
112
|
+
resource,
|
|
102
113
|
})
|
|
103
114
|
currentDoc = documentState.getCurrent()
|
|
104
115
|
expect(currentDoc?.title).toEqual('My First Article')
|
|
@@ -106,17 +117,19 @@ it('creates, edits, and publishes a document', async () => {
|
|
|
106
117
|
// Publish the document; the resulting transactionId is used as the new _rev
|
|
107
118
|
const {transactionId, submitted} = await applyDocumentActions(instance, {
|
|
108
119
|
actions: [publishDocument(doc)],
|
|
120
|
+
resource,
|
|
109
121
|
})
|
|
110
122
|
await submitted()
|
|
111
123
|
currentDoc = documentState.getCurrent()
|
|
112
124
|
|
|
113
|
-
expect(currentDoc).toMatchObject({_id:
|
|
125
|
+
expect(currentDoc).toMatchObject({_id: documentId, _rev: transactionId})
|
|
114
126
|
unsubscribe()
|
|
115
127
|
})
|
|
116
128
|
|
|
117
129
|
it('creates a document with initial values', async () => {
|
|
118
|
-
const
|
|
119
|
-
const
|
|
130
|
+
const documentId = DocumentId('doc-with-initial')
|
|
131
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
132
|
+
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
120
133
|
|
|
121
134
|
expect(documentState.getCurrent()).toBeUndefined()
|
|
122
135
|
|
|
@@ -131,11 +144,12 @@ it('creates a document with initial values', async () => {
|
|
|
131
144
|
count: 42,
|
|
132
145
|
}),
|
|
133
146
|
],
|
|
147
|
+
resource,
|
|
134
148
|
})
|
|
135
|
-
expect(appeared).toContain(getDraftId(
|
|
149
|
+
expect(appeared).toContain(getDraftId(documentId))
|
|
136
150
|
|
|
137
151
|
const currentDoc = documentState.getCurrent()
|
|
138
|
-
expect(currentDoc?._id).toEqual(getDraftId(
|
|
152
|
+
expect(currentDoc?._id).toEqual(getDraftId(documentId))
|
|
139
153
|
expect(currentDoc?.title).toEqual('Article with Initial Values')
|
|
140
154
|
expect(currentDoc?.['author']).toEqual('Jane Doe')
|
|
141
155
|
expect(currentDoc?.['count']).toEqual(42)
|
|
@@ -144,8 +158,9 @@ it('creates a document with initial values', async () => {
|
|
|
144
158
|
})
|
|
145
159
|
|
|
146
160
|
it('edits existing documents', async () => {
|
|
147
|
-
const
|
|
148
|
-
const
|
|
161
|
+
const documentId = DocumentId('existing-doc')
|
|
162
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
163
|
+
const state = getDocumentState<TestDocument>(instance, doc)
|
|
149
164
|
|
|
150
165
|
// not subscribed yet so the value is undefined
|
|
151
166
|
expect(state.getCurrent()).toBeUndefined()
|
|
@@ -156,15 +171,16 @@ it('edits existing documents', async () => {
|
|
|
156
171
|
await firstValueFrom(state.observable.pipe(first((i) => !!i)))
|
|
157
172
|
|
|
158
173
|
expect(state.getCurrent()).toMatchObject({
|
|
159
|
-
_id: getDraftId(
|
|
174
|
+
_id: getDraftId(documentId),
|
|
160
175
|
title: 'existing doc',
|
|
161
176
|
})
|
|
162
177
|
|
|
163
178
|
await applyDocumentActions(instance, {
|
|
164
179
|
actions: [editDocument(doc, {set: {title: 'updated title'}})],
|
|
180
|
+
resource,
|
|
165
181
|
})
|
|
166
182
|
expect(state.getCurrent()).toMatchObject({
|
|
167
|
-
_id: getDraftId(
|
|
183
|
+
_id: getDraftId(documentId),
|
|
168
184
|
title: 'updated title',
|
|
169
185
|
})
|
|
170
186
|
|
|
@@ -172,23 +188,31 @@ it('edits existing documents', async () => {
|
|
|
172
188
|
})
|
|
173
189
|
|
|
174
190
|
it('sets optimistic changes synchronously', async () => {
|
|
175
|
-
const
|
|
191
|
+
const doc1 = {
|
|
192
|
+
documentId: DocumentId('optimistic'),
|
|
193
|
+
documentType: 'article',
|
|
194
|
+
}
|
|
195
|
+
const doc2 = {
|
|
196
|
+
documentId: DocumentId('optimistic'),
|
|
197
|
+
documentType: 'article',
|
|
198
|
+
}
|
|
176
199
|
|
|
177
|
-
const state1 = getDocumentState(instance1,
|
|
178
|
-
const state2 = getDocumentState(instance2,
|
|
200
|
+
const state1 = getDocumentState(instance1, doc1)
|
|
201
|
+
const state2 = getDocumentState(instance2, doc2)
|
|
179
202
|
|
|
180
203
|
const unsubscribe1 = state1.subscribe()
|
|
181
204
|
const unsubscribe2 = state2.subscribe()
|
|
182
205
|
|
|
183
206
|
// wait until the value is primed in the store
|
|
184
|
-
await resolveDocument(instance1,
|
|
207
|
+
await resolveDocument(instance1, doc1)
|
|
185
208
|
|
|
186
209
|
// then the actions are synchronous
|
|
187
210
|
expect(state1.getCurrent()).toBeNull()
|
|
188
|
-
applyDocumentActions(instance1, {actions: [createDocument(
|
|
189
|
-
expect(state1.getCurrent()).toMatchObject({_id: getDraftId(
|
|
211
|
+
applyDocumentActions(instance1, {actions: [createDocument(doc1)], resource: source1})
|
|
212
|
+
expect(state1.getCurrent()).toMatchObject({_id: getDraftId(doc1.documentId)})
|
|
190
213
|
const actionResult1Promise = applyDocumentActions(instance1, {
|
|
191
|
-
actions: [editDocument(
|
|
214
|
+
actions: [editDocument(doc1, {set: {title: 'initial title'}})],
|
|
215
|
+
resource: source1,
|
|
192
216
|
})
|
|
193
217
|
expect(state1.getCurrent()?.title).toBe('initial title')
|
|
194
218
|
|
|
@@ -208,7 +232,8 @@ it('sets optimistic changes synchronously', async () => {
|
|
|
208
232
|
|
|
209
233
|
// synchronous for state 2
|
|
210
234
|
const actionResult2Promise = applyDocumentActions(instance2, {
|
|
211
|
-
actions: [editDocument(
|
|
235
|
+
actions: [editDocument(doc2, {set: {title: 'updated title'}})],
|
|
236
|
+
resource: source2,
|
|
212
237
|
})
|
|
213
238
|
expect(state2.getCurrent()?.title).toBe('updated title')
|
|
214
239
|
// async for state 1
|
|
@@ -231,16 +256,19 @@ it('propagates changes between two instances', async () => {
|
|
|
231
256
|
const state2Unsubscribe = state2.subscribe()
|
|
232
257
|
|
|
233
258
|
// Create the document from instance1.
|
|
234
|
-
await applyDocumentActions(instance1, {actions: [createDocument(doc)]}).then(
|
|
259
|
+
await applyDocumentActions(instance1, {actions: [createDocument(doc)], resource: source1}).then(
|
|
260
|
+
(r) => r.submitted(),
|
|
261
|
+
)
|
|
235
262
|
|
|
236
263
|
const doc1 = state1.getCurrent()
|
|
237
264
|
const doc2 = state2.getCurrent()
|
|
238
|
-
expect(doc1?._id).toEqual(getDraftId(doc.documentId))
|
|
239
|
-
expect(doc2?._id).toEqual(getDraftId(doc.documentId))
|
|
265
|
+
expect(doc1?._id).toEqual(getDraftId(DocumentId(doc.documentId)))
|
|
266
|
+
expect(doc2?._id).toEqual(getDraftId(DocumentId(doc.documentId)))
|
|
240
267
|
|
|
241
268
|
// Now, edit the document from instance2.
|
|
242
269
|
await applyDocumentActions(instance2, {
|
|
243
270
|
actions: [editDocument(doc, {set: {title: 'Hello world!'}})],
|
|
271
|
+
resource: source2,
|
|
244
272
|
}).then((r) => r.submitted())
|
|
245
273
|
|
|
246
274
|
const updated1 = state1.getCurrent()
|
|
@@ -268,16 +296,19 @@ it('handles concurrent edits and resolves conflicts', async () => {
|
|
|
268
296
|
createDocument(doc),
|
|
269
297
|
editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy dog'}}),
|
|
270
298
|
],
|
|
299
|
+
resource,
|
|
271
300
|
}).then((res) => res.submitted())
|
|
272
301
|
|
|
273
302
|
// Both instances now issue an edit simultaneously.
|
|
274
303
|
const p1 = applyDocumentActions(instance1, {
|
|
275
304
|
actions: [editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy cat'}})],
|
|
305
|
+
resource: source1,
|
|
276
306
|
}).then((r) => r.submitted())
|
|
277
307
|
const p2 = applyDocumentActions(instance2, {
|
|
278
308
|
actions: [
|
|
279
309
|
editDocument(doc, {set: {title: 'The quick brown elephant jumps over the lazy dog'}}),
|
|
280
310
|
],
|
|
311
|
+
resource: source2,
|
|
281
312
|
}).then((r) => r.submitted())
|
|
282
313
|
|
|
283
314
|
// Wait for both actions to complete (or reject).
|
|
@@ -294,27 +325,31 @@ it('handles concurrent edits and resolves conflicts', async () => {
|
|
|
294
325
|
})
|
|
295
326
|
|
|
296
327
|
it('unpublishes and discards a document', async () => {
|
|
297
|
-
const
|
|
298
|
-
const
|
|
328
|
+
const documentId = DocumentId('doc-pub-unpub')
|
|
329
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
330
|
+
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
299
331
|
const unsubscribe = documentState.subscribe()
|
|
300
332
|
|
|
301
333
|
// Create and publish the document.
|
|
302
|
-
await applyDocumentActions(instance, {actions: [createDocument(doc)]})
|
|
303
|
-
const afterPublish = await applyDocumentActions(instance, {
|
|
334
|
+
await applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
|
|
335
|
+
const afterPublish = await applyDocumentActions(instance, {
|
|
336
|
+
actions: [publishDocument(doc)],
|
|
337
|
+
resource,
|
|
338
|
+
})
|
|
304
339
|
const publishedDoc = documentState.getCurrent()
|
|
305
340
|
expect(publishedDoc).toMatchObject({
|
|
306
|
-
_id: getPublishedId(
|
|
341
|
+
_id: getPublishedId(documentId),
|
|
307
342
|
_rev: afterPublish.transactionId,
|
|
308
343
|
})
|
|
309
344
|
|
|
310
345
|
// Unpublish the document (which should delete the published version and create a draft).
|
|
311
|
-
await applyDocumentActions(instance, {actions: [unpublishDocument(doc)]})
|
|
346
|
+
await applyDocumentActions(instance, {actions: [unpublishDocument(doc)], resource})
|
|
312
347
|
const afterUnpublish = documentState.getCurrent()
|
|
313
348
|
// In our mock implementation the _id remains the same but the published copy is removed.
|
|
314
|
-
expect(afterUnpublish?._id).toEqual(getDraftId(
|
|
349
|
+
expect(afterUnpublish?._id).toEqual(getDraftId(documentId))
|
|
315
350
|
|
|
316
351
|
// Discard the draft (which deletes the draft version).
|
|
317
|
-
await applyDocumentActions(instance, {actions: [discardDocument(doc)]})
|
|
352
|
+
await applyDocumentActions(instance, {actions: [discardDocument(doc)], resource})
|
|
318
353
|
const afterDiscard = documentState.getCurrent()
|
|
319
354
|
expect(afterDiscard).toBeNull()
|
|
320
355
|
|
|
@@ -322,17 +357,21 @@ it('unpublishes and discards a document', async () => {
|
|
|
322
357
|
})
|
|
323
358
|
|
|
324
359
|
it('deletes a document', async () => {
|
|
325
|
-
const
|
|
360
|
+
const documentId = DocumentId('doc-delete')
|
|
361
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
326
362
|
|
|
327
363
|
const documentState = getDocumentState(instance, doc)
|
|
328
364
|
const unsubscribe = documentState.subscribe()
|
|
329
365
|
|
|
330
|
-
await applyDocumentActions(instance, {
|
|
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,15 @@ it('deletes a document', async () => {
|
|
|
340
379
|
})
|
|
341
380
|
|
|
342
381
|
it('cleans up document state when there are no subscribers', async () => {
|
|
343
|
-
const
|
|
344
|
-
const
|
|
382
|
+
const documentId = DocumentId('doc-cleanup')
|
|
383
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
384
|
+
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
345
385
|
|
|
346
386
|
// Subscribe to the document state.
|
|
347
387
|
const unsubscribe = documentState.subscribe()
|
|
348
388
|
|
|
349
389
|
// Create a document.
|
|
350
|
-
await applyDocumentActions(instance, {actions: [createDocument(doc)]})
|
|
390
|
+
await applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
|
|
351
391
|
expect(documentState.getCurrent()).toBeDefined()
|
|
352
392
|
|
|
353
393
|
// Unsubscribe from the document.
|
|
@@ -374,6 +414,7 @@ it('fetches documents if there are no active subscriptions for the actions appli
|
|
|
374
414
|
// transaction for this action has been accepted by the server
|
|
375
415
|
const setNewTitle = applyDocumentActions(instance, {
|
|
376
416
|
actions: [editDocument(doc, {set: {title: 'new title'}})],
|
|
417
|
+
resource,
|
|
377
418
|
})
|
|
378
419
|
expect(getCurrent()?.title).toBeUndefined()
|
|
379
420
|
expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBe(false)
|
|
@@ -382,9 +423,15 @@ it('fetches documents if there are no active subscriptions for the actions appli
|
|
|
382
423
|
expect(getCurrent()?.title).toBe('new title')
|
|
383
424
|
|
|
384
425
|
// there is an active subscriber now so the edits are synchronous
|
|
385
|
-
applyDocumentActions(instance, {
|
|
426
|
+
applyDocumentActions(instance, {
|
|
427
|
+
actions: [editDocument(doc, {set: {title: 'updated title'}})],
|
|
428
|
+
resource,
|
|
429
|
+
})
|
|
386
430
|
expect(getCurrent()?.title).toBe('updated title')
|
|
387
|
-
applyDocumentActions(instance, {
|
|
431
|
+
applyDocumentActions(instance, {
|
|
432
|
+
actions: [editDocument(doc, {set: {title: 'updated title!'}})],
|
|
433
|
+
resource,
|
|
434
|
+
})
|
|
388
435
|
expect(getCurrent()?.title).toBe('updated title!')
|
|
389
436
|
|
|
390
437
|
expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBe(false)
|
|
@@ -392,6 +439,7 @@ it('fetches documents if there are no active subscriptions for the actions appli
|
|
|
392
439
|
// await submitted in order to test that there is no subscriptions
|
|
393
440
|
const result = await applyDocumentActions(instance, {
|
|
394
441
|
actions: [editDocument(doc, {set: {title: 'updated title'}})],
|
|
442
|
+
resource,
|
|
395
443
|
})
|
|
396
444
|
await result.submitted()
|
|
397
445
|
|
|
@@ -400,6 +448,7 @@ it('fetches documents if there are no active subscriptions for the actions appli
|
|
|
400
448
|
|
|
401
449
|
const setNewNewTitle = applyDocumentActions(instance, {
|
|
402
450
|
actions: [editDocument(doc, {set: {title: 'new new title'}})],
|
|
451
|
+
resource,
|
|
403
452
|
})
|
|
404
453
|
// now we'll have to await again
|
|
405
454
|
expect(getCurrent()?.title).toBe(undefined)
|
|
@@ -414,14 +463,18 @@ it('batches edit transaction into one outgoing transaction', async () => {
|
|
|
414
463
|
const unsubscribe = getDocumentState(instance, doc).subscribe()
|
|
415
464
|
|
|
416
465
|
// this creates its own transaction
|
|
417
|
-
applyDocumentActions(instance, {actions: [createDocument(doc)]})
|
|
466
|
+
applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
|
|
418
467
|
|
|
419
468
|
// 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, {
|
|
469
|
+
applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!'}})], resource})
|
|
470
|
+
applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!!'}})], resource})
|
|
471
|
+
applyDocumentActions(instance, {
|
|
472
|
+
actions: [editDocument(doc, {set: {title: 'name!!!'}})],
|
|
473
|
+
resource,
|
|
474
|
+
})
|
|
423
475
|
const res = await applyDocumentActions(instance, {
|
|
424
476
|
actions: [editDocument(doc, {set: {title: 'name!!!!'}})],
|
|
477
|
+
resource,
|
|
425
478
|
})
|
|
426
479
|
await res.submitted()
|
|
427
480
|
|
|
@@ -434,6 +487,52 @@ it('batches edit transaction into one outgoing transaction', async () => {
|
|
|
434
487
|
unsubscribe()
|
|
435
488
|
})
|
|
436
489
|
|
|
490
|
+
it('submits liveEdit document edits through observable.mutate', async () => {
|
|
491
|
+
const liveDoc = createDocumentHandle({
|
|
492
|
+
documentId: 'live-edit-test-doc',
|
|
493
|
+
documentType: 'article',
|
|
494
|
+
liveEdit: true,
|
|
495
|
+
})
|
|
496
|
+
const state = getDocumentState(instance, liveDoc)
|
|
497
|
+
const unsubscribe = state.subscribe()
|
|
498
|
+
|
|
499
|
+
await firstValueFrom(state.observable.pipe(first((doc) => doc !== undefined)))
|
|
500
|
+
expect(state.getCurrent()).toMatchObject({
|
|
501
|
+
_id: 'live-edit-test-doc',
|
|
502
|
+
title: 'live initial',
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const callCountBefore = mutateSubmission.mock.calls.length
|
|
506
|
+
|
|
507
|
+
const result = await applyDocumentActions(instance, {
|
|
508
|
+
actions: [editDocument(liveDoc, {set: {title: 'patched via mutate'}})],
|
|
509
|
+
resource,
|
|
510
|
+
})
|
|
511
|
+
await result.submitted()
|
|
512
|
+
|
|
513
|
+
expect(mutateSubmission.mock.calls.length).toBe(callCountBefore + 1)
|
|
514
|
+
const [mutationList, mutateOptions] = mutateSubmission.mock.calls[callCountBefore]!
|
|
515
|
+
expect(mutateOptions).toMatchObject({
|
|
516
|
+
transactionId: result.transactionId,
|
|
517
|
+
visibility: 'async',
|
|
518
|
+
returnDocuments: false,
|
|
519
|
+
returnFirst: false,
|
|
520
|
+
tag: 'document.mutate',
|
|
521
|
+
skipCrossDatasetReferenceValidation: true,
|
|
522
|
+
})
|
|
523
|
+
expect(mutationList.length).toBeGreaterThan(0)
|
|
524
|
+
expect(
|
|
525
|
+
mutationList.every(
|
|
526
|
+
(m) => 'patch' in m && (m as {patch: {id: string}}).patch.id === liveDoc.documentId,
|
|
527
|
+
),
|
|
528
|
+
).toBe(true)
|
|
529
|
+
|
|
530
|
+
expect(state.getCurrent()?.title).toBe('patched via mutate')
|
|
531
|
+
expect(client.action).not.toHaveBeenCalled()
|
|
532
|
+
|
|
533
|
+
unsubscribe()
|
|
534
|
+
})
|
|
535
|
+
|
|
437
536
|
it('provides the consistency status via `getDocumentSyncStatus`', async () => {
|
|
438
537
|
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
439
538
|
|
|
@@ -443,7 +542,7 @@ it('provides the consistency status via `getDocumentSyncStatus`', async () => {
|
|
|
443
542
|
const unsubscribe = syncStatus.subscribe()
|
|
444
543
|
expect(syncStatus.getCurrent()).toBe(true)
|
|
445
544
|
|
|
446
|
-
const applied = applyDocumentActions(instance, {actions: [createDocument(doc)]})
|
|
545
|
+
const applied = applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
|
|
447
546
|
expect(syncStatus.getCurrent()).toBe(false)
|
|
448
547
|
|
|
449
548
|
const createResult = await applied
|
|
@@ -452,11 +551,17 @@ it('provides the consistency status via `getDocumentSyncStatus`', async () => {
|
|
|
452
551
|
await createResult.submitted()
|
|
453
552
|
expect(syncStatus.getCurrent()).toBe(true)
|
|
454
553
|
|
|
455
|
-
applyDocumentActions(instance, {
|
|
554
|
+
applyDocumentActions(instance, {
|
|
555
|
+
actions: [editDocument(doc, {set: {title: 'initial name'}})],
|
|
556
|
+
resource,
|
|
557
|
+
})
|
|
456
558
|
expect(syncStatus.getCurrent()).toBe(false)
|
|
457
559
|
|
|
458
|
-
applyDocumentActions(instance, {
|
|
459
|
-
|
|
560
|
+
applyDocumentActions(instance, {
|
|
561
|
+
actions: [editDocument(doc, {set: {title: 'updated name'}})],
|
|
562
|
+
resource,
|
|
563
|
+
})
|
|
564
|
+
const publishResult = applyDocumentActions(instance, {actions: [publishDocument(doc)], resource})
|
|
460
565
|
expect(syncStatus.getCurrent()).toBe(false)
|
|
461
566
|
await publishResult.then((res) => res.submitted())
|
|
462
567
|
expect(syncStatus.getCurrent()).toBe(true)
|
|
@@ -473,35 +578,45 @@ it('reverts failed outgoing transaction locally', async () => {
|
|
|
473
578
|
})
|
|
474
579
|
|
|
475
580
|
const revertedEventPromise = new Promise<TransactionRevertedEvent>((resolve) => {
|
|
476
|
-
const unsubscribe = subscribeDocumentEvents(instance,
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
581
|
+
const unsubscribe = subscribeDocumentEvents(instance, {
|
|
582
|
+
resource,
|
|
583
|
+
eventHandler: (e) => {
|
|
584
|
+
if (e.type === 'reverted') {
|
|
585
|
+
resolve(e)
|
|
586
|
+
unsubscribe()
|
|
587
|
+
}
|
|
588
|
+
},
|
|
481
589
|
})
|
|
482
590
|
})
|
|
483
591
|
|
|
484
|
-
const
|
|
592
|
+
const documentId = DocumentId(crypto.randomUUID())
|
|
593
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
485
594
|
|
|
486
595
|
const {getCurrent, subscribe} = getDocumentState(instance, doc)
|
|
487
596
|
const unsubscribe = subscribe()
|
|
488
597
|
|
|
489
|
-
await applyDocumentActions(instance, {actions: [createDocument(doc)]})
|
|
490
|
-
applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'the'}})]})
|
|
491
|
-
applyDocumentActions(instance, {
|
|
598
|
+
await applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
|
|
599
|
+
applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'the'}})], resource})
|
|
600
|
+
applyDocumentActions(instance, {
|
|
601
|
+
actions: [editDocument(doc, {set: {title: 'the quick'}})],
|
|
602
|
+
resource,
|
|
603
|
+
})
|
|
492
604
|
|
|
493
605
|
// this edit action is simulated to fail from the backend and will be reverted
|
|
494
606
|
const revertedActionResult = applyDocumentActions(instance, {
|
|
495
607
|
actions: [editDocument(doc, {set: {title: 'the quick brown'}})],
|
|
496
608
|
transactionId: 'force-revert',
|
|
497
609
|
disableBatching: true,
|
|
610
|
+
resource,
|
|
498
611
|
})
|
|
499
612
|
|
|
500
613
|
applyDocumentActions(instance, {
|
|
501
614
|
actions: [editDocument(doc, {set: {title: 'the quick brown fox'}})],
|
|
615
|
+
resource,
|
|
502
616
|
})
|
|
503
617
|
await applyDocumentActions(instance, {
|
|
504
618
|
actions: [editDocument(doc, {set: {title: 'the quick brown fox jumps'}})],
|
|
619
|
+
resource,
|
|
505
620
|
}).then((e) => e.submitted())
|
|
506
621
|
|
|
507
622
|
await expect(revertedEventPromise).resolves.toMatchObject({
|
|
@@ -521,6 +636,7 @@ it('reverts failed outgoing transaction locally', async () => {
|
|
|
521
636
|
// check that we can still edit after recovering from the error
|
|
522
637
|
applyDocumentActions(instance, {
|
|
523
638
|
actions: [editDocument(doc, {set: {title: 'TEST the quick fox jumps'}})],
|
|
639
|
+
resource,
|
|
524
640
|
})
|
|
525
641
|
expect(getCurrent()?.title).toBe('TEST the quick fox jumps')
|
|
526
642
|
|
|
@@ -530,11 +646,14 @@ it('reverts failed outgoing transaction locally', async () => {
|
|
|
530
646
|
|
|
531
647
|
it('removes a queued transaction if it fails to apply', async () => {
|
|
532
648
|
const actionErrorEventPromise = new Promise<ActionErrorEvent>((resolve) => {
|
|
533
|
-
const unsubscribe = subscribeDocumentEvents(instance,
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
649
|
+
const unsubscribe = subscribeDocumentEvents(instance, {
|
|
650
|
+
resource,
|
|
651
|
+
eventHandler: (e) => {
|
|
652
|
+
if (e.type === 'error') {
|
|
653
|
+
resolve(e)
|
|
654
|
+
unsubscribe()
|
|
655
|
+
}
|
|
656
|
+
},
|
|
538
657
|
})
|
|
539
658
|
})
|
|
540
659
|
|
|
@@ -543,7 +662,10 @@ it('removes a queued transaction if it fails to apply', async () => {
|
|
|
543
662
|
const unsubscribe = state.subscribe()
|
|
544
663
|
|
|
545
664
|
await expect(
|
|
546
|
-
applyDocumentActions(instance, {
|
|
665
|
+
applyDocumentActions(instance, {
|
|
666
|
+
actions: [editDocument(doc, {set: {title: "can't set"}})],
|
|
667
|
+
resource,
|
|
668
|
+
}),
|
|
547
669
|
).rejects.toThrowError(/Cannot edit document/)
|
|
548
670
|
|
|
549
671
|
await expect(actionErrorEventPromise).resolves.toMatchObject({
|
|
@@ -553,8 +675,11 @@ it('removes a queued transaction if it fails to apply', async () => {
|
|
|
553
675
|
})
|
|
554
676
|
|
|
555
677
|
// editing should still work after though (no crashing)
|
|
556
|
-
await applyDocumentActions(instance, {actions: [createDocument(doc)]})
|
|
557
|
-
applyDocumentActions(instance, {
|
|
678
|
+
await applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
|
|
679
|
+
applyDocumentActions(instance, {
|
|
680
|
+
actions: [editDocument(doc, {set: {title: 'can set!'}})],
|
|
681
|
+
resource,
|
|
682
|
+
})
|
|
558
683
|
|
|
559
684
|
expect(state.getCurrent()?.title).toBe('can set!')
|
|
560
685
|
|
|
@@ -574,7 +699,9 @@ it('returns allowed true when no permission errors occur', async () => {
|
|
|
574
699
|
})
|
|
575
700
|
const state = getDocumentState(instance, doc)
|
|
576
701
|
const unsubscribe = state.subscribe()
|
|
577
|
-
await applyDocumentActions(instance, {actions: [createDocument(doc)]}).then((r) =>
|
|
702
|
+
await applyDocumentActions(instance, {actions: [createDocument(doc)], resource}).then((r) =>
|
|
703
|
+
r.submitted(),
|
|
704
|
+
)
|
|
578
705
|
|
|
579
706
|
// Use an action that includes a patch (so that update permission check is bypassed).
|
|
580
707
|
const permissionsState = getPermissionsState(instance, {
|
|
@@ -596,9 +723,9 @@ it('returns allowed true when no permission errors occur', async () => {
|
|
|
596
723
|
it("should reject applying the action if a precondition isn't met", async () => {
|
|
597
724
|
const doc = createDocumentHandle({documentId: 'does-not-exist', documentType: 'article'})
|
|
598
725
|
|
|
599
|
-
await expect(
|
|
600
|
-
|
|
601
|
-
)
|
|
726
|
+
await expect(
|
|
727
|
+
applyDocumentActions(instance, {actions: [deleteDocument(doc)], resource}),
|
|
728
|
+
).rejects.toThrow('The document you are trying to delete does not exist.')
|
|
602
729
|
})
|
|
603
730
|
|
|
604
731
|
it("should reject applying the action if a permission isn't met", async () => {
|
|
@@ -607,9 +734,9 @@ it("should reject applying the action if a permission isn't met", async () => {
|
|
|
607
734
|
const datasetAcl = [{filter: 'false', permissions: ['create']}]
|
|
608
735
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
609
736
|
|
|
610
|
-
await expect(
|
|
611
|
-
|
|
612
|
-
)
|
|
737
|
+
await expect(
|
|
738
|
+
applyDocumentActions(instance, {actions: [createDocument(doc)], resource}),
|
|
739
|
+
).rejects.toThrow('You do not have permission to create a draft for document "does-not-exist".')
|
|
613
740
|
})
|
|
614
741
|
|
|
615
742
|
it('returns allowed false with reasons when permission errors occur', async () => {
|
|
@@ -647,8 +774,57 @@ it('fetches dataset ACL and updates grants in the document store state', async (
|
|
|
647
774
|
})
|
|
648
775
|
})
|
|
649
776
|
|
|
650
|
-
it('
|
|
777
|
+
it('fetches ACL for MediaLibraryResource', async () => {
|
|
778
|
+
const mediaLibraryInstance = createSanityInstance({
|
|
779
|
+
projectId: 'p',
|
|
780
|
+
dataset: 'd',
|
|
781
|
+
resources: {
|
|
782
|
+
'media-library': {mediaLibraryId: 'test-media-library'},
|
|
783
|
+
},
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
|
|
787
|
+
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
788
|
+
|
|
789
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
790
|
+
const mediaLibrarySource = {mediaLibraryId: 'test-media-library'}
|
|
791
|
+
|
|
792
|
+
const result = await resolvePermissions(mediaLibraryInstance, {
|
|
793
|
+
actions: [createDocument(doc)],
|
|
794
|
+
resource: mediaLibrarySource,
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
expect(result).toEqual({allowed: true})
|
|
798
|
+
mediaLibraryInstance.dispose()
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
it('fetches ACL for CanvasResource', async () => {
|
|
802
|
+
const canvasInstance = createSanityInstance({
|
|
803
|
+
projectId: 'p',
|
|
804
|
+
dataset: 'd',
|
|
805
|
+
resources: {
|
|
806
|
+
canvas: {canvasId: 'test-canvas'},
|
|
807
|
+
},
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
|
|
811
|
+
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
812
|
+
|
|
651
813
|
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
814
|
+
const canvasSource = {canvasId: 'test-canvas'}
|
|
815
|
+
|
|
816
|
+
const result = await resolvePermissions(canvasInstance, {
|
|
817
|
+
actions: [createDocument(doc)],
|
|
818
|
+
resource: canvasSource,
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
expect(result).toEqual({allowed: true})
|
|
822
|
+
canvasInstance.dispose()
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
it('returns a promise that resolves when a document has been loaded in the store (useful for suspense)', async () => {
|
|
826
|
+
const documentId = DocumentId(crypto.randomUUID())
|
|
827
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
652
828
|
|
|
653
829
|
expect(await resolveDocument(instance, doc)).toBe(null)
|
|
654
830
|
|
|
@@ -656,11 +832,12 @@ it('returns a promise that resolves when a document has been loaded in the store
|
|
|
656
832
|
const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
657
833
|
const result = await applyDocumentActions(oneOffInstance, {
|
|
658
834
|
actions: [createDocument(doc), editDocument(doc, {set: {title: 'initial title'}})],
|
|
835
|
+
resource,
|
|
659
836
|
})
|
|
660
837
|
await result.submitted() // wait till submitted to server before resolving
|
|
661
838
|
|
|
662
839
|
await expect(resolveDocument(instance, doc)).resolves.toMatchObject({
|
|
663
|
-
_id: getDraftId(
|
|
840
|
+
_id: getDraftId(documentId),
|
|
664
841
|
_type: 'article',
|
|
665
842
|
title: 'initial title',
|
|
666
843
|
})
|
|
@@ -669,9 +846,9 @@ it('returns a promise that resolves when a document has been loaded in the store
|
|
|
669
846
|
|
|
670
847
|
it('emits an event for each action after an outgoing transaction has been accepted', async () => {
|
|
671
848
|
const handler = vi.fn()
|
|
672
|
-
const unsubscribe = subscribeDocumentEvents(instance, handler)
|
|
849
|
+
const unsubscribe = subscribeDocumentEvents(instance, {resource, eventHandler: handler})
|
|
673
850
|
|
|
674
|
-
const documentId = crypto.randomUUID()
|
|
851
|
+
const documentId = DocumentId(crypto.randomUUID())
|
|
675
852
|
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
676
853
|
expect(handler).toHaveBeenCalledTimes(0)
|
|
677
854
|
|
|
@@ -681,6 +858,7 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
681
858
|
editDocument(doc, {set: {title: 'new name'}}),
|
|
682
859
|
publishDocument(doc),
|
|
683
860
|
],
|
|
861
|
+
resource,
|
|
684
862
|
}).then((e) => e.submitted())
|
|
685
863
|
expect(handler).toHaveBeenCalledTimes(4)
|
|
686
864
|
|
|
@@ -691,6 +869,7 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
691
869
|
editDocument(doc, {set: {title: 'updated name'}}),
|
|
692
870
|
discardDocument(doc),
|
|
693
871
|
],
|
|
872
|
+
resource,
|
|
694
873
|
}).then((e) => e.submitted())
|
|
695
874
|
expect(handler).toHaveBeenCalledTimes(9)
|
|
696
875
|
|
|
@@ -706,11 +885,127 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
706
885
|
[{type: 'accepted', outgoing: {transactionId: tnx2.transactionId}}],
|
|
707
886
|
])
|
|
708
887
|
|
|
709
|
-
await applyDocumentActions(instance, {actions: [deleteDocument(doc)]})
|
|
888
|
+
await applyDocumentActions(instance, {actions: [deleteDocument(doc)], resource})
|
|
889
|
+
|
|
890
|
+
unsubscribe()
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
it('creates and edits a version document with a release perspective', async () => {
|
|
894
|
+
const documentId = DocumentId('doc-release')
|
|
895
|
+
const releaseName = 'test-release'
|
|
896
|
+
const doc = createDocumentHandle({
|
|
897
|
+
documentId,
|
|
898
|
+
documentType: 'article',
|
|
899
|
+
perspective: {releaseName},
|
|
900
|
+
})
|
|
901
|
+
const versionId = `versions.${releaseName}.${documentId}`
|
|
902
|
+
|
|
903
|
+
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
904
|
+
expect(documentState.getCurrent()).toBeUndefined()
|
|
905
|
+
|
|
906
|
+
const unsubscribe = documentState.subscribe()
|
|
907
|
+
|
|
908
|
+
// Create a version document for the release
|
|
909
|
+
const {appeared} = await applyDocumentActions(instance, {
|
|
910
|
+
actions: [createDocument(doc)],
|
|
911
|
+
})
|
|
912
|
+
expect(appeared).toContain(versionId)
|
|
913
|
+
|
|
914
|
+
let currentDoc = documentState.getCurrent()
|
|
915
|
+
expect(currentDoc?._id).toEqual(versionId)
|
|
916
|
+
expect(currentDoc?._type).toEqual('article')
|
|
917
|
+
|
|
918
|
+
// Edit the version document
|
|
919
|
+
await applyDocumentActions(instance, {
|
|
920
|
+
actions: [editDocument(doc, {set: {title: 'Release Version Title'}})],
|
|
921
|
+
})
|
|
922
|
+
currentDoc = documentState.getCurrent()
|
|
923
|
+
expect(currentDoc?.title).toEqual('Release Version Title')
|
|
924
|
+
expect(currentDoc?._id).toEqual(versionId)
|
|
925
|
+
|
|
926
|
+
unsubscribe()
|
|
927
|
+
})
|
|
928
|
+
|
|
929
|
+
it('creates a version document with initial values and then discards it', async () => {
|
|
930
|
+
const documentId = DocumentId('doc-release-discard')
|
|
931
|
+
const releaseName = 'test-release-discard'
|
|
932
|
+
const doc = createDocumentHandle({
|
|
933
|
+
documentId,
|
|
934
|
+
documentType: 'article',
|
|
935
|
+
perspective: {releaseName},
|
|
936
|
+
})
|
|
937
|
+
const versionId = `versions.${releaseName}.${documentId}`
|
|
938
|
+
|
|
939
|
+
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
940
|
+
const unsubscribe = documentState.subscribe()
|
|
941
|
+
|
|
942
|
+
// Create a version document with initial values
|
|
943
|
+
await applyDocumentActions(instance, {
|
|
944
|
+
actions: [createDocument(doc, {title: 'Initial Release Title'})],
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
let currentDoc = documentState.getCurrent()
|
|
948
|
+
expect(currentDoc?._id).toEqual(versionId)
|
|
949
|
+
expect(currentDoc?.title).toEqual('Initial Release Title')
|
|
950
|
+
|
|
951
|
+
// Discard the version document
|
|
952
|
+
const {disappeared} = await applyDocumentActions(instance, {
|
|
953
|
+
actions: [discardDocument(doc)],
|
|
954
|
+
})
|
|
955
|
+
expect(disappeared).toContain(versionId)
|
|
956
|
+
|
|
957
|
+
currentDoc = documentState.getCurrent()
|
|
958
|
+
expect(currentDoc).toBeNull()
|
|
710
959
|
|
|
711
960
|
unsubscribe()
|
|
712
961
|
})
|
|
713
962
|
|
|
963
|
+
it('version edits are isolated from draft state', async () => {
|
|
964
|
+
const documentId = DocumentId('doc-version-isolation')
|
|
965
|
+
const releaseName = 'isolation-release'
|
|
966
|
+
const versionDoc = createDocumentHandle({
|
|
967
|
+
documentId,
|
|
968
|
+
documentType: 'article',
|
|
969
|
+
perspective: {releaseName},
|
|
970
|
+
})
|
|
971
|
+
const draftDoc = createDocumentHandle({documentId, documentType: 'article'})
|
|
972
|
+
const versionId = `versions.${releaseName}.${documentId}`
|
|
973
|
+
|
|
974
|
+
const versionState = getDocumentState<TestDocument>(instance, versionDoc)
|
|
975
|
+
const draftState = getDocumentState<TestDocument>(instance, draftDoc)
|
|
976
|
+
|
|
977
|
+
const unsubscribeVersion = versionState.subscribe()
|
|
978
|
+
const unsubscribeDraft = draftState.subscribe()
|
|
979
|
+
|
|
980
|
+
// Create draft and version documents
|
|
981
|
+
await applyDocumentActions(instance, {
|
|
982
|
+
actions: [createDocument(draftDoc)],
|
|
983
|
+
})
|
|
984
|
+
await applyDocumentActions(instance, {
|
|
985
|
+
actions: [editDocument(draftDoc, {set: {title: 'Draft Title'}})],
|
|
986
|
+
})
|
|
987
|
+
await applyDocumentActions(instance, {
|
|
988
|
+
actions: [createDocument(versionDoc, {title: 'Release Title'})],
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
// Version perspective shows the version doc
|
|
992
|
+
expect(versionState.getCurrent()?._id).toEqual(versionId)
|
|
993
|
+
expect(versionState.getCurrent()?.title).toEqual('Release Title')
|
|
994
|
+
|
|
995
|
+
// Draft state shows the draft doc
|
|
996
|
+
expect(draftState.getCurrent()?.title).toEqual('Draft Title')
|
|
997
|
+
|
|
998
|
+
// Editing the version doc should not affect the draft
|
|
999
|
+
await applyDocumentActions(instance, {
|
|
1000
|
+
actions: [editDocument(versionDoc, {set: {title: 'Updated Release Title'}})],
|
|
1001
|
+
})
|
|
1002
|
+
expect(versionState.getCurrent()?.title).toEqual('Updated Release Title')
|
|
1003
|
+
expect(draftState.getCurrent()?.title).toEqual('Draft Title')
|
|
1004
|
+
|
|
1005
|
+
unsubscribeVersion()
|
|
1006
|
+
unsubscribeDraft()
|
|
1007
|
+
})
|
|
1008
|
+
|
|
714
1009
|
vi.mock('../client/clientStore.ts', () => ({
|
|
715
1010
|
getClientState: vi.fn().mockReturnValue({observable: new ReplaySubject(1)}),
|
|
716
1011
|
}))
|
|
@@ -749,7 +1044,21 @@ vi.mock('./documentConstants.ts', async (importOriginal) => {
|
|
|
749
1044
|
|
|
750
1045
|
let client: SanityClient
|
|
751
1046
|
|
|
1047
|
+
/** Mock for `client.observable.mutate` (liveEdit submission path); implementation reset in `beforeEach`. */
|
|
1048
|
+
const mutateSubmission = vi.fn(
|
|
1049
|
+
async (
|
|
1050
|
+
_mutations: Mutation[],
|
|
1051
|
+
_options?: BaseMutationOptions,
|
|
1052
|
+
): Promise<MultipleMutationResult> => ({
|
|
1053
|
+
transactionId: '',
|
|
1054
|
+
documentIds: [],
|
|
1055
|
+
results: [],
|
|
1056
|
+
}),
|
|
1057
|
+
)
|
|
1058
|
+
|
|
752
1059
|
beforeEach(() => {
|
|
1060
|
+
mutateSubmission.mockReset()
|
|
1061
|
+
|
|
753
1062
|
const client$ = (getClientState as () => StateSource<SanityClient>)()
|
|
754
1063
|
.observable as ReplaySubject<SanityClient>
|
|
755
1064
|
const sharedListener = (
|
|
@@ -760,14 +1069,130 @@ beforeEach(() => {
|
|
|
760
1069
|
)()
|
|
761
1070
|
|
|
762
1071
|
let documents: DocumentSet = {
|
|
763
|
-
[getDraftId('existing-doc')]: {
|
|
764
|
-
_id: getDraftId('existing-doc'),
|
|
1072
|
+
[getDraftId(DocumentId('existing-doc'))]: {
|
|
1073
|
+
_id: getDraftId(DocumentId('existing-doc')),
|
|
765
1074
|
_createdAt: '2025-02-06T06:43:46.236Z',
|
|
766
1075
|
_updatedAt: '2025-02-06T06:43:46.236Z',
|
|
767
1076
|
_rev: 'initial-rev',
|
|
768
1077
|
_type: 'book',
|
|
769
1078
|
title: 'existing doc',
|
|
770
1079
|
},
|
|
1080
|
+
'live-edit-test-doc': {
|
|
1081
|
+
_id: 'live-edit-test-doc',
|
|
1082
|
+
_type: 'article',
|
|
1083
|
+
_createdAt: '2025-02-06T06:43:46.236Z',
|
|
1084
|
+
_updatedAt: '2025-02-06T06:43:46.236Z',
|
|
1085
|
+
_rev: 'rev-live-initial',
|
|
1086
|
+
title: 'live initial',
|
|
1087
|
+
},
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const emitDatasetChangeEvents = (
|
|
1091
|
+
prior: DocumentSet,
|
|
1092
|
+
next: DocumentSet,
|
|
1093
|
+
transactionId: string,
|
|
1094
|
+
timestamp: string,
|
|
1095
|
+
) => {
|
|
1096
|
+
const existingIds = new Set(
|
|
1097
|
+
Object.entries(prior)
|
|
1098
|
+
.filter(([, value]) => !!value)
|
|
1099
|
+
.map(([key]) => key),
|
|
1100
|
+
)
|
|
1101
|
+
const resultingIds = new Set(
|
|
1102
|
+
Object.entries(next)
|
|
1103
|
+
.filter(([, value]) => !!value)
|
|
1104
|
+
.map(([key]) => key),
|
|
1105
|
+
)
|
|
1106
|
+
const allKeys = new Set([...existingIds, ...resultingIds])
|
|
1107
|
+
|
|
1108
|
+
const {appeared, disappeared, updated} = Array.from(allKeys).reduce<{
|
|
1109
|
+
updated: string[]
|
|
1110
|
+
appeared: string[]
|
|
1111
|
+
disappeared: string[]
|
|
1112
|
+
}>(
|
|
1113
|
+
(acc, id) => {
|
|
1114
|
+
if (existingIds.has(id) && resultingIds.has(id)) {
|
|
1115
|
+
acc.updated.push(id)
|
|
1116
|
+
} else if (!existingIds.has(id) && resultingIds.has(id)) {
|
|
1117
|
+
acc.appeared.push(id)
|
|
1118
|
+
} else if (!resultingIds.has(id) && existingIds.has(id)) {
|
|
1119
|
+
acc.disappeared.push(id)
|
|
1120
|
+
}
|
|
1121
|
+
return acc
|
|
1122
|
+
},
|
|
1123
|
+
{updated: [], appeared: [], disappeared: []},
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
const transactionTotalEvents = appeared.length + disappeared.length + updated.length
|
|
1127
|
+
let transactionCurrentEvent = 0
|
|
1128
|
+
|
|
1129
|
+
const mutationEvents: MutationEvent[] = []
|
|
1130
|
+
|
|
1131
|
+
for (const id of appeared) {
|
|
1132
|
+
transactionCurrentEvent++
|
|
1133
|
+
const nextDoc = next[id]!
|
|
1134
|
+
mutationEvents.push({
|
|
1135
|
+
type: 'mutation',
|
|
1136
|
+
documentId: id,
|
|
1137
|
+
eventId: `${transactionId}#${id}`,
|
|
1138
|
+
identity: 'example-user',
|
|
1139
|
+
mutations: [{create: nextDoc}],
|
|
1140
|
+
timestamp,
|
|
1141
|
+
transactionId,
|
|
1142
|
+
transactionCurrentEvent,
|
|
1143
|
+
transactionTotalEvents,
|
|
1144
|
+
transition: 'appear',
|
|
1145
|
+
visibility: 'query',
|
|
1146
|
+
resultRev: transactionId,
|
|
1147
|
+
})
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
for (const id of updated) {
|
|
1151
|
+
transactionCurrentEvent++
|
|
1152
|
+
const prevDoc = prior[id]!
|
|
1153
|
+
const nextDoc = next[id]!
|
|
1154
|
+
|
|
1155
|
+
mutationEvents.push({
|
|
1156
|
+
type: 'mutation',
|
|
1157
|
+
documentId: id,
|
|
1158
|
+
eventId: `${transactionId}#${id}`,
|
|
1159
|
+
identity: 'example-user',
|
|
1160
|
+
mutations: diffValue(prevDoc, nextDoc).map((patch): Mutation => ({patch: {...patch, id}})),
|
|
1161
|
+
timestamp,
|
|
1162
|
+
transactionCurrentEvent,
|
|
1163
|
+
transactionTotalEvents,
|
|
1164
|
+
transactionId,
|
|
1165
|
+
transition: 'update',
|
|
1166
|
+
visibility: 'query',
|
|
1167
|
+
previousRev: prevDoc._rev,
|
|
1168
|
+
resultRev: transactionId,
|
|
1169
|
+
})
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
for (const id of disappeared) {
|
|
1173
|
+
transactionCurrentEvent++
|
|
1174
|
+
const prevDoc = prior[id]!
|
|
1175
|
+
mutationEvents.push({
|
|
1176
|
+
type: 'mutation',
|
|
1177
|
+
documentId: id,
|
|
1178
|
+
eventId: `${transactionId}#${id}`,
|
|
1179
|
+
identity: 'example-user',
|
|
1180
|
+
mutations: [{delete: {id}}],
|
|
1181
|
+
timestamp,
|
|
1182
|
+
transactionId,
|
|
1183
|
+
transactionCurrentEvent,
|
|
1184
|
+
transactionTotalEvents,
|
|
1185
|
+
transition: 'disappear',
|
|
1186
|
+
visibility: 'query',
|
|
1187
|
+
previousRev: prevDoc._rev,
|
|
1188
|
+
})
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
documents = next
|
|
1192
|
+
|
|
1193
|
+
for (const mutationEvent of mutationEvents) {
|
|
1194
|
+
sharedListener.events.next(mutationEvent)
|
|
1195
|
+
}
|
|
771
1196
|
}
|
|
772
1197
|
|
|
773
1198
|
vi.mocked(createFetchDocument).mockReturnValue(
|
|
@@ -779,6 +1204,33 @@ beforeEach(() => {
|
|
|
779
1204
|
),
|
|
780
1205
|
)
|
|
781
1206
|
|
|
1207
|
+
mutateSubmission.mockImplementation(
|
|
1208
|
+
async (
|
|
1209
|
+
mutations: Mutation[],
|
|
1210
|
+
options: BaseMutationOptions = {},
|
|
1211
|
+
): Promise<MultipleMutationResult> => {
|
|
1212
|
+
const transactionId = options.transactionId ?? crypto.randomUUID()
|
|
1213
|
+
const timestamp = new Date().toISOString()
|
|
1214
|
+
const prior = {...documents}
|
|
1215
|
+
let next = {...documents}
|
|
1216
|
+
next = processMutations({documents: next, mutations, transactionId, timestamp})
|
|
1217
|
+
|
|
1218
|
+
if (!options.dryRun) {
|
|
1219
|
+
emitDatasetChangeEvents(prior, next, transactionId, timestamp)
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
1223
|
+
|
|
1224
|
+
return {
|
|
1225
|
+
transactionId,
|
|
1226
|
+
documentIds: Object.entries(next)
|
|
1227
|
+
.filter(([, v]) => !!v)
|
|
1228
|
+
.map(([k]) => k),
|
|
1229
|
+
results: [],
|
|
1230
|
+
}
|
|
1231
|
+
},
|
|
1232
|
+
)
|
|
1233
|
+
|
|
782
1234
|
const isNonNullable = <T>(t: T): t is NonNullable<T> => !!t
|
|
783
1235
|
|
|
784
1236
|
const fetch = vi.fn(
|
|
@@ -859,8 +1311,8 @@ beforeEach(() => {
|
|
|
859
1311
|
continue
|
|
860
1312
|
}
|
|
861
1313
|
case 'sanity.action.document.edit': {
|
|
862
|
-
const
|
|
863
|
-
if (!
|
|
1314
|
+
const documentSource = (i.draftId && next[i.draftId]) ?? next[i.publishedId]
|
|
1315
|
+
if (!documentSource) {
|
|
864
1316
|
throw new Error(
|
|
865
1317
|
`Could not find a document to edit from \`draftId\` \`${i.draftId}\` or \`publishedId\` ${i.publishedId}`,
|
|
866
1318
|
)
|
|
@@ -869,8 +1321,8 @@ beforeEach(() => {
|
|
|
869
1321
|
next = processMutations({
|
|
870
1322
|
documents: next,
|
|
871
1323
|
mutations: [
|
|
872
|
-
{createIfNotExists: {...
|
|
873
|
-
{patch: {id: i.draftId
|
|
1324
|
+
{createIfNotExists: {...documentSource, _id: i.draftId!}},
|
|
1325
|
+
{patch: {id: i.draftId!, ...i.patch}},
|
|
874
1326
|
],
|
|
875
1327
|
transactionId,
|
|
876
1328
|
timestamp,
|
|
@@ -951,114 +1403,15 @@ beforeEach(() => {
|
|
|
951
1403
|
continue
|
|
952
1404
|
}
|
|
953
1405
|
default: {
|
|
954
|
-
throw new Error(
|
|
1406
|
+
throw new Error(
|
|
1407
|
+
`Unsupported action for mock backend: ${(i as {actionType: string}).actionType}`,
|
|
1408
|
+
)
|
|
955
1409
|
}
|
|
956
1410
|
}
|
|
957
1411
|
}
|
|
958
1412
|
|
|
959
1413
|
if (!dryRun) {
|
|
960
|
-
|
|
961
|
-
Object.entries(documents)
|
|
962
|
-
.filter(([, value]) => !!value)
|
|
963
|
-
.map(([key]) => key),
|
|
964
|
-
)
|
|
965
|
-
const resultingIds = new Set(
|
|
966
|
-
Object.entries(next)
|
|
967
|
-
.filter(([, value]) => !!value)
|
|
968
|
-
.map(([key]) => key),
|
|
969
|
-
)
|
|
970
|
-
const allKeys = new Set([...existingIds, ...resultingIds])
|
|
971
|
-
|
|
972
|
-
const {appeared, disappeared, updated} = Array.from(allKeys).reduce<{
|
|
973
|
-
updated: string[]
|
|
974
|
-
appeared: string[]
|
|
975
|
-
disappeared: string[]
|
|
976
|
-
}>(
|
|
977
|
-
(acc, id) => {
|
|
978
|
-
if (existingIds.has(id) && resultingIds.has(id)) {
|
|
979
|
-
acc.updated.push(id)
|
|
980
|
-
} else if (!existingIds.has(id) && resultingIds.has(id)) {
|
|
981
|
-
acc.appeared.push(id)
|
|
982
|
-
} else if (!resultingIds.has(id) && existingIds.has(id)) {
|
|
983
|
-
acc.disappeared.push(id)
|
|
984
|
-
}
|
|
985
|
-
return acc
|
|
986
|
-
},
|
|
987
|
-
{updated: [], appeared: [], disappeared: []},
|
|
988
|
-
)
|
|
989
|
-
|
|
990
|
-
const transactionTotalEvents = appeared.length + disappeared.length + updated.length
|
|
991
|
-
let transactionCurrentEvent = 0
|
|
992
|
-
|
|
993
|
-
const mutationEvents: MutationEvent[] = []
|
|
994
|
-
|
|
995
|
-
for (const id of appeared) {
|
|
996
|
-
transactionCurrentEvent++ // index seems to start at 1
|
|
997
|
-
const nextDoc = next[id]!
|
|
998
|
-
mutationEvents.push({
|
|
999
|
-
type: 'mutation',
|
|
1000
|
-
documentId: id,
|
|
1001
|
-
eventId: `${transactionId}#${id}`,
|
|
1002
|
-
identity: 'example-user',
|
|
1003
|
-
mutations: [{create: nextDoc}],
|
|
1004
|
-
timestamp,
|
|
1005
|
-
transactionId,
|
|
1006
|
-
transactionCurrentEvent,
|
|
1007
|
-
transactionTotalEvents,
|
|
1008
|
-
transition: 'appear',
|
|
1009
|
-
visibility: 'query',
|
|
1010
|
-
resultRev: transactionId,
|
|
1011
|
-
})
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
for (const id of updated) {
|
|
1015
|
-
transactionCurrentEvent++
|
|
1016
|
-
const prevDoc = documents[id]!
|
|
1017
|
-
const nextDoc = next[id]!
|
|
1018
|
-
|
|
1019
|
-
mutationEvents.push({
|
|
1020
|
-
type: 'mutation',
|
|
1021
|
-
documentId: id,
|
|
1022
|
-
eventId: `${transactionId}#${id}`,
|
|
1023
|
-
identity: 'example-user',
|
|
1024
|
-
mutations: diffValue(prevDoc, nextDoc).map(
|
|
1025
|
-
(patch): Mutation => ({patch: {...patch, id}}),
|
|
1026
|
-
),
|
|
1027
|
-
timestamp,
|
|
1028
|
-
transactionCurrentEvent,
|
|
1029
|
-
transactionTotalEvents,
|
|
1030
|
-
transactionId,
|
|
1031
|
-
transition: 'update',
|
|
1032
|
-
visibility: 'query',
|
|
1033
|
-
previousRev: prevDoc._rev,
|
|
1034
|
-
resultRev: transactionId,
|
|
1035
|
-
})
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
for (const id of disappeared) {
|
|
1039
|
-
transactionCurrentEvent++
|
|
1040
|
-
const prevDoc = documents[id]!
|
|
1041
|
-
mutationEvents.push({
|
|
1042
|
-
type: 'mutation',
|
|
1043
|
-
documentId: id,
|
|
1044
|
-
eventId: `${transactionId}#${id}`,
|
|
1045
|
-
identity: 'example-user',
|
|
1046
|
-
mutations: [{delete: {id}}],
|
|
1047
|
-
timestamp,
|
|
1048
|
-
transactionId,
|
|
1049
|
-
transactionCurrentEvent,
|
|
1050
|
-
transactionTotalEvents,
|
|
1051
|
-
transition: 'disappear',
|
|
1052
|
-
visibility: 'query',
|
|
1053
|
-
previousRev: prevDoc._rev,
|
|
1054
|
-
})
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
documents = next
|
|
1058
|
-
|
|
1059
|
-
for (const mutationEvent of mutationEvents) {
|
|
1060
|
-
sharedListener.events.next(mutationEvent)
|
|
1061
|
-
}
|
|
1414
|
+
emitDatasetChangeEvents(documents, next, transactionId, timestamp)
|
|
1062
1415
|
}
|
|
1063
1416
|
|
|
1064
1417
|
// add a tick for realism
|
|
@@ -1085,6 +1438,7 @@ beforeEach(() => {
|
|
|
1085
1438
|
action: (...args: Parameters<typeof action>) => from(action(...args)),
|
|
1086
1439
|
fetch: (...args: Parameters<typeof fetch>) => from(fetch(...args)),
|
|
1087
1440
|
request: (...args: Parameters<typeof request>) => from(request(...args)),
|
|
1441
|
+
mutate: (...args: Parameters<typeof mutateSubmission>) => from(mutateSubmission(...args)),
|
|
1088
1442
|
},
|
|
1089
1443
|
} as SanityClient
|
|
1090
1444
|
client$.next(client)
|