@sanity/sdk 2.7.0 → 2.9.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 +2396 -0
- package/dist/_chunks-es/_internal.js +129 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1460 -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 +383 -1777
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
- package/src/_exports/_internal.ts +14 -0
- package/src/_exports/index.ts +10 -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 +14 -0
- package/src/client/clientStore.ts +2 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/sanityConfig.ts +6 -0
- 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 +536 -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 +104 -76
- package/src/document/reducers.test.ts +117 -29
- package/src/document/reducers.ts +43 -36
- 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.ts +6 -1
- 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 +29 -10
- package/src/preview/{previewStore.ts → types.ts} +8 -17
- package/src/projection/getProjectionState.test.ts +16 -16
- package/src/projection/getProjectionState.ts +2 -1
- package/src/projection/projectionQuery.ts +2 -3
- package/src/projection/types.ts +1 -1
- package/src/query/queryStore.ts +2 -1
- 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/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/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 +2 -1
- 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/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 source = {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
|
+
source,
|
|
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
|
+
source,
|
|
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
|
+
source,
|
|
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
|
+
source,
|
|
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
|
+
source,
|
|
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)], source: 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
|
+
source: 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
|
+
source: 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)], source: 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
|
+
source: 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
|
+
source,
|
|
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
|
+
source: 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
|
+
source: 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)], source})
|
|
335
|
+
const afterPublish = await applyDocumentActions(instance, {
|
|
336
|
+
actions: [publishDocument(doc)],
|
|
337
|
+
source,
|
|
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)], source})
|
|
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)], source})
|
|
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
|
+
source,
|
|
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)], source})
|
|
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)], source})
|
|
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
|
+
source,
|
|
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
|
+
source,
|
|
429
|
+
})
|
|
386
430
|
expect(getCurrent()?.title).toBe('updated title')
|
|
387
|
-
applyDocumentActions(instance, {
|
|
431
|
+
applyDocumentActions(instance, {
|
|
432
|
+
actions: [editDocument(doc, {set: {title: 'updated title!'}})],
|
|
433
|
+
source,
|
|
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
|
+
source,
|
|
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
|
+
source,
|
|
403
452
|
})
|
|
404
453
|
// now we'll have to await again
|
|
405
454
|
expect(getCurrent()?.title).toBe(undefined)
|
|
@@ -414,14 +463,15 @@ 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)], source})
|
|
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, {actions: [editDocument(doc, {set: {title: 'name!!!'}})]})
|
|
469
|
+
applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!'}})], source})
|
|
470
|
+
applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!!'}})], source})
|
|
471
|
+
applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'name!!!'}})], source})
|
|
423
472
|
const res = await applyDocumentActions(instance, {
|
|
424
473
|
actions: [editDocument(doc, {set: {title: 'name!!!!'}})],
|
|
474
|
+
source,
|
|
425
475
|
})
|
|
426
476
|
await res.submitted()
|
|
427
477
|
|
|
@@ -434,6 +484,52 @@ it('batches edit transaction into one outgoing transaction', async () => {
|
|
|
434
484
|
unsubscribe()
|
|
435
485
|
})
|
|
436
486
|
|
|
487
|
+
it('submits liveEdit document edits through observable.mutate', async () => {
|
|
488
|
+
const liveDoc = createDocumentHandle({
|
|
489
|
+
documentId: 'live-edit-test-doc',
|
|
490
|
+
documentType: 'article',
|
|
491
|
+
liveEdit: true,
|
|
492
|
+
})
|
|
493
|
+
const state = getDocumentState(instance, liveDoc)
|
|
494
|
+
const unsubscribe = state.subscribe()
|
|
495
|
+
|
|
496
|
+
await firstValueFrom(state.observable.pipe(first((doc) => doc !== undefined)))
|
|
497
|
+
expect(state.getCurrent()).toMatchObject({
|
|
498
|
+
_id: 'live-edit-test-doc',
|
|
499
|
+
title: 'live initial',
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
const callCountBefore = mutateSubmission.mock.calls.length
|
|
503
|
+
|
|
504
|
+
const result = await applyDocumentActions(instance, {
|
|
505
|
+
actions: [editDocument(liveDoc, {set: {title: 'patched via mutate'}})],
|
|
506
|
+
source,
|
|
507
|
+
})
|
|
508
|
+
await result.submitted()
|
|
509
|
+
|
|
510
|
+
expect(mutateSubmission.mock.calls.length).toBe(callCountBefore + 1)
|
|
511
|
+
const [mutationList, mutateOptions] = mutateSubmission.mock.calls[callCountBefore]!
|
|
512
|
+
expect(mutateOptions).toMatchObject({
|
|
513
|
+
transactionId: result.transactionId,
|
|
514
|
+
visibility: 'async',
|
|
515
|
+
returnDocuments: false,
|
|
516
|
+
returnFirst: false,
|
|
517
|
+
tag: 'document.mutate',
|
|
518
|
+
skipCrossDatasetReferenceValidation: true,
|
|
519
|
+
})
|
|
520
|
+
expect(mutationList.length).toBeGreaterThan(0)
|
|
521
|
+
expect(
|
|
522
|
+
mutationList.every(
|
|
523
|
+
(m) => 'patch' in m && (m as {patch: {id: string}}).patch.id === liveDoc.documentId,
|
|
524
|
+
),
|
|
525
|
+
).toBe(true)
|
|
526
|
+
|
|
527
|
+
expect(state.getCurrent()?.title).toBe('patched via mutate')
|
|
528
|
+
expect(client.action).not.toHaveBeenCalled()
|
|
529
|
+
|
|
530
|
+
unsubscribe()
|
|
531
|
+
})
|
|
532
|
+
|
|
437
533
|
it('provides the consistency status via `getDocumentSyncStatus`', async () => {
|
|
438
534
|
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
439
535
|
|
|
@@ -443,7 +539,7 @@ it('provides the consistency status via `getDocumentSyncStatus`', async () => {
|
|
|
443
539
|
const unsubscribe = syncStatus.subscribe()
|
|
444
540
|
expect(syncStatus.getCurrent()).toBe(true)
|
|
445
541
|
|
|
446
|
-
const applied = applyDocumentActions(instance, {actions: [createDocument(doc)]})
|
|
542
|
+
const applied = applyDocumentActions(instance, {actions: [createDocument(doc)], source})
|
|
447
543
|
expect(syncStatus.getCurrent()).toBe(false)
|
|
448
544
|
|
|
449
545
|
const createResult = await applied
|
|
@@ -452,11 +548,17 @@ it('provides the consistency status via `getDocumentSyncStatus`', async () => {
|
|
|
452
548
|
await createResult.submitted()
|
|
453
549
|
expect(syncStatus.getCurrent()).toBe(true)
|
|
454
550
|
|
|
455
|
-
applyDocumentActions(instance, {
|
|
551
|
+
applyDocumentActions(instance, {
|
|
552
|
+
actions: [editDocument(doc, {set: {title: 'initial name'}})],
|
|
553
|
+
source,
|
|
554
|
+
})
|
|
456
555
|
expect(syncStatus.getCurrent()).toBe(false)
|
|
457
556
|
|
|
458
|
-
applyDocumentActions(instance, {
|
|
459
|
-
|
|
557
|
+
applyDocumentActions(instance, {
|
|
558
|
+
actions: [editDocument(doc, {set: {title: 'updated name'}})],
|
|
559
|
+
source,
|
|
560
|
+
})
|
|
561
|
+
const publishResult = applyDocumentActions(instance, {actions: [publishDocument(doc)], source})
|
|
460
562
|
expect(syncStatus.getCurrent()).toBe(false)
|
|
461
563
|
await publishResult.then((res) => res.submitted())
|
|
462
564
|
expect(syncStatus.getCurrent()).toBe(true)
|
|
@@ -473,35 +575,45 @@ it('reverts failed outgoing transaction locally', async () => {
|
|
|
473
575
|
})
|
|
474
576
|
|
|
475
577
|
const revertedEventPromise = new Promise<TransactionRevertedEvent>((resolve) => {
|
|
476
|
-
const unsubscribe = subscribeDocumentEvents(instance,
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
578
|
+
const unsubscribe = subscribeDocumentEvents(instance, {
|
|
579
|
+
source,
|
|
580
|
+
eventHandler: (e) => {
|
|
581
|
+
if (e.type === 'reverted') {
|
|
582
|
+
resolve(e)
|
|
583
|
+
unsubscribe()
|
|
584
|
+
}
|
|
585
|
+
},
|
|
481
586
|
})
|
|
482
587
|
})
|
|
483
588
|
|
|
484
|
-
const
|
|
589
|
+
const documentId = DocumentId(crypto.randomUUID())
|
|
590
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
485
591
|
|
|
486
592
|
const {getCurrent, subscribe} = getDocumentState(instance, doc)
|
|
487
593
|
const unsubscribe = subscribe()
|
|
488
594
|
|
|
489
|
-
await applyDocumentActions(instance, {actions: [createDocument(doc)]})
|
|
490
|
-
applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'the'}})]})
|
|
491
|
-
applyDocumentActions(instance, {
|
|
595
|
+
await applyDocumentActions(instance, {actions: [createDocument(doc)], source})
|
|
596
|
+
applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'the'}})], source})
|
|
597
|
+
applyDocumentActions(instance, {
|
|
598
|
+
actions: [editDocument(doc, {set: {title: 'the quick'}})],
|
|
599
|
+
source,
|
|
600
|
+
})
|
|
492
601
|
|
|
493
602
|
// this edit action is simulated to fail from the backend and will be reverted
|
|
494
603
|
const revertedActionResult = applyDocumentActions(instance, {
|
|
495
604
|
actions: [editDocument(doc, {set: {title: 'the quick brown'}})],
|
|
496
605
|
transactionId: 'force-revert',
|
|
497
606
|
disableBatching: true,
|
|
607
|
+
source,
|
|
498
608
|
})
|
|
499
609
|
|
|
500
610
|
applyDocumentActions(instance, {
|
|
501
611
|
actions: [editDocument(doc, {set: {title: 'the quick brown fox'}})],
|
|
612
|
+
source,
|
|
502
613
|
})
|
|
503
614
|
await applyDocumentActions(instance, {
|
|
504
615
|
actions: [editDocument(doc, {set: {title: 'the quick brown fox jumps'}})],
|
|
616
|
+
source,
|
|
505
617
|
}).then((e) => e.submitted())
|
|
506
618
|
|
|
507
619
|
await expect(revertedEventPromise).resolves.toMatchObject({
|
|
@@ -521,6 +633,7 @@ it('reverts failed outgoing transaction locally', async () => {
|
|
|
521
633
|
// check that we can still edit after recovering from the error
|
|
522
634
|
applyDocumentActions(instance, {
|
|
523
635
|
actions: [editDocument(doc, {set: {title: 'TEST the quick fox jumps'}})],
|
|
636
|
+
source,
|
|
524
637
|
})
|
|
525
638
|
expect(getCurrent()?.title).toBe('TEST the quick fox jumps')
|
|
526
639
|
|
|
@@ -530,11 +643,14 @@ it('reverts failed outgoing transaction locally', async () => {
|
|
|
530
643
|
|
|
531
644
|
it('removes a queued transaction if it fails to apply', async () => {
|
|
532
645
|
const actionErrorEventPromise = new Promise<ActionErrorEvent>((resolve) => {
|
|
533
|
-
const unsubscribe = subscribeDocumentEvents(instance,
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
646
|
+
const unsubscribe = subscribeDocumentEvents(instance, {
|
|
647
|
+
source,
|
|
648
|
+
eventHandler: (e) => {
|
|
649
|
+
if (e.type === 'error') {
|
|
650
|
+
resolve(e)
|
|
651
|
+
unsubscribe()
|
|
652
|
+
}
|
|
653
|
+
},
|
|
538
654
|
})
|
|
539
655
|
})
|
|
540
656
|
|
|
@@ -543,7 +659,10 @@ it('removes a queued transaction if it fails to apply', async () => {
|
|
|
543
659
|
const unsubscribe = state.subscribe()
|
|
544
660
|
|
|
545
661
|
await expect(
|
|
546
|
-
applyDocumentActions(instance, {
|
|
662
|
+
applyDocumentActions(instance, {
|
|
663
|
+
actions: [editDocument(doc, {set: {title: "can't set"}})],
|
|
664
|
+
source,
|
|
665
|
+
}),
|
|
547
666
|
).rejects.toThrowError(/Cannot edit document/)
|
|
548
667
|
|
|
549
668
|
await expect(actionErrorEventPromise).resolves.toMatchObject({
|
|
@@ -553,8 +672,8 @@ it('removes a queued transaction if it fails to apply', async () => {
|
|
|
553
672
|
})
|
|
554
673
|
|
|
555
674
|
// 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!'}})]})
|
|
675
|
+
await applyDocumentActions(instance, {actions: [createDocument(doc)], source})
|
|
676
|
+
applyDocumentActions(instance, {actions: [editDocument(doc, {set: {title: 'can set!'}})], source})
|
|
558
677
|
|
|
559
678
|
expect(state.getCurrent()?.title).toBe('can set!')
|
|
560
679
|
|
|
@@ -574,7 +693,9 @@ it('returns allowed true when no permission errors occur', async () => {
|
|
|
574
693
|
})
|
|
575
694
|
const state = getDocumentState(instance, doc)
|
|
576
695
|
const unsubscribe = state.subscribe()
|
|
577
|
-
await applyDocumentActions(instance, {actions: [createDocument(doc)]}).then((r) =>
|
|
696
|
+
await applyDocumentActions(instance, {actions: [createDocument(doc)], source}).then((r) =>
|
|
697
|
+
r.submitted(),
|
|
698
|
+
)
|
|
578
699
|
|
|
579
700
|
// Use an action that includes a patch (so that update permission check is bypassed).
|
|
580
701
|
const permissionsState = getPermissionsState(instance, {
|
|
@@ -596,9 +717,9 @@ it('returns allowed true when no permission errors occur', async () => {
|
|
|
596
717
|
it("should reject applying the action if a precondition isn't met", async () => {
|
|
597
718
|
const doc = createDocumentHandle({documentId: 'does-not-exist', documentType: 'article'})
|
|
598
719
|
|
|
599
|
-
await expect(
|
|
600
|
-
|
|
601
|
-
)
|
|
720
|
+
await expect(
|
|
721
|
+
applyDocumentActions(instance, {actions: [deleteDocument(doc)], source}),
|
|
722
|
+
).rejects.toThrow('The document you are trying to delete does not exist.')
|
|
602
723
|
})
|
|
603
724
|
|
|
604
725
|
it("should reject applying the action if a permission isn't met", async () => {
|
|
@@ -607,9 +728,9 @@ it("should reject applying the action if a permission isn't met", async () => {
|
|
|
607
728
|
const datasetAcl = [{filter: 'false', permissions: ['create']}]
|
|
608
729
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
609
730
|
|
|
610
|
-
await expect(
|
|
611
|
-
|
|
612
|
-
)
|
|
731
|
+
await expect(
|
|
732
|
+
applyDocumentActions(instance, {actions: [createDocument(doc)], source}),
|
|
733
|
+
).rejects.toThrow('You do not have permission to create a draft for document "does-not-exist".')
|
|
613
734
|
})
|
|
614
735
|
|
|
615
736
|
it('returns allowed false with reasons when permission errors occur', async () => {
|
|
@@ -647,8 +768,57 @@ it('fetches dataset ACL and updates grants in the document store state', async (
|
|
|
647
768
|
})
|
|
648
769
|
})
|
|
649
770
|
|
|
650
|
-
it('
|
|
771
|
+
it('fetches ACL for MediaLibrarySource', async () => {
|
|
772
|
+
const mediaLibraryInstance = createSanityInstance({
|
|
773
|
+
projectId: 'p',
|
|
774
|
+
dataset: 'd',
|
|
775
|
+
sources: {
|
|
776
|
+
'media-library': {mediaLibraryId: 'test-media-library'},
|
|
777
|
+
},
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
|
|
781
|
+
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
782
|
+
|
|
783
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
784
|
+
const mediaLibrarySource = {mediaLibraryId: 'test-media-library'}
|
|
785
|
+
|
|
786
|
+
const result = await resolvePermissions(mediaLibraryInstance, {
|
|
787
|
+
actions: [createDocument(doc)],
|
|
788
|
+
source: mediaLibrarySource,
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
expect(result).toEqual({allowed: true})
|
|
792
|
+
mediaLibraryInstance.dispose()
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
it('fetches ACL for CanvasSource', async () => {
|
|
796
|
+
const canvasInstance = createSanityInstance({
|
|
797
|
+
projectId: 'p',
|
|
798
|
+
dataset: 'd',
|
|
799
|
+
sources: {
|
|
800
|
+
canvas: {canvasId: 'test-canvas'},
|
|
801
|
+
},
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
|
|
805
|
+
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
806
|
+
|
|
651
807
|
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
808
|
+
const canvasSource = {canvasId: 'test-canvas'}
|
|
809
|
+
|
|
810
|
+
const result = await resolvePermissions(canvasInstance, {
|
|
811
|
+
actions: [createDocument(doc)],
|
|
812
|
+
source: canvasSource,
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
expect(result).toEqual({allowed: true})
|
|
816
|
+
canvasInstance.dispose()
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
it('returns a promise that resolves when a document has been loaded in the store (useful for suspense)', async () => {
|
|
820
|
+
const documentId = DocumentId(crypto.randomUUID())
|
|
821
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
652
822
|
|
|
653
823
|
expect(await resolveDocument(instance, doc)).toBe(null)
|
|
654
824
|
|
|
@@ -656,11 +826,12 @@ it('returns a promise that resolves when a document has been loaded in the store
|
|
|
656
826
|
const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
657
827
|
const result = await applyDocumentActions(oneOffInstance, {
|
|
658
828
|
actions: [createDocument(doc), editDocument(doc, {set: {title: 'initial title'}})],
|
|
829
|
+
source,
|
|
659
830
|
})
|
|
660
831
|
await result.submitted() // wait till submitted to server before resolving
|
|
661
832
|
|
|
662
833
|
await expect(resolveDocument(instance, doc)).resolves.toMatchObject({
|
|
663
|
-
_id: getDraftId(
|
|
834
|
+
_id: getDraftId(documentId),
|
|
664
835
|
_type: 'article',
|
|
665
836
|
title: 'initial title',
|
|
666
837
|
})
|
|
@@ -669,9 +840,9 @@ it('returns a promise that resolves when a document has been loaded in the store
|
|
|
669
840
|
|
|
670
841
|
it('emits an event for each action after an outgoing transaction has been accepted', async () => {
|
|
671
842
|
const handler = vi.fn()
|
|
672
|
-
const unsubscribe = subscribeDocumentEvents(instance, handler)
|
|
843
|
+
const unsubscribe = subscribeDocumentEvents(instance, {source, eventHandler: handler})
|
|
673
844
|
|
|
674
|
-
const documentId = crypto.randomUUID()
|
|
845
|
+
const documentId = DocumentId(crypto.randomUUID())
|
|
675
846
|
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
676
847
|
expect(handler).toHaveBeenCalledTimes(0)
|
|
677
848
|
|
|
@@ -681,6 +852,7 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
681
852
|
editDocument(doc, {set: {title: 'new name'}}),
|
|
682
853
|
publishDocument(doc),
|
|
683
854
|
],
|
|
855
|
+
source,
|
|
684
856
|
}).then((e) => e.submitted())
|
|
685
857
|
expect(handler).toHaveBeenCalledTimes(4)
|
|
686
858
|
|
|
@@ -691,6 +863,7 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
691
863
|
editDocument(doc, {set: {title: 'updated name'}}),
|
|
692
864
|
discardDocument(doc),
|
|
693
865
|
],
|
|
866
|
+
source,
|
|
694
867
|
}).then((e) => e.submitted())
|
|
695
868
|
expect(handler).toHaveBeenCalledTimes(9)
|
|
696
869
|
|
|
@@ -706,11 +879,127 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
706
879
|
[{type: 'accepted', outgoing: {transactionId: tnx2.transactionId}}],
|
|
707
880
|
])
|
|
708
881
|
|
|
709
|
-
await applyDocumentActions(instance, {actions: [deleteDocument(doc)]})
|
|
882
|
+
await applyDocumentActions(instance, {actions: [deleteDocument(doc)], source})
|
|
710
883
|
|
|
711
884
|
unsubscribe()
|
|
712
885
|
})
|
|
713
886
|
|
|
887
|
+
it('creates and edits a version document with a release perspective', async () => {
|
|
888
|
+
const documentId = DocumentId('doc-release')
|
|
889
|
+
const releaseName = 'test-release'
|
|
890
|
+
const doc = createDocumentHandle({
|
|
891
|
+
documentId,
|
|
892
|
+
documentType: 'article',
|
|
893
|
+
perspective: {releaseName},
|
|
894
|
+
})
|
|
895
|
+
const versionId = `versions.${releaseName}.${documentId}`
|
|
896
|
+
|
|
897
|
+
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
898
|
+
expect(documentState.getCurrent()).toBeUndefined()
|
|
899
|
+
|
|
900
|
+
const unsubscribe = documentState.subscribe()
|
|
901
|
+
|
|
902
|
+
// Create a version document for the release
|
|
903
|
+
const {appeared} = await applyDocumentActions(instance, {
|
|
904
|
+
actions: [createDocument(doc)],
|
|
905
|
+
})
|
|
906
|
+
expect(appeared).toContain(versionId)
|
|
907
|
+
|
|
908
|
+
let currentDoc = documentState.getCurrent()
|
|
909
|
+
expect(currentDoc?._id).toEqual(versionId)
|
|
910
|
+
expect(currentDoc?._type).toEqual('article')
|
|
911
|
+
|
|
912
|
+
// Edit the version document
|
|
913
|
+
await applyDocumentActions(instance, {
|
|
914
|
+
actions: [editDocument(doc, {set: {title: 'Release Version Title'}})],
|
|
915
|
+
})
|
|
916
|
+
currentDoc = documentState.getCurrent()
|
|
917
|
+
expect(currentDoc?.title).toEqual('Release Version Title')
|
|
918
|
+
expect(currentDoc?._id).toEqual(versionId)
|
|
919
|
+
|
|
920
|
+
unsubscribe()
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
it('creates a version document with initial values and then discards it', async () => {
|
|
924
|
+
const documentId = DocumentId('doc-release-discard')
|
|
925
|
+
const releaseName = 'test-release-discard'
|
|
926
|
+
const doc = createDocumentHandle({
|
|
927
|
+
documentId,
|
|
928
|
+
documentType: 'article',
|
|
929
|
+
perspective: {releaseName},
|
|
930
|
+
})
|
|
931
|
+
const versionId = `versions.${releaseName}.${documentId}`
|
|
932
|
+
|
|
933
|
+
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
934
|
+
const unsubscribe = documentState.subscribe()
|
|
935
|
+
|
|
936
|
+
// Create a version document with initial values
|
|
937
|
+
await applyDocumentActions(instance, {
|
|
938
|
+
actions: [createDocument(doc, {title: 'Initial Release Title'})],
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
let currentDoc = documentState.getCurrent()
|
|
942
|
+
expect(currentDoc?._id).toEqual(versionId)
|
|
943
|
+
expect(currentDoc?.title).toEqual('Initial Release Title')
|
|
944
|
+
|
|
945
|
+
// Discard the version document
|
|
946
|
+
const {disappeared} = await applyDocumentActions(instance, {
|
|
947
|
+
actions: [discardDocument(doc)],
|
|
948
|
+
})
|
|
949
|
+
expect(disappeared).toContain(versionId)
|
|
950
|
+
|
|
951
|
+
currentDoc = documentState.getCurrent()
|
|
952
|
+
expect(currentDoc).toBeNull()
|
|
953
|
+
|
|
954
|
+
unsubscribe()
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
it('version edits are isolated from draft state', async () => {
|
|
958
|
+
const documentId = DocumentId('doc-version-isolation')
|
|
959
|
+
const releaseName = 'isolation-release'
|
|
960
|
+
const versionDoc = createDocumentHandle({
|
|
961
|
+
documentId,
|
|
962
|
+
documentType: 'article',
|
|
963
|
+
perspective: {releaseName},
|
|
964
|
+
})
|
|
965
|
+
const draftDoc = createDocumentHandle({documentId, documentType: 'article'})
|
|
966
|
+
const versionId = `versions.${releaseName}.${documentId}`
|
|
967
|
+
|
|
968
|
+
const versionState = getDocumentState<TestDocument>(instance, versionDoc)
|
|
969
|
+
const draftState = getDocumentState<TestDocument>(instance, draftDoc)
|
|
970
|
+
|
|
971
|
+
const unsubscribeVersion = versionState.subscribe()
|
|
972
|
+
const unsubscribeDraft = draftState.subscribe()
|
|
973
|
+
|
|
974
|
+
// Create draft and version documents
|
|
975
|
+
await applyDocumentActions(instance, {
|
|
976
|
+
actions: [createDocument(draftDoc)],
|
|
977
|
+
})
|
|
978
|
+
await applyDocumentActions(instance, {
|
|
979
|
+
actions: [editDocument(draftDoc, {set: {title: 'Draft Title'}})],
|
|
980
|
+
})
|
|
981
|
+
await applyDocumentActions(instance, {
|
|
982
|
+
actions: [createDocument(versionDoc, {title: 'Release Title'})],
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
// Version perspective shows the version doc
|
|
986
|
+
expect(versionState.getCurrent()?._id).toEqual(versionId)
|
|
987
|
+
expect(versionState.getCurrent()?.title).toEqual('Release Title')
|
|
988
|
+
|
|
989
|
+
// Draft state shows the draft doc
|
|
990
|
+
expect(draftState.getCurrent()?.title).toEqual('Draft Title')
|
|
991
|
+
|
|
992
|
+
// Editing the version doc should not affect the draft
|
|
993
|
+
await applyDocumentActions(instance, {
|
|
994
|
+
actions: [editDocument(versionDoc, {set: {title: 'Updated Release Title'}})],
|
|
995
|
+
})
|
|
996
|
+
expect(versionState.getCurrent()?.title).toEqual('Updated Release Title')
|
|
997
|
+
expect(draftState.getCurrent()?.title).toEqual('Draft Title')
|
|
998
|
+
|
|
999
|
+
unsubscribeVersion()
|
|
1000
|
+
unsubscribeDraft()
|
|
1001
|
+
})
|
|
1002
|
+
|
|
714
1003
|
vi.mock('../client/clientStore.ts', () => ({
|
|
715
1004
|
getClientState: vi.fn().mockReturnValue({observable: new ReplaySubject(1)}),
|
|
716
1005
|
}))
|
|
@@ -749,7 +1038,21 @@ vi.mock('./documentConstants.ts', async (importOriginal) => {
|
|
|
749
1038
|
|
|
750
1039
|
let client: SanityClient
|
|
751
1040
|
|
|
1041
|
+
/** Mock for `client.observable.mutate` (liveEdit submission path); implementation reset in `beforeEach`. */
|
|
1042
|
+
const mutateSubmission = vi.fn(
|
|
1043
|
+
async (
|
|
1044
|
+
_mutations: Mutation[],
|
|
1045
|
+
_options?: BaseMutationOptions,
|
|
1046
|
+
): Promise<MultipleMutationResult> => ({
|
|
1047
|
+
transactionId: '',
|
|
1048
|
+
documentIds: [],
|
|
1049
|
+
results: [],
|
|
1050
|
+
}),
|
|
1051
|
+
)
|
|
1052
|
+
|
|
752
1053
|
beforeEach(() => {
|
|
1054
|
+
mutateSubmission.mockReset()
|
|
1055
|
+
|
|
753
1056
|
const client$ = (getClientState as () => StateSource<SanityClient>)()
|
|
754
1057
|
.observable as ReplaySubject<SanityClient>
|
|
755
1058
|
const sharedListener = (
|
|
@@ -760,14 +1063,130 @@ beforeEach(() => {
|
|
|
760
1063
|
)()
|
|
761
1064
|
|
|
762
1065
|
let documents: DocumentSet = {
|
|
763
|
-
[getDraftId('existing-doc')]: {
|
|
764
|
-
_id: getDraftId('existing-doc'),
|
|
1066
|
+
[getDraftId(DocumentId('existing-doc'))]: {
|
|
1067
|
+
_id: getDraftId(DocumentId('existing-doc')),
|
|
765
1068
|
_createdAt: '2025-02-06T06:43:46.236Z',
|
|
766
1069
|
_updatedAt: '2025-02-06T06:43:46.236Z',
|
|
767
1070
|
_rev: 'initial-rev',
|
|
768
1071
|
_type: 'book',
|
|
769
1072
|
title: 'existing doc',
|
|
770
1073
|
},
|
|
1074
|
+
'live-edit-test-doc': {
|
|
1075
|
+
_id: 'live-edit-test-doc',
|
|
1076
|
+
_type: 'article',
|
|
1077
|
+
_createdAt: '2025-02-06T06:43:46.236Z',
|
|
1078
|
+
_updatedAt: '2025-02-06T06:43:46.236Z',
|
|
1079
|
+
_rev: 'rev-live-initial',
|
|
1080
|
+
title: 'live initial',
|
|
1081
|
+
},
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const emitDatasetChangeEvents = (
|
|
1085
|
+
prior: DocumentSet,
|
|
1086
|
+
next: DocumentSet,
|
|
1087
|
+
transactionId: string,
|
|
1088
|
+
timestamp: string,
|
|
1089
|
+
) => {
|
|
1090
|
+
const existingIds = new Set(
|
|
1091
|
+
Object.entries(prior)
|
|
1092
|
+
.filter(([, value]) => !!value)
|
|
1093
|
+
.map(([key]) => key),
|
|
1094
|
+
)
|
|
1095
|
+
const resultingIds = new Set(
|
|
1096
|
+
Object.entries(next)
|
|
1097
|
+
.filter(([, value]) => !!value)
|
|
1098
|
+
.map(([key]) => key),
|
|
1099
|
+
)
|
|
1100
|
+
const allKeys = new Set([...existingIds, ...resultingIds])
|
|
1101
|
+
|
|
1102
|
+
const {appeared, disappeared, updated} = Array.from(allKeys).reduce<{
|
|
1103
|
+
updated: string[]
|
|
1104
|
+
appeared: string[]
|
|
1105
|
+
disappeared: string[]
|
|
1106
|
+
}>(
|
|
1107
|
+
(acc, id) => {
|
|
1108
|
+
if (existingIds.has(id) && resultingIds.has(id)) {
|
|
1109
|
+
acc.updated.push(id)
|
|
1110
|
+
} else if (!existingIds.has(id) && resultingIds.has(id)) {
|
|
1111
|
+
acc.appeared.push(id)
|
|
1112
|
+
} else if (!resultingIds.has(id) && existingIds.has(id)) {
|
|
1113
|
+
acc.disappeared.push(id)
|
|
1114
|
+
}
|
|
1115
|
+
return acc
|
|
1116
|
+
},
|
|
1117
|
+
{updated: [], appeared: [], disappeared: []},
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
const transactionTotalEvents = appeared.length + disappeared.length + updated.length
|
|
1121
|
+
let transactionCurrentEvent = 0
|
|
1122
|
+
|
|
1123
|
+
const mutationEvents: MutationEvent[] = []
|
|
1124
|
+
|
|
1125
|
+
for (const id of appeared) {
|
|
1126
|
+
transactionCurrentEvent++
|
|
1127
|
+
const nextDoc = next[id]!
|
|
1128
|
+
mutationEvents.push({
|
|
1129
|
+
type: 'mutation',
|
|
1130
|
+
documentId: id,
|
|
1131
|
+
eventId: `${transactionId}#${id}`,
|
|
1132
|
+
identity: 'example-user',
|
|
1133
|
+
mutations: [{create: nextDoc}],
|
|
1134
|
+
timestamp,
|
|
1135
|
+
transactionId,
|
|
1136
|
+
transactionCurrentEvent,
|
|
1137
|
+
transactionTotalEvents,
|
|
1138
|
+
transition: 'appear',
|
|
1139
|
+
visibility: 'query',
|
|
1140
|
+
resultRev: transactionId,
|
|
1141
|
+
})
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
for (const id of updated) {
|
|
1145
|
+
transactionCurrentEvent++
|
|
1146
|
+
const prevDoc = prior[id]!
|
|
1147
|
+
const nextDoc = next[id]!
|
|
1148
|
+
|
|
1149
|
+
mutationEvents.push({
|
|
1150
|
+
type: 'mutation',
|
|
1151
|
+
documentId: id,
|
|
1152
|
+
eventId: `${transactionId}#${id}`,
|
|
1153
|
+
identity: 'example-user',
|
|
1154
|
+
mutations: diffValue(prevDoc, nextDoc).map((patch): Mutation => ({patch: {...patch, id}})),
|
|
1155
|
+
timestamp,
|
|
1156
|
+
transactionCurrentEvent,
|
|
1157
|
+
transactionTotalEvents,
|
|
1158
|
+
transactionId,
|
|
1159
|
+
transition: 'update',
|
|
1160
|
+
visibility: 'query',
|
|
1161
|
+
previousRev: prevDoc._rev,
|
|
1162
|
+
resultRev: transactionId,
|
|
1163
|
+
})
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
for (const id of disappeared) {
|
|
1167
|
+
transactionCurrentEvent++
|
|
1168
|
+
const prevDoc = prior[id]!
|
|
1169
|
+
mutationEvents.push({
|
|
1170
|
+
type: 'mutation',
|
|
1171
|
+
documentId: id,
|
|
1172
|
+
eventId: `${transactionId}#${id}`,
|
|
1173
|
+
identity: 'example-user',
|
|
1174
|
+
mutations: [{delete: {id}}],
|
|
1175
|
+
timestamp,
|
|
1176
|
+
transactionId,
|
|
1177
|
+
transactionCurrentEvent,
|
|
1178
|
+
transactionTotalEvents,
|
|
1179
|
+
transition: 'disappear',
|
|
1180
|
+
visibility: 'query',
|
|
1181
|
+
previousRev: prevDoc._rev,
|
|
1182
|
+
})
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
documents = next
|
|
1186
|
+
|
|
1187
|
+
for (const mutationEvent of mutationEvents) {
|
|
1188
|
+
sharedListener.events.next(mutationEvent)
|
|
1189
|
+
}
|
|
771
1190
|
}
|
|
772
1191
|
|
|
773
1192
|
vi.mocked(createFetchDocument).mockReturnValue(
|
|
@@ -779,6 +1198,33 @@ beforeEach(() => {
|
|
|
779
1198
|
),
|
|
780
1199
|
)
|
|
781
1200
|
|
|
1201
|
+
mutateSubmission.mockImplementation(
|
|
1202
|
+
async (
|
|
1203
|
+
mutations: Mutation[],
|
|
1204
|
+
options: BaseMutationOptions = {},
|
|
1205
|
+
): Promise<MultipleMutationResult> => {
|
|
1206
|
+
const transactionId = options.transactionId ?? crypto.randomUUID()
|
|
1207
|
+
const timestamp = new Date().toISOString()
|
|
1208
|
+
const prior = {...documents}
|
|
1209
|
+
let next = {...documents}
|
|
1210
|
+
next = processMutations({documents: next, mutations, transactionId, timestamp})
|
|
1211
|
+
|
|
1212
|
+
if (!options.dryRun) {
|
|
1213
|
+
emitDatasetChangeEvents(prior, next, transactionId, timestamp)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
1217
|
+
|
|
1218
|
+
return {
|
|
1219
|
+
transactionId,
|
|
1220
|
+
documentIds: Object.entries(next)
|
|
1221
|
+
.filter(([, v]) => !!v)
|
|
1222
|
+
.map(([k]) => k),
|
|
1223
|
+
results: [],
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
)
|
|
1227
|
+
|
|
782
1228
|
const isNonNullable = <T>(t: T): t is NonNullable<T> => !!t
|
|
783
1229
|
|
|
784
1230
|
const fetch = vi.fn(
|
|
@@ -859,8 +1305,8 @@ beforeEach(() => {
|
|
|
859
1305
|
continue
|
|
860
1306
|
}
|
|
861
1307
|
case 'sanity.action.document.edit': {
|
|
862
|
-
const
|
|
863
|
-
if (!
|
|
1308
|
+
const documentSource = (i.draftId && next[i.draftId]) ?? next[i.publishedId]
|
|
1309
|
+
if (!documentSource) {
|
|
864
1310
|
throw new Error(
|
|
865
1311
|
`Could not find a document to edit from \`draftId\` \`${i.draftId}\` or \`publishedId\` ${i.publishedId}`,
|
|
866
1312
|
)
|
|
@@ -869,8 +1315,8 @@ beforeEach(() => {
|
|
|
869
1315
|
next = processMutations({
|
|
870
1316
|
documents: next,
|
|
871
1317
|
mutations: [
|
|
872
|
-
{createIfNotExists: {...
|
|
873
|
-
{patch: {id: i.draftId
|
|
1318
|
+
{createIfNotExists: {...documentSource, _id: i.draftId!}},
|
|
1319
|
+
{patch: {id: i.draftId!, ...i.patch}},
|
|
874
1320
|
],
|
|
875
1321
|
transactionId,
|
|
876
1322
|
timestamp,
|
|
@@ -951,114 +1397,15 @@ beforeEach(() => {
|
|
|
951
1397
|
continue
|
|
952
1398
|
}
|
|
953
1399
|
default: {
|
|
954
|
-
throw new Error(
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
`Unsupported action for mock backend: ${(i as {actionType: string}).actionType}`,
|
|
1402
|
+
)
|
|
955
1403
|
}
|
|
956
1404
|
}
|
|
957
1405
|
}
|
|
958
1406
|
|
|
959
1407
|
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
|
-
}
|
|
1408
|
+
emitDatasetChangeEvents(documents, next, transactionId, timestamp)
|
|
1062
1409
|
}
|
|
1063
1410
|
|
|
1064
1411
|
// add a tick for realism
|
|
@@ -1085,6 +1432,7 @@ beforeEach(() => {
|
|
|
1085
1432
|
action: (...args: Parameters<typeof action>) => from(action(...args)),
|
|
1086
1433
|
fetch: (...args: Parameters<typeof fetch>) => from(fetch(...args)),
|
|
1087
1434
|
request: (...args: Parameters<typeof request>) => from(request(...args)),
|
|
1435
|
+
mutate: (...args: Parameters<typeof mutateSubmission>) => from(mutateSubmission(...args)),
|
|
1088
1436
|
},
|
|
1089
1437
|
} as SanityClient
|
|
1090
1438
|
client$.next(client)
|