@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.1
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/createGroqSearchFilter.d.ts +1147 -0
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
- package/dist/_chunks-es/_internal.js +134 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +93 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_exports/_internal.d.ts +95 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +24 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +1079 -1076
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1967 -2302
- package/dist/index.js.map +1 -1
- package/package.json +48 -47
- package/src/_exports/_internal.ts +26 -0
- package/src/_exports/index.ts +120 -11
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- package/src/agent/agentActions.test.ts +16 -60
- package/src/agent/agentActions.ts +25 -38
- package/src/applications/applications.test-d.ts +83 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +364 -0
- package/src/applications/userApplications.test.ts +126 -0
- package/src/applications/userApplications.ts +261 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authMode.test.ts +25 -0
- package/src/auth/authMode.ts +6 -3
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +59 -26
- package/src/auth/getOrganizationVerificationState.test.ts +18 -17
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +35 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +75 -2
- package/src/auth/logout.ts +23 -4
- package/src/auth/refreshStampedToken.test.ts +219 -469
- package/src/auth/refreshStampedToken.ts +79 -195
- package/src/auth/studioAuth.ts +1 -1
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
- package/src/client/clientStore.test.ts +62 -37
- package/src/client/clientStore.ts +45 -75
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/comlink/controller/actions/destroyController.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
- package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +7 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +109 -79
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +10 -4
- package/src/document/actions.test.ts +115 -33
- package/src/document/actions.ts +212 -15
- package/src/document/applyDocumentActions.test.ts +38 -12
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +673 -278
- package/src/document/documentStore.ts +162 -71
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +82 -26
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +74 -21
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +49 -2
- package/src/document/patchOperations.ts +50 -0
- package/src/document/permissions.test.ts +105 -60
- package/src/document/permissions.ts +44 -19
- package/src/document/processActions/create.ts +138 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +154 -0
- package/src/document/processActions/processActions.ts +218 -0
- package/src/document/processActions/publish.ts +120 -0
- package/src/document/processActions/releaseArchive.ts +77 -0
- package/src/document/processActions/releaseCreate.ts +59 -0
- package/src/document/processActions/releaseDelete.ts +65 -0
- package/src/document/processActions/releaseEdit.ts +37 -0
- package/src/document/processActions/releasePublish.ts +45 -0
- package/src/document/processActions/releaseSchedule.ts +87 -0
- package/src/document/processActions/releaseUtil.ts +31 -0
- package/src/document/processActions/shared.ts +308 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +1129 -121
- package/src/document/reducers.test.ts +639 -64
- package/src/document/reducers.ts +187 -48
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/src/document/sharedListener.test.ts +16 -14
- package/src/document/sharedListener.ts +24 -8
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +68 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +202 -0
- package/src/organization/organization.test-d.ts +36 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +138 -0
- package/src/organizations/organizations.test-d.ts +56 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +107 -0
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +94 -23
- package/src/presence/presenceStore.ts +85 -29
- package/src/preview/getPreviewState.test.ts +137 -0
- package/src/preview/getPreviewState.ts +69 -0
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/resolvePreview.test.ts +64 -0
- package/src/preview/resolvePreview.ts +43 -0
- package/src/preview/types.ts +10 -0
- package/src/project/project.test-d.ts +37 -0
- package/src/project/project.test.ts +157 -12
- package/src/project/project.ts +138 -25
- package/src/projection/getProjectionState.test.ts +18 -20
- package/src/projection/getProjectionState.ts +23 -3
- package/src/projection/projectionQuery.ts +5 -5
- package/src/projection/projectionStore.test.ts +1 -1
- package/src/projection/resolveProjection.test.ts +1 -2
- package/src/projection/resolveProjection.ts +16 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
- package/src/projection/types.ts +8 -0
- package/src/projects/projects.test-d.ts +32 -0
- package/src/projects/projects.test.ts +108 -42
- package/src/projects/projects.ts +63 -27
- package/src/query/queryStore.test.ts +259 -123
- package/src/query/queryStore.ts +78 -63
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +2 -18
- package/src/releases/getPerspectiveState.test.ts +17 -23
- package/src/releases/getPerspectiveState.ts +8 -20
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +100 -53
- package/src/releases/releasesStore.ts +69 -44
- package/src/releases/utils/sortReleases.test.ts +38 -2
- package/src/releases/utils/sortReleases.ts +15 -10
- package/src/store/createActionBinder.test.ts +87 -90
- package/src/store/createActionBinder.ts +79 -25
- package/src/store/createSanityInstance.test.ts +83 -13
- package/src/store/createSanityInstance.ts +77 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +39 -44
- package/src/store/createStoreInstance.test.ts +4 -2
- package/src/store/fetcherStore.test-d.ts +121 -0
- package/src/store/fetcherStore.test.ts +299 -0
- package/src/store/fetcherStore.ts +801 -0
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/events.ts +42 -0
- package/src/telemetry/initTelemetry.test.ts +432 -0
- package/src/telemetry/initTelemetry.ts +218 -0
- package/src/telemetry/telemetryManager.test.ts +327 -0
- package/src/telemetry/telemetryManager.ts +199 -0
- package/src/users/reducers.test.ts +6 -1
- package/src/users/reducers.ts +5 -6
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +16 -12
- package/src/users/usersStore.ts +5 -1
- package/src/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/createFetcherStore.test.ts +7 -5
- 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/includeShape.ts +36 -0
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/logger.test.ts +12 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +37 -0
- package/src/document/processActions.ts +0 -707
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
|
@@ -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,
|
|
@@ -39,7 +41,12 @@ import {
|
|
|
39
41
|
resolvePermissions,
|
|
40
42
|
subscribeDocumentEvents,
|
|
41
43
|
} from './documentStore'
|
|
42
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
type ActionErrorEvent,
|
|
46
|
+
type DocumentRemotePatchesEvent,
|
|
47
|
+
type TransactionRevertedEvent,
|
|
48
|
+
} from './events'
|
|
49
|
+
import {DEFAULT_MAX_BUFFER_SIZE} from './listen'
|
|
43
50
|
import {type DatasetAcl} from './permissions'
|
|
44
51
|
import {type DocumentSet, processMutations} from './processMutations'
|
|
45
52
|
import {type HttpAction} from './reducers'
|
|
@@ -51,23 +58,42 @@ interface TestDocument extends SanityDocument {
|
|
|
51
58
|
title?: string
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
// Scope the TestDocument type to the project/datasets used in tests
|
|
62
|
+
type AllTestSchemaTypes = TestDocument
|
|
63
|
+
|
|
64
|
+
// Augment the 'groq' module
|
|
65
|
+
declare module 'groq' {
|
|
66
|
+
interface SanitySchemas {
|
|
67
|
+
default: AllTestSchemaTypes
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
let instance: SanityInstance
|
|
72
|
+
let instance1: SanityInstance
|
|
73
|
+
let instance2: SanityInstance
|
|
55
74
|
|
|
56
|
-
// bindActionByResource should ensure docs from different resources are isolated
|
|
57
75
|
const resource = {projectId: 'p', dataset: 'd'}
|
|
58
|
-
const
|
|
59
|
-
const
|
|
76
|
+
const source1 = {projectId: 'p', dataset: 'd1'}
|
|
77
|
+
const source2 = {projectId: 'p', dataset: 'd2'}
|
|
60
78
|
|
|
61
79
|
beforeEach(() => {
|
|
62
|
-
instance = createSanityInstance()
|
|
80
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
81
|
+
// test uses two instances that share the same in-memory dataset, but separate
|
|
82
|
+
// store instances. in real scenarios, this would be separate machines but with
|
|
83
|
+
// the same project + dataset
|
|
84
|
+
instance1 = createSanityInstance({projectId: 'p', dataset: 'd1'})
|
|
85
|
+
instance2 = createSanityInstance({projectId: 'p', dataset: 'd2'})
|
|
63
86
|
})
|
|
64
87
|
|
|
65
88
|
afterEach(() => {
|
|
66
89
|
instance?.dispose()
|
|
90
|
+
instance1?.dispose()
|
|
91
|
+
instance2?.dispose()
|
|
67
92
|
})
|
|
68
93
|
|
|
69
94
|
it('creates, edits, and publishes a document', async () => {
|
|
70
|
-
const
|
|
95
|
+
const documentId = DocumentId('doc-single')
|
|
96
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
71
97
|
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
72
98
|
|
|
73
99
|
// Initially the document is undefined
|
|
@@ -80,10 +106,10 @@ it('creates, edits, and publishes a document', async () => {
|
|
|
80
106
|
actions: [createDocument(doc)],
|
|
81
107
|
resource,
|
|
82
108
|
})
|
|
83
|
-
expect(appeared).toContain(getDraftId(
|
|
109
|
+
expect(appeared).toContain(getDraftId(documentId))
|
|
84
110
|
|
|
85
111
|
let currentDoc = documentState.getCurrent()
|
|
86
|
-
expect(currentDoc?._id).toEqual(getDraftId(
|
|
112
|
+
expect(currentDoc?._id).toEqual(getDraftId(documentId))
|
|
87
113
|
|
|
88
114
|
// Edit the document – add a title
|
|
89
115
|
await applyDocumentActions(instance, {
|
|
@@ -101,16 +127,13 @@ it('creates, edits, and publishes a document', async () => {
|
|
|
101
127
|
await submitted()
|
|
102
128
|
currentDoc = documentState.getCurrent()
|
|
103
129
|
|
|
104
|
-
expect(currentDoc).toMatchObject({_id:
|
|
130
|
+
expect(currentDoc).toMatchObject({_id: documentId, _rev: transactionId})
|
|
105
131
|
unsubscribe()
|
|
106
132
|
})
|
|
107
133
|
|
|
108
134
|
it('creates a document with initial values', async () => {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
documentType: 'article',
|
|
112
|
-
resource,
|
|
113
|
-
})
|
|
135
|
+
const documentId = DocumentId('doc-with-initial')
|
|
136
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
114
137
|
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
115
138
|
|
|
116
139
|
expect(documentState.getCurrent()).toBeUndefined()
|
|
@@ -128,10 +151,10 @@ it('creates a document with initial values', async () => {
|
|
|
128
151
|
],
|
|
129
152
|
resource,
|
|
130
153
|
})
|
|
131
|
-
expect(appeared).toContain(getDraftId(
|
|
154
|
+
expect(appeared).toContain(getDraftId(documentId))
|
|
132
155
|
|
|
133
156
|
const currentDoc = documentState.getCurrent()
|
|
134
|
-
expect(currentDoc?._id).toEqual(getDraftId(
|
|
157
|
+
expect(currentDoc?._id).toEqual(getDraftId(documentId))
|
|
135
158
|
expect(currentDoc?.title).toEqual('Article with Initial Values')
|
|
136
159
|
expect(currentDoc?.['author']).toEqual('Jane Doe')
|
|
137
160
|
expect(currentDoc?.['count']).toEqual(42)
|
|
@@ -140,7 +163,8 @@ it('creates a document with initial values', async () => {
|
|
|
140
163
|
})
|
|
141
164
|
|
|
142
165
|
it('edits existing documents', async () => {
|
|
143
|
-
const
|
|
166
|
+
const documentId = DocumentId('existing-doc')
|
|
167
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
144
168
|
const state = getDocumentState<TestDocument>(instance, doc)
|
|
145
169
|
|
|
146
170
|
// not subscribed yet so the value is undefined
|
|
@@ -152,7 +176,7 @@ it('edits existing documents', async () => {
|
|
|
152
176
|
await firstValueFrom(state.observable.pipe(first((i) => !!i)))
|
|
153
177
|
|
|
154
178
|
expect(state.getCurrent()).toMatchObject({
|
|
155
|
-
_id: getDraftId(
|
|
179
|
+
_id: getDraftId(documentId),
|
|
156
180
|
title: 'existing doc',
|
|
157
181
|
})
|
|
158
182
|
|
|
@@ -161,7 +185,7 @@ it('edits existing documents', async () => {
|
|
|
161
185
|
resource,
|
|
162
186
|
})
|
|
163
187
|
expect(state.getCurrent()).toMatchObject({
|
|
164
|
-
_id: getDraftId(
|
|
188
|
+
_id: getDraftId(documentId),
|
|
165
189
|
title: 'updated title',
|
|
166
190
|
})
|
|
167
191
|
|
|
@@ -169,33 +193,31 @@ it('edits existing documents', async () => {
|
|
|
169
193
|
})
|
|
170
194
|
|
|
171
195
|
it('sets optimistic changes synchronously', async () => {
|
|
172
|
-
const doc1 =
|
|
173
|
-
documentId: 'optimistic',
|
|
196
|
+
const doc1 = {
|
|
197
|
+
documentId: DocumentId('optimistic'),
|
|
174
198
|
documentType: 'article',
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
documentId: 'optimistic',
|
|
199
|
+
}
|
|
200
|
+
const doc2 = {
|
|
201
|
+
documentId: DocumentId('optimistic'),
|
|
179
202
|
documentType: 'article',
|
|
180
|
-
|
|
181
|
-
})
|
|
203
|
+
}
|
|
182
204
|
|
|
183
|
-
const state1 = getDocumentState
|
|
184
|
-
const state2 = getDocumentState
|
|
205
|
+
const state1 = getDocumentState(instance1, doc1)
|
|
206
|
+
const state2 = getDocumentState(instance2, doc2)
|
|
185
207
|
|
|
186
208
|
const unsubscribe1 = state1.subscribe()
|
|
187
209
|
const unsubscribe2 = state2.subscribe()
|
|
188
210
|
|
|
189
211
|
// wait until the value is primed in the store
|
|
190
|
-
await resolveDocument(
|
|
212
|
+
await resolveDocument(instance1, doc1)
|
|
191
213
|
|
|
192
214
|
// then the actions are synchronous
|
|
193
215
|
expect(state1.getCurrent()).toBeNull()
|
|
194
|
-
applyDocumentActions(
|
|
216
|
+
applyDocumentActions(instance1, {actions: [createDocument(doc1)], resource: source1})
|
|
195
217
|
expect(state1.getCurrent()).toMatchObject({_id: getDraftId(doc1.documentId)})
|
|
196
|
-
const actionResult1Promise = applyDocumentActions(
|
|
218
|
+
const actionResult1Promise = applyDocumentActions(instance1, {
|
|
197
219
|
actions: [editDocument(doc1, {set: {title: 'initial title'}})],
|
|
198
|
-
resource:
|
|
220
|
+
resource: source1,
|
|
199
221
|
})
|
|
200
222
|
expect(state1.getCurrent()?.title).toBe('initial title')
|
|
201
223
|
|
|
@@ -214,9 +236,9 @@ it('sets optimistic changes synchronously', async () => {
|
|
|
214
236
|
expect(state2.getCurrent()?.title).toBe('initial title')
|
|
215
237
|
|
|
216
238
|
// synchronous for state 2
|
|
217
|
-
const actionResult2Promise = applyDocumentActions(
|
|
239
|
+
const actionResult2Promise = applyDocumentActions(instance2, {
|
|
218
240
|
actions: [editDocument(doc2, {set: {title: 'updated title'}})],
|
|
219
|
-
resource:
|
|
241
|
+
resource: source2,
|
|
220
242
|
})
|
|
221
243
|
expect(state2.getCurrent()?.title).toBe('updated title')
|
|
222
244
|
// async for state 1
|
|
@@ -231,37 +253,27 @@ it('sets optimistic changes synchronously', async () => {
|
|
|
231
253
|
})
|
|
232
254
|
|
|
233
255
|
it('propagates changes between two instances', async () => {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
resource: resource1,
|
|
238
|
-
})
|
|
239
|
-
const doc2 = createDocumentHandle({
|
|
240
|
-
documentId: 'doc-collab',
|
|
241
|
-
documentType: 'article',
|
|
242
|
-
resource: resource2,
|
|
243
|
-
})
|
|
244
|
-
const state1 = getDocumentState<TestDocument>(instance, doc1)
|
|
245
|
-
const state2 = getDocumentState<TestDocument>(instance, doc2)
|
|
256
|
+
const doc = createDocumentHandle({documentId: 'doc-collab', documentType: 'article'})
|
|
257
|
+
const state1 = getDocumentState(instance1, doc)
|
|
258
|
+
const state2 = getDocumentState(instance2, doc)
|
|
246
259
|
|
|
247
260
|
const state1Unsubscribe = state1.subscribe()
|
|
248
261
|
const state2Unsubscribe = state2.subscribe()
|
|
249
262
|
|
|
250
|
-
// Create the document from
|
|
251
|
-
await applyDocumentActions(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}).then((r) => r.submitted())
|
|
263
|
+
// Create the document from instance1.
|
|
264
|
+
await applyDocumentActions(instance1, {actions: [createDocument(doc)], resource: source1}).then(
|
|
265
|
+
(r) => r.submitted(),
|
|
266
|
+
)
|
|
255
267
|
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
expect(
|
|
259
|
-
expect(
|
|
268
|
+
const doc1 = state1.getCurrent()
|
|
269
|
+
const doc2 = state2.getCurrent()
|
|
270
|
+
expect(doc1?._id).toEqual(getDraftId(DocumentId(doc.documentId)))
|
|
271
|
+
expect(doc2?._id).toEqual(getDraftId(DocumentId(doc.documentId)))
|
|
260
272
|
|
|
261
|
-
// Now, edit the document from
|
|
262
|
-
await applyDocumentActions(
|
|
263
|
-
actions: [editDocument(
|
|
264
|
-
resource:
|
|
273
|
+
// Now, edit the document from instance2.
|
|
274
|
+
await applyDocumentActions(instance2, {
|
|
275
|
+
actions: [editDocument(doc, {set: {title: 'Hello world!'}})],
|
|
276
|
+
resource: source2,
|
|
265
277
|
}).then((r) => r.submitted())
|
|
266
278
|
|
|
267
279
|
const updated1 = state1.getCurrent()
|
|
@@ -273,44 +285,95 @@ it('propagates changes between two instances', async () => {
|
|
|
273
285
|
state2Unsubscribe()
|
|
274
286
|
})
|
|
275
287
|
|
|
276
|
-
it('
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
288
|
+
it('emits remote-patches events distinguishing own and foreign transactions', async () => {
|
|
289
|
+
const doc = createDocumentHandle({documentId: 'doc-remote-patches', documentType: 'article'})
|
|
290
|
+
const state1 = getDocumentState(instance1, doc)
|
|
291
|
+
const state2 = getDocumentState(instance2, doc)
|
|
292
|
+
|
|
293
|
+
const state1Unsubscribe = state1.subscribe()
|
|
294
|
+
const state2Unsubscribe = state2.subscribe()
|
|
295
|
+
|
|
296
|
+
await applyDocumentActions(instance1, {actions: [createDocument(doc)], resource: source1}).then(
|
|
297
|
+
(r) => r.submitted(),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
const events1: DocumentRemotePatchesEvent[] = []
|
|
301
|
+
const events2: DocumentRemotePatchesEvent[] = []
|
|
302
|
+
const unsubscribeEvents1 = subscribeDocumentEvents(instance1, {
|
|
303
|
+
resource: source1,
|
|
304
|
+
eventHandler: (e) => {
|
|
305
|
+
if (e.type === 'remote-patches') events1.push(e)
|
|
306
|
+
},
|
|
281
307
|
})
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
308
|
+
const unsubscribeEvents2 = subscribeDocumentEvents(instance2, {
|
|
309
|
+
resource: source2,
|
|
310
|
+
eventHandler: (e) => {
|
|
311
|
+
if (e.type === 'remote-patches') events2.push(e)
|
|
312
|
+
},
|
|
286
313
|
})
|
|
287
|
-
|
|
288
|
-
|
|
314
|
+
|
|
315
|
+
// edit from instance2: an own transaction for instance2, a foreign one for instance1
|
|
316
|
+
const result = await applyDocumentActions(instance2, {
|
|
317
|
+
actions: [editDocument(doc, {set: {title: 'Hello world!'}})],
|
|
318
|
+
resource: source2,
|
|
319
|
+
}).then((r) => r.submitted())
|
|
320
|
+
|
|
321
|
+
const draftId = getDraftId(DocumentId(doc.documentId))
|
|
322
|
+
|
|
323
|
+
expect(events1).toHaveLength(1)
|
|
324
|
+
expect(events1[0]).toMatchObject({
|
|
325
|
+
type: 'remote-patches',
|
|
326
|
+
documentId: draftId,
|
|
327
|
+
transactionId: result.transactionId,
|
|
328
|
+
origin: 'remote',
|
|
329
|
+
})
|
|
330
|
+
expect(events1[0].patches).toEqual(
|
|
331
|
+
expect.arrayContaining([{set: expect.objectContaining({title: 'Hello world!'})}]),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
expect(events2).toHaveLength(1)
|
|
335
|
+
expect(events2[0]).toMatchObject({
|
|
336
|
+
type: 'remote-patches',
|
|
337
|
+
documentId: draftId,
|
|
338
|
+
transactionId: result.transactionId,
|
|
339
|
+
origin: 'local',
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
unsubscribeEvents1()
|
|
343
|
+
unsubscribeEvents2()
|
|
344
|
+
state1Unsubscribe()
|
|
345
|
+
state2Unsubscribe()
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('handles concurrent edits and resolves conflicts', async () => {
|
|
349
|
+
const doc = createDocumentHandle({documentId: 'doc-concurrent', documentType: 'article'})
|
|
350
|
+
const state1 = getDocumentState(instance1, doc)
|
|
351
|
+
const state2 = getDocumentState(instance2, doc)
|
|
289
352
|
|
|
290
353
|
const state1Unsubscribe = state1.subscribe()
|
|
291
354
|
const state2Unsubscribe = state2.subscribe()
|
|
292
355
|
|
|
293
|
-
const oneOffInstance = createSanityInstance()
|
|
356
|
+
const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
294
357
|
|
|
295
358
|
// Create the initial document from a one-off instance.
|
|
296
359
|
await applyDocumentActions(oneOffInstance, {
|
|
297
360
|
actions: [
|
|
298
|
-
createDocument(
|
|
299
|
-
editDocument(
|
|
361
|
+
createDocument(doc),
|
|
362
|
+
editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy dog'}}),
|
|
300
363
|
],
|
|
301
364
|
resource,
|
|
302
365
|
}).then((res) => res.submitted())
|
|
303
366
|
|
|
304
|
-
// Both
|
|
305
|
-
const p1 = applyDocumentActions(
|
|
306
|
-
actions: [editDocument(
|
|
307
|
-
resource:
|
|
367
|
+
// Both instances now issue an edit simultaneously.
|
|
368
|
+
const p1 = applyDocumentActions(instance1, {
|
|
369
|
+
actions: [editDocument(doc, {set: {title: 'The quick brown fox jumps over the lazy cat'}})],
|
|
370
|
+
resource: source1,
|
|
308
371
|
}).then((r) => r.submitted())
|
|
309
|
-
const p2 = applyDocumentActions(
|
|
372
|
+
const p2 = applyDocumentActions(instance2, {
|
|
310
373
|
actions: [
|
|
311
|
-
editDocument(
|
|
374
|
+
editDocument(doc, {set: {title: 'The quick brown elephant jumps over the lazy dog'}}),
|
|
312
375
|
],
|
|
313
|
-
resource:
|
|
376
|
+
resource: source2,
|
|
314
377
|
}).then((r) => r.submitted())
|
|
315
378
|
|
|
316
379
|
// Wait for both actions to complete (or reject).
|
|
@@ -326,8 +389,76 @@ it('handles concurrent edits and resolves conflicts', async () => {
|
|
|
326
389
|
oneOffInstance.dispose()
|
|
327
390
|
})
|
|
328
391
|
|
|
392
|
+
it('interleaves concurrent keyed-array edits when operations are preserved', async () => {
|
|
393
|
+
const doc = createDocumentHandle({documentId: 'doc-preserve-ops', documentType: 'article'})
|
|
394
|
+
const state1 = getDocumentState(instance1, doc)
|
|
395
|
+
const state2 = getDocumentState(instance2, doc)
|
|
396
|
+
|
|
397
|
+
const state1Unsubscribe = state1.subscribe()
|
|
398
|
+
const state2Unsubscribe = state2.subscribe()
|
|
399
|
+
|
|
400
|
+
const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
401
|
+
await applyDocumentActions(oneOffInstance, {
|
|
402
|
+
actions: [
|
|
403
|
+
createDocument(doc),
|
|
404
|
+
editDocument(doc, {
|
|
405
|
+
set: {
|
|
406
|
+
items: [
|
|
407
|
+
{_key: 'a', value: 'first'},
|
|
408
|
+
{_key: 'b', value: 'second'},
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
}),
|
|
412
|
+
],
|
|
413
|
+
resource,
|
|
414
|
+
}).then((r) => r.submitted())
|
|
415
|
+
|
|
416
|
+
// both instances insert into the same keyed array simultaneously, each
|
|
417
|
+
// preserving their operational patches instead of re-diffing snapshots
|
|
418
|
+
const p1 = applyDocumentActions(instance1, {
|
|
419
|
+
actions: [
|
|
420
|
+
editDocument(
|
|
421
|
+
doc,
|
|
422
|
+
{insert: {after: 'items[_key=="a"]', items: [{_key: 'c', value: 'from instance1'}]}},
|
|
423
|
+
{preserveOperations: true},
|
|
424
|
+
),
|
|
425
|
+
],
|
|
426
|
+
resource: source1,
|
|
427
|
+
}).then((r) => r.submitted())
|
|
428
|
+
const p2 = applyDocumentActions(instance2, {
|
|
429
|
+
actions: [
|
|
430
|
+
editDocument(
|
|
431
|
+
doc,
|
|
432
|
+
{insert: {after: 'items[_key=="b"]', items: [{_key: 'd', value: 'from instance2'}]}},
|
|
433
|
+
{preserveOperations: true},
|
|
434
|
+
),
|
|
435
|
+
],
|
|
436
|
+
resource: source2,
|
|
437
|
+
}).then((r) => r.submitted())
|
|
438
|
+
|
|
439
|
+
await Promise.allSettled([p1, p2])
|
|
440
|
+
|
|
441
|
+
const finalDoc1 = state1.getCurrent()
|
|
442
|
+
const finalDoc2 = state2.getCurrent()
|
|
443
|
+
|
|
444
|
+
// neither edit overwrote the other: both keyed inserts landed and both
|
|
445
|
+
// instances converged on the same interleaved result
|
|
446
|
+
expect(finalDoc1?.['items']).toEqual(finalDoc2?.['items'])
|
|
447
|
+
expect(finalDoc1?.['items']).toEqual([
|
|
448
|
+
{_key: 'a', value: 'first'},
|
|
449
|
+
{_key: 'c', value: 'from instance1'},
|
|
450
|
+
{_key: 'b', value: 'second'},
|
|
451
|
+
{_key: 'd', value: 'from instance2'},
|
|
452
|
+
])
|
|
453
|
+
|
|
454
|
+
state1Unsubscribe()
|
|
455
|
+
state2Unsubscribe()
|
|
456
|
+
oneOffInstance.dispose()
|
|
457
|
+
})
|
|
458
|
+
|
|
329
459
|
it('unpublishes and discards a document', async () => {
|
|
330
|
-
const
|
|
460
|
+
const documentId = DocumentId('doc-pub-unpub')
|
|
461
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
331
462
|
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
332
463
|
const unsubscribe = documentState.subscribe()
|
|
333
464
|
|
|
@@ -339,7 +470,7 @@ it('unpublishes and discards a document', async () => {
|
|
|
339
470
|
})
|
|
340
471
|
const publishedDoc = documentState.getCurrent()
|
|
341
472
|
expect(publishedDoc).toMatchObject({
|
|
342
|
-
_id: getPublishedId(
|
|
473
|
+
_id: getPublishedId(documentId),
|
|
343
474
|
_rev: afterPublish.transactionId,
|
|
344
475
|
})
|
|
345
476
|
|
|
@@ -347,7 +478,7 @@ it('unpublishes and discards a document', async () => {
|
|
|
347
478
|
await applyDocumentActions(instance, {actions: [unpublishDocument(doc)], resource})
|
|
348
479
|
const afterUnpublish = documentState.getCurrent()
|
|
349
480
|
// In our mock implementation the _id remains the same but the published copy is removed.
|
|
350
|
-
expect(afterUnpublish?._id).toEqual(getDraftId(
|
|
481
|
+
expect(afterUnpublish?._id).toEqual(getDraftId(documentId))
|
|
351
482
|
|
|
352
483
|
// Discard the draft (which deletes the draft version).
|
|
353
484
|
await applyDocumentActions(instance, {actions: [discardDocument(doc)], resource})
|
|
@@ -358,9 +489,10 @@ it('unpublishes and discards a document', async () => {
|
|
|
358
489
|
})
|
|
359
490
|
|
|
360
491
|
it('deletes a document', async () => {
|
|
361
|
-
const
|
|
492
|
+
const documentId = DocumentId('doc-delete')
|
|
493
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
362
494
|
|
|
363
|
-
const documentState = getDocumentState
|
|
495
|
+
const documentState = getDocumentState(instance, doc)
|
|
364
496
|
const unsubscribe = documentState.subscribe()
|
|
365
497
|
|
|
366
498
|
await applyDocumentActions(instance, {
|
|
@@ -379,7 +511,8 @@ it('deletes a document', async () => {
|
|
|
379
511
|
})
|
|
380
512
|
|
|
381
513
|
it('cleans up document state when there are no subscribers', async () => {
|
|
382
|
-
const
|
|
514
|
+
const documentId = DocumentId('doc-cleanup')
|
|
515
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
383
516
|
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
384
517
|
|
|
385
518
|
// Subscribe to the document state.
|
|
@@ -397,14 +530,14 @@ it('cleans up document state when there are no subscribers', async () => {
|
|
|
397
530
|
await new Promise((resolve) => setTimeout(resolve, 30))
|
|
398
531
|
|
|
399
532
|
// When a new subscriber is created, if the state was cleared it should return undefined.
|
|
400
|
-
const newDocumentState = getDocumentState
|
|
533
|
+
const newDocumentState = getDocumentState(instance, doc)
|
|
401
534
|
expect(newDocumentState.getCurrent()).toBeUndefined()
|
|
402
535
|
})
|
|
403
536
|
|
|
404
537
|
it('fetches documents if there are no active subscriptions for the actions applied', async () => {
|
|
405
|
-
const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article'
|
|
538
|
+
const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article'})
|
|
406
539
|
|
|
407
|
-
const {getCurrent} = getDocumentState
|
|
540
|
+
const {getCurrent} = getDocumentState(instance, doc)
|
|
408
541
|
expect(getCurrent()).toBeUndefined()
|
|
409
542
|
expect(getDocumentSyncStatus(instance, doc).getCurrent()).toBeUndefined()
|
|
410
543
|
|
|
@@ -457,13 +590,9 @@ it('fetches documents if there are no active subscriptions for the actions appli
|
|
|
457
590
|
})
|
|
458
591
|
|
|
459
592
|
it('batches edit transaction into one outgoing transaction', async () => {
|
|
460
|
-
const doc = createDocumentHandle({
|
|
461
|
-
documentId: crypto.randomUUID(),
|
|
462
|
-
documentType: 'article',
|
|
463
|
-
resource,
|
|
464
|
-
})
|
|
593
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
465
594
|
|
|
466
|
-
const unsubscribe = getDocumentState
|
|
595
|
+
const unsubscribe = getDocumentState(instance, doc).subscribe()
|
|
467
596
|
|
|
468
597
|
// this creates its own transaction
|
|
469
598
|
applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
|
|
@@ -490,12 +619,54 @@ it('batches edit transaction into one outgoing transaction', async () => {
|
|
|
490
619
|
unsubscribe()
|
|
491
620
|
})
|
|
492
621
|
|
|
493
|
-
it('
|
|
494
|
-
const
|
|
495
|
-
documentId:
|
|
622
|
+
it('submits liveEdit document edits through observable.mutate', async () => {
|
|
623
|
+
const liveDoc = createDocumentHandle({
|
|
624
|
+
documentId: 'live-edit-test-doc',
|
|
496
625
|
documentType: 'article',
|
|
626
|
+
liveEdit: true,
|
|
627
|
+
})
|
|
628
|
+
const state = getDocumentState(instance, liveDoc)
|
|
629
|
+
const unsubscribe = state.subscribe()
|
|
630
|
+
|
|
631
|
+
await firstValueFrom(state.observable.pipe(first((doc) => doc !== undefined)))
|
|
632
|
+
expect(state.getCurrent()).toMatchObject({
|
|
633
|
+
_id: 'live-edit-test-doc',
|
|
634
|
+
title: 'live initial',
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
const callCountBefore = mutateSubmission.mock.calls.length
|
|
638
|
+
|
|
639
|
+
const result = await applyDocumentActions(instance, {
|
|
640
|
+
actions: [editDocument(liveDoc, {set: {title: 'patched via mutate'}})],
|
|
497
641
|
resource,
|
|
498
642
|
})
|
|
643
|
+
await result.submitted()
|
|
644
|
+
|
|
645
|
+
expect(mutateSubmission.mock.calls.length).toBe(callCountBefore + 1)
|
|
646
|
+
const [mutationList, mutateOptions] = mutateSubmission.mock.calls[callCountBefore]!
|
|
647
|
+
expect(mutateOptions).toMatchObject({
|
|
648
|
+
transactionId: result.transactionId,
|
|
649
|
+
visibility: 'async',
|
|
650
|
+
returnDocuments: false,
|
|
651
|
+
returnFirst: false,
|
|
652
|
+
tag: 'document.mutate',
|
|
653
|
+
skipCrossDatasetReferenceValidation: true,
|
|
654
|
+
})
|
|
655
|
+
expect(mutationList.length).toBeGreaterThan(0)
|
|
656
|
+
expect(
|
|
657
|
+
mutationList.every(
|
|
658
|
+
(m) => 'patch' in m && (m as {patch: {id: string}}).patch.id === liveDoc.documentId,
|
|
659
|
+
),
|
|
660
|
+
).toBe(true)
|
|
661
|
+
|
|
662
|
+
expect(state.getCurrent()?.title).toBe('patched via mutate')
|
|
663
|
+
expect(client.action).not.toHaveBeenCalled()
|
|
664
|
+
|
|
665
|
+
unsubscribe()
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
it('provides the consistency status via `getDocumentSyncStatus`', async () => {
|
|
669
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
499
670
|
|
|
500
671
|
const syncStatus = getDocumentSyncStatus(instance, doc)
|
|
501
672
|
expect(syncStatus.getCurrent()).toBeUndefined()
|
|
@@ -550,13 +721,10 @@ it('reverts failed outgoing transaction locally', async () => {
|
|
|
550
721
|
})
|
|
551
722
|
})
|
|
552
723
|
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
documentType: 'article',
|
|
556
|
-
resource,
|
|
557
|
-
})
|
|
724
|
+
const documentId = DocumentId(crypto.randomUUID())
|
|
725
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
558
726
|
|
|
559
|
-
const {getCurrent, subscribe} = getDocumentState
|
|
727
|
+
const {getCurrent, subscribe} = getDocumentState(instance, doc)
|
|
560
728
|
const unsubscribe = subscribe()
|
|
561
729
|
|
|
562
730
|
await applyDocumentActions(instance, {actions: [createDocument(doc)], resource})
|
|
@@ -621,12 +789,8 @@ it('removes a queued transaction if it fails to apply', async () => {
|
|
|
621
789
|
})
|
|
622
790
|
})
|
|
623
791
|
|
|
624
|
-
const doc = createDocumentHandle({
|
|
625
|
-
|
|
626
|
-
documentType: 'article',
|
|
627
|
-
resource,
|
|
628
|
-
})
|
|
629
|
-
const state = getDocumentState<TestDocument>(instance, doc)
|
|
792
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
793
|
+
const state = getDocumentState(instance, doc)
|
|
630
794
|
const unsubscribe = state.subscribe()
|
|
631
795
|
|
|
632
796
|
await expect(
|
|
@@ -664,9 +828,8 @@ it('returns allowed true when no permission errors occur', async () => {
|
|
|
664
828
|
const doc = createDocumentHandle({
|
|
665
829
|
documentId: 'doc-perm-allowed',
|
|
666
830
|
documentType: 'article',
|
|
667
|
-
resource,
|
|
668
831
|
})
|
|
669
|
-
const state = getDocumentState
|
|
832
|
+
const state = getDocumentState(instance, doc)
|
|
670
833
|
const unsubscribe = state.subscribe()
|
|
671
834
|
await applyDocumentActions(instance, {actions: [createDocument(doc)], resource}).then((r) =>
|
|
672
835
|
r.submitted(),
|
|
@@ -674,7 +837,6 @@ it('returns allowed true when no permission errors occur', async () => {
|
|
|
674
837
|
|
|
675
838
|
// Use an action that includes a patch (so that update permission check is bypassed).
|
|
676
839
|
const permissionsState = getPermissionsState(instance, {
|
|
677
|
-
resource,
|
|
678
840
|
actions: [
|
|
679
841
|
{
|
|
680
842
|
...doc,
|
|
@@ -691,11 +853,7 @@ it('returns allowed true when no permission errors occur', async () => {
|
|
|
691
853
|
})
|
|
692
854
|
|
|
693
855
|
it("should reject applying the action if a precondition isn't met", async () => {
|
|
694
|
-
const doc = createDocumentHandle({
|
|
695
|
-
documentId: 'does-not-exist',
|
|
696
|
-
documentType: 'article',
|
|
697
|
-
resource,
|
|
698
|
-
})
|
|
856
|
+
const doc = createDocumentHandle({documentId: 'does-not-exist', documentType: 'article'})
|
|
699
857
|
|
|
700
858
|
await expect(
|
|
701
859
|
applyDocumentActions(instance, {actions: [deleteDocument(doc)], resource}),
|
|
@@ -703,11 +861,7 @@ it("should reject applying the action if a precondition isn't met", async () =>
|
|
|
703
861
|
})
|
|
704
862
|
|
|
705
863
|
it("should reject applying the action if a permission isn't met", async () => {
|
|
706
|
-
const doc = createDocumentHandle({
|
|
707
|
-
documentId: 'does-not-exist',
|
|
708
|
-
documentType: 'article',
|
|
709
|
-
resource,
|
|
710
|
-
})
|
|
864
|
+
const doc = createDocumentHandle({documentId: 'does-not-exist', documentType: 'article'})
|
|
711
865
|
|
|
712
866
|
const datasetAcl = [{filter: 'false', permissions: ['create']}]
|
|
713
867
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
@@ -721,12 +875,8 @@ it('returns allowed false with reasons when permission errors occur', async () =
|
|
|
721
875
|
const datasetAcl = [{filter: 'false', permissions: ['create']}]
|
|
722
876
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
723
877
|
|
|
724
|
-
const doc = createDocumentHandle({
|
|
725
|
-
|
|
726
|
-
documentType: 'article',
|
|
727
|
-
resource,
|
|
728
|
-
})
|
|
729
|
-
const result = await resolvePermissions(instance, {actions: [createDocument(doc)], resource})
|
|
878
|
+
const doc = createDocumentHandle({documentId: 'doc-perm-denied', documentType: 'article'})
|
|
879
|
+
const result = await resolvePermissions(instance, {actions: [createDocument(doc)]})
|
|
730
880
|
|
|
731
881
|
const message = 'You do not have permission to create a draft for document "doc-perm-denied".'
|
|
732
882
|
expect(result).toMatchObject({
|
|
@@ -744,44 +894,47 @@ it('fetches dataset ACL and updates grants in the document store state', async (
|
|
|
744
894
|
]
|
|
745
895
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
746
896
|
|
|
747
|
-
const book = createDocumentHandle({
|
|
748
|
-
|
|
749
|
-
documentType: 'book',
|
|
750
|
-
resource,
|
|
751
|
-
})
|
|
752
|
-
const author = createDocumentHandle({
|
|
753
|
-
documentId: crypto.randomUUID(),
|
|
754
|
-
documentType: 'author',
|
|
755
|
-
resource,
|
|
756
|
-
})
|
|
897
|
+
const book = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'book'})
|
|
898
|
+
const author = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'author'})
|
|
757
899
|
|
|
758
|
-
expect(await resolvePermissions(instance, {actions: [createDocument(book)]
|
|
900
|
+
expect(await resolvePermissions(instance, {actions: [createDocument(book)]})).toEqual({
|
|
759
901
|
allowed: true,
|
|
760
902
|
})
|
|
761
|
-
expect(
|
|
762
|
-
await resolvePermissions(instance, {actions: [createDocument(author)], resource}),
|
|
763
|
-
).toMatchObject({
|
|
903
|
+
expect(await resolvePermissions(instance, {actions: [createDocument(author)]})).toMatchObject({
|
|
764
904
|
allowed: false,
|
|
765
905
|
message: expect.stringContaining('You do not have permission to create a draft for document'),
|
|
766
906
|
})
|
|
767
907
|
})
|
|
768
908
|
|
|
909
|
+
it('does not send credentials with the dataset ACL request', async () => {
|
|
910
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
911
|
+
await resolvePermissions(instance, {actions: [createDocument(doc)]})
|
|
912
|
+
|
|
913
|
+
const aclCall = vi
|
|
914
|
+
.mocked(client.request)
|
|
915
|
+
.mock.calls.find(([options]) => options.uri?.endsWith('/acl'))
|
|
916
|
+
expect(aclCall).toBeDefined()
|
|
917
|
+
expect(aclCall![0]).not.toHaveProperty('withCredentials')
|
|
918
|
+
})
|
|
919
|
+
|
|
769
920
|
it('fetches ACL for MediaLibraryResource', async () => {
|
|
770
|
-
const mediaLibraryInstance = createSanityInstance(
|
|
921
|
+
const mediaLibraryInstance = createSanityInstance({
|
|
922
|
+
projectId: 'p',
|
|
923
|
+
dataset: 'd',
|
|
924
|
+
resources: {
|
|
925
|
+
'media-library': {mediaLibraryId: 'test-media-library'},
|
|
926
|
+
},
|
|
927
|
+
})
|
|
771
928
|
|
|
772
929
|
const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
|
|
773
930
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
774
931
|
|
|
775
|
-
const doc = createDocumentHandle({
|
|
776
|
-
|
|
777
|
-
documentType: 'article',
|
|
778
|
-
resource,
|
|
779
|
-
})
|
|
780
|
-
const mediaLibraryResource = {mediaLibraryId: 'test-media-library'}
|
|
932
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
933
|
+
const mediaLibrarySource = {mediaLibraryId: 'test-media-library'}
|
|
781
934
|
|
|
782
935
|
const result = await resolvePermissions(mediaLibraryInstance, {
|
|
783
936
|
actions: [createDocument(doc)],
|
|
784
|
-
resource:
|
|
937
|
+
resource: mediaLibrarySource,
|
|
785
938
|
})
|
|
786
939
|
|
|
787
940
|
expect(result).toEqual({allowed: true})
|
|
@@ -789,21 +942,23 @@ it('fetches ACL for MediaLibraryResource', async () => {
|
|
|
789
942
|
})
|
|
790
943
|
|
|
791
944
|
it('fetches ACL for CanvasResource', async () => {
|
|
792
|
-
const canvasInstance = createSanityInstance(
|
|
945
|
+
const canvasInstance = createSanityInstance({
|
|
946
|
+
projectId: 'p',
|
|
947
|
+
dataset: 'd',
|
|
948
|
+
resources: {
|
|
949
|
+
canvas: {canvasId: 'test-canvas'},
|
|
950
|
+
},
|
|
951
|
+
})
|
|
793
952
|
|
|
794
953
|
const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
|
|
795
954
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
796
955
|
|
|
797
|
-
const doc = createDocumentHandle({
|
|
798
|
-
|
|
799
|
-
documentType: 'article',
|
|
800
|
-
resource,
|
|
801
|
-
})
|
|
802
|
-
const canvasResource = {canvasId: 'test-canvas'}
|
|
956
|
+
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
|
|
957
|
+
const canvasSource = {canvasId: 'test-canvas'}
|
|
803
958
|
|
|
804
959
|
const result = await resolvePermissions(canvasInstance, {
|
|
805
960
|
actions: [createDocument(doc)],
|
|
806
|
-
resource:
|
|
961
|
+
resource: canvasSource,
|
|
807
962
|
})
|
|
808
963
|
|
|
809
964
|
expect(result).toEqual({allowed: true})
|
|
@@ -811,16 +966,13 @@ it('fetches ACL for CanvasResource', async () => {
|
|
|
811
966
|
})
|
|
812
967
|
|
|
813
968
|
it('returns a promise that resolves when a document has been loaded in the store (useful for suspense)', async () => {
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
documentType: 'article',
|
|
817
|
-
resource,
|
|
818
|
-
})
|
|
969
|
+
const documentId = DocumentId(crypto.randomUUID())
|
|
970
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
819
971
|
|
|
820
972
|
expect(await resolveDocument(instance, doc)).toBe(null)
|
|
821
973
|
|
|
822
974
|
// use one-off instance to create the document in the mock backend
|
|
823
|
-
const oneOffInstance = createSanityInstance()
|
|
975
|
+
const oneOffInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
824
976
|
const result = await applyDocumentActions(oneOffInstance, {
|
|
825
977
|
actions: [createDocument(doc), editDocument(doc, {set: {title: 'initial title'}})],
|
|
826
978
|
resource,
|
|
@@ -828,7 +980,7 @@ it('returns a promise that resolves when a document has been loaded in the store
|
|
|
828
980
|
await result.submitted() // wait till submitted to server before resolving
|
|
829
981
|
|
|
830
982
|
await expect(resolveDocument(instance, doc)).resolves.toMatchObject({
|
|
831
|
-
_id: getDraftId(
|
|
983
|
+
_id: getDraftId(documentId),
|
|
832
984
|
_type: 'article',
|
|
833
985
|
title: 'initial title',
|
|
834
986
|
})
|
|
@@ -839,8 +991,8 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
839
991
|
const handler = vi.fn()
|
|
840
992
|
const unsubscribe = subscribeDocumentEvents(instance, {resource, eventHandler: handler})
|
|
841
993
|
|
|
842
|
-
const documentId = crypto.randomUUID()
|
|
843
|
-
const doc = createDocumentHandle({documentId, documentType: 'article'
|
|
994
|
+
const documentId = DocumentId(crypto.randomUUID())
|
|
995
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
844
996
|
expect(handler).toHaveBeenCalledTimes(0)
|
|
845
997
|
|
|
846
998
|
const tnx1 = await applyDocumentActions(instance, {
|
|
@@ -881,6 +1033,188 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
881
1033
|
unsubscribe()
|
|
882
1034
|
})
|
|
883
1035
|
|
|
1036
|
+
it('creates and edits a version document with a release perspective', async () => {
|
|
1037
|
+
const documentId = DocumentId('doc-release')
|
|
1038
|
+
const releaseName = 'test-release'
|
|
1039
|
+
const doc = createDocumentHandle({
|
|
1040
|
+
documentId,
|
|
1041
|
+
documentType: 'article',
|
|
1042
|
+
perspective: {releaseName},
|
|
1043
|
+
})
|
|
1044
|
+
const versionId = `versions.${releaseName}.${documentId}`
|
|
1045
|
+
|
|
1046
|
+
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
1047
|
+
expect(documentState.getCurrent()).toBeUndefined()
|
|
1048
|
+
|
|
1049
|
+
const unsubscribe = documentState.subscribe()
|
|
1050
|
+
|
|
1051
|
+
// Create a version document for the release
|
|
1052
|
+
const {appeared} = await applyDocumentActions(instance, {
|
|
1053
|
+
actions: [createDocument(doc)],
|
|
1054
|
+
})
|
|
1055
|
+
expect(appeared).toContain(versionId)
|
|
1056
|
+
|
|
1057
|
+
let currentDoc = documentState.getCurrent()
|
|
1058
|
+
expect(currentDoc?._id).toEqual(versionId)
|
|
1059
|
+
expect(currentDoc?._type).toEqual('article')
|
|
1060
|
+
|
|
1061
|
+
// Edit the version document
|
|
1062
|
+
await applyDocumentActions(instance, {
|
|
1063
|
+
actions: [editDocument(doc, {set: {title: 'Release Version Title'}})],
|
|
1064
|
+
})
|
|
1065
|
+
currentDoc = documentState.getCurrent()
|
|
1066
|
+
expect(currentDoc?.title).toEqual('Release Version Title')
|
|
1067
|
+
expect(currentDoc?._id).toEqual(versionId)
|
|
1068
|
+
|
|
1069
|
+
unsubscribe()
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
it('creates a version document with initial values and then discards it', async () => {
|
|
1073
|
+
const documentId = DocumentId('doc-release-discard')
|
|
1074
|
+
const releaseName = 'test-release-discard'
|
|
1075
|
+
const doc = createDocumentHandle({
|
|
1076
|
+
documentId,
|
|
1077
|
+
documentType: 'article',
|
|
1078
|
+
perspective: {releaseName},
|
|
1079
|
+
})
|
|
1080
|
+
const versionId = `versions.${releaseName}.${documentId}`
|
|
1081
|
+
|
|
1082
|
+
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
1083
|
+
const unsubscribe = documentState.subscribe()
|
|
1084
|
+
|
|
1085
|
+
// Create a version document with initial values
|
|
1086
|
+
await applyDocumentActions(instance, {
|
|
1087
|
+
actions: [createDocument(doc, {title: 'Initial Release Title'})],
|
|
1088
|
+
})
|
|
1089
|
+
|
|
1090
|
+
let currentDoc = documentState.getCurrent()
|
|
1091
|
+
expect(currentDoc?._id).toEqual(versionId)
|
|
1092
|
+
expect(currentDoc?.title).toEqual('Initial Release Title')
|
|
1093
|
+
|
|
1094
|
+
// Discard the version document
|
|
1095
|
+
const {disappeared} = await applyDocumentActions(instance, {
|
|
1096
|
+
actions: [discardDocument(doc)],
|
|
1097
|
+
})
|
|
1098
|
+
expect(disappeared).toContain(versionId)
|
|
1099
|
+
|
|
1100
|
+
currentDoc = documentState.getCurrent()
|
|
1101
|
+
expect(currentDoc).toBeNull()
|
|
1102
|
+
|
|
1103
|
+
unsubscribe()
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
it('version edits are isolated from draft state', async () => {
|
|
1107
|
+
const documentId = DocumentId('doc-version-isolation')
|
|
1108
|
+
const releaseName = 'isolation-release'
|
|
1109
|
+
const versionDoc = createDocumentHandle({
|
|
1110
|
+
documentId,
|
|
1111
|
+
documentType: 'article',
|
|
1112
|
+
perspective: {releaseName},
|
|
1113
|
+
})
|
|
1114
|
+
const draftDoc = createDocumentHandle({documentId, documentType: 'article'})
|
|
1115
|
+
const versionId = `versions.${releaseName}.${documentId}`
|
|
1116
|
+
|
|
1117
|
+
const versionState = getDocumentState<TestDocument>(instance, versionDoc)
|
|
1118
|
+
const draftState = getDocumentState<TestDocument>(instance, draftDoc)
|
|
1119
|
+
|
|
1120
|
+
const unsubscribeVersion = versionState.subscribe()
|
|
1121
|
+
const unsubscribeDraft = draftState.subscribe()
|
|
1122
|
+
|
|
1123
|
+
// Create draft and version documents
|
|
1124
|
+
await applyDocumentActions(instance, {
|
|
1125
|
+
actions: [createDocument(draftDoc)],
|
|
1126
|
+
})
|
|
1127
|
+
await applyDocumentActions(instance, {
|
|
1128
|
+
actions: [editDocument(draftDoc, {set: {title: 'Draft Title'}})],
|
|
1129
|
+
})
|
|
1130
|
+
await applyDocumentActions(instance, {
|
|
1131
|
+
actions: [createDocument(versionDoc, {title: 'Release Title'})],
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
// Version perspective shows the version doc
|
|
1135
|
+
expect(versionState.getCurrent()?._id).toEqual(versionId)
|
|
1136
|
+
expect(versionState.getCurrent()?.title).toEqual('Release Title')
|
|
1137
|
+
|
|
1138
|
+
// Draft state shows the draft doc
|
|
1139
|
+
expect(draftState.getCurrent()?.title).toEqual('Draft Title')
|
|
1140
|
+
|
|
1141
|
+
// Editing the version doc should not affect the draft
|
|
1142
|
+
await applyDocumentActions(instance, {
|
|
1143
|
+
actions: [editDocument(versionDoc, {set: {title: 'Updated Release Title'}})],
|
|
1144
|
+
})
|
|
1145
|
+
expect(versionState.getCurrent()?.title).toEqual('Updated Release Title')
|
|
1146
|
+
expect(draftState.getCurrent()?.title).toEqual('Draft Title')
|
|
1147
|
+
|
|
1148
|
+
unsubscribeVersion()
|
|
1149
|
+
unsubscribeDraft()
|
|
1150
|
+
})
|
|
1151
|
+
|
|
1152
|
+
it('subscribeToSubscriptionsAndListenToDocuments recovers from an OutOfSyncError instead of failing the document store', async () => {
|
|
1153
|
+
const documentId = DocumentId('doc-out-of-sync-recovery')
|
|
1154
|
+
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
1155
|
+
const documentState = getDocumentState<TestDocument>(instance, doc)
|
|
1156
|
+
const unsubscribe = documentState.subscribe()
|
|
1157
|
+
const draftId = getDraftId(documentId)
|
|
1158
|
+
|
|
1159
|
+
// Establish a synced document so the listener has a base revision.
|
|
1160
|
+
const created = await applyDocumentActions(instance, {
|
|
1161
|
+
actions: [createDocument(doc), editDocument(doc, {set: {title: 'initial'}})],
|
|
1162
|
+
})
|
|
1163
|
+
await created.submitted()
|
|
1164
|
+
expect(documentState.getCurrent()?.title).toBe('initial')
|
|
1165
|
+
|
|
1166
|
+
// Capture the fetchDocument spy so we can detect when the listener
|
|
1167
|
+
// re-subscribes. (every frest subscription re-runs fetchDocument)
|
|
1168
|
+
const fetchDocumentSpy = vi.mocked(createFetchDocument).mock.results.at(-1)?.value as ReturnType<
|
|
1169
|
+
typeof vi.fn
|
|
1170
|
+
>
|
|
1171
|
+
const fetchCallsBefore = fetchDocumentSpy.mock.calls.filter(([id]) => id === draftId).length
|
|
1172
|
+
|
|
1173
|
+
// Inject DEFAULT_MAX_BUFFER_SIZE mutations whose previousRev doesn't chain
|
|
1174
|
+
// to the base revision (basically forcing the error)
|
|
1175
|
+
const sharedListener = (
|
|
1176
|
+
createSharedListener as unknown as () => {events: Subject<ListenEvent<SanityDocument>>}
|
|
1177
|
+
)()
|
|
1178
|
+
for (let i = 0; i < DEFAULT_MAX_BUFFER_SIZE; i++) {
|
|
1179
|
+
sharedListener.events.next({
|
|
1180
|
+
type: 'mutation',
|
|
1181
|
+
documentId: draftId,
|
|
1182
|
+
eventId: `bad-${i}`,
|
|
1183
|
+
identity: 'user',
|
|
1184
|
+
mutations: [],
|
|
1185
|
+
timestamp: new Date().toISOString(),
|
|
1186
|
+
transactionId: `bad-tx-${i}`,
|
|
1187
|
+
transactionCurrentEvent: 0,
|
|
1188
|
+
transactionTotalEvents: 1,
|
|
1189
|
+
previousRev: `dangling-rev-${i}`,
|
|
1190
|
+
resultRev: `bad-rev-${i}`,
|
|
1191
|
+
transition: 'update',
|
|
1192
|
+
visibility: 'query',
|
|
1193
|
+
})
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// The above should have triggered a retry and re-subscribed to the listener
|
|
1197
|
+
await vi.waitFor(() => {
|
|
1198
|
+
expect(fetchDocumentSpy.mock.calls.filter(([id]) => id === draftId).length).toBeGreaterThan(
|
|
1199
|
+
fetchCallsBefore,
|
|
1200
|
+
)
|
|
1201
|
+
})
|
|
1202
|
+
|
|
1203
|
+
// The store stayed healthy through the OutOfSyncError.
|
|
1204
|
+
expect(() => documentState.getCurrent()).not.toThrow()
|
|
1205
|
+
expect(() => getDocumentSyncStatus(instance, doc).getCurrent()).not.toThrow()
|
|
1206
|
+
|
|
1207
|
+
// A follow-up edit still propagates through the recovered listener.
|
|
1208
|
+
const edited = await applyDocumentActions(instance, {
|
|
1209
|
+
actions: [editDocument(doc, {set: {title: 'after-recovery'}})],
|
|
1210
|
+
resource,
|
|
1211
|
+
})
|
|
1212
|
+
await edited.submitted()
|
|
1213
|
+
expect(documentState.getCurrent()?.title).toBe('after-recovery')
|
|
1214
|
+
|
|
1215
|
+
unsubscribe()
|
|
1216
|
+
})
|
|
1217
|
+
|
|
884
1218
|
vi.mock('../client/clientStore.ts', () => ({
|
|
885
1219
|
getClientState: vi.fn().mockReturnValue({observable: new ReplaySubject(1)}),
|
|
886
1220
|
}))
|
|
@@ -914,12 +1248,28 @@ vi.mock('./documentConstants.ts', async (importOriginal) => {
|
|
|
914
1248
|
...original,
|
|
915
1249
|
INITIAL_OUTGOING_THROTTLE_TIME: 0,
|
|
916
1250
|
DOCUMENT_STATE_CLEAR_DELAY: 25,
|
|
1251
|
+
OUT_OF_SYNC_RETRY_BASE_DELAY: 0,
|
|
1252
|
+
OUT_OF_SYNC_RETRY_MAX_DELAY: 0,
|
|
917
1253
|
}
|
|
918
1254
|
})
|
|
919
1255
|
|
|
920
1256
|
let client: SanityClient
|
|
921
1257
|
|
|
1258
|
+
/** Mock for `client.observable.mutate` (liveEdit submission path); implementation reset in `beforeEach`. */
|
|
1259
|
+
const mutateSubmission = vi.fn(
|
|
1260
|
+
async (
|
|
1261
|
+
_mutations: Mutation[],
|
|
1262
|
+
_options?: BaseMutationOptions,
|
|
1263
|
+
): Promise<MultipleMutationResult> => ({
|
|
1264
|
+
transactionId: '',
|
|
1265
|
+
documentIds: [],
|
|
1266
|
+
results: [],
|
|
1267
|
+
}),
|
|
1268
|
+
)
|
|
1269
|
+
|
|
922
1270
|
beforeEach(() => {
|
|
1271
|
+
mutateSubmission.mockReset()
|
|
1272
|
+
|
|
923
1273
|
const client$ = (getClientState as () => StateSource<SanityClient>)()
|
|
924
1274
|
.observable as ReplaySubject<SanityClient>
|
|
925
1275
|
const sharedListener = (
|
|
@@ -930,14 +1280,130 @@ beforeEach(() => {
|
|
|
930
1280
|
)()
|
|
931
1281
|
|
|
932
1282
|
let documents: DocumentSet = {
|
|
933
|
-
[getDraftId('existing-doc')]: {
|
|
934
|
-
_id: getDraftId('existing-doc'),
|
|
1283
|
+
[getDraftId(DocumentId('existing-doc'))]: {
|
|
1284
|
+
_id: getDraftId(DocumentId('existing-doc')),
|
|
935
1285
|
_createdAt: '2025-02-06T06:43:46.236Z',
|
|
936
1286
|
_updatedAt: '2025-02-06T06:43:46.236Z',
|
|
937
1287
|
_rev: 'initial-rev',
|
|
938
1288
|
_type: 'book',
|
|
939
1289
|
title: 'existing doc',
|
|
940
1290
|
},
|
|
1291
|
+
'live-edit-test-doc': {
|
|
1292
|
+
_id: 'live-edit-test-doc',
|
|
1293
|
+
_type: 'article',
|
|
1294
|
+
_createdAt: '2025-02-06T06:43:46.236Z',
|
|
1295
|
+
_updatedAt: '2025-02-06T06:43:46.236Z',
|
|
1296
|
+
_rev: 'rev-live-initial',
|
|
1297
|
+
title: 'live initial',
|
|
1298
|
+
},
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const emitDatasetChangeEvents = (
|
|
1302
|
+
prior: DocumentSet,
|
|
1303
|
+
next: DocumentSet,
|
|
1304
|
+
transactionId: string,
|
|
1305
|
+
timestamp: string,
|
|
1306
|
+
) => {
|
|
1307
|
+
const existingIds = new Set(
|
|
1308
|
+
Object.entries(prior)
|
|
1309
|
+
.filter(([, value]) => !!value)
|
|
1310
|
+
.map(([key]) => key),
|
|
1311
|
+
)
|
|
1312
|
+
const resultingIds = new Set(
|
|
1313
|
+
Object.entries(next)
|
|
1314
|
+
.filter(([, value]) => !!value)
|
|
1315
|
+
.map(([key]) => key),
|
|
1316
|
+
)
|
|
1317
|
+
const allKeys = new Set([...existingIds, ...resultingIds])
|
|
1318
|
+
|
|
1319
|
+
const {appeared, disappeared, updated} = Array.from(allKeys).reduce<{
|
|
1320
|
+
updated: string[]
|
|
1321
|
+
appeared: string[]
|
|
1322
|
+
disappeared: string[]
|
|
1323
|
+
}>(
|
|
1324
|
+
(acc, id) => {
|
|
1325
|
+
if (existingIds.has(id) && resultingIds.has(id)) {
|
|
1326
|
+
acc.updated.push(id)
|
|
1327
|
+
} else if (!existingIds.has(id) && resultingIds.has(id)) {
|
|
1328
|
+
acc.appeared.push(id)
|
|
1329
|
+
} else if (!resultingIds.has(id) && existingIds.has(id)) {
|
|
1330
|
+
acc.disappeared.push(id)
|
|
1331
|
+
}
|
|
1332
|
+
return acc
|
|
1333
|
+
},
|
|
1334
|
+
{updated: [], appeared: [], disappeared: []},
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
const transactionTotalEvents = appeared.length + disappeared.length + updated.length
|
|
1338
|
+
let transactionCurrentEvent = 0
|
|
1339
|
+
|
|
1340
|
+
const mutationEvents: MutationEvent[] = []
|
|
1341
|
+
|
|
1342
|
+
for (const id of appeared) {
|
|
1343
|
+
transactionCurrentEvent++
|
|
1344
|
+
const nextDoc = next[id]!
|
|
1345
|
+
mutationEvents.push({
|
|
1346
|
+
type: 'mutation',
|
|
1347
|
+
documentId: id,
|
|
1348
|
+
eventId: `${transactionId}#${id}`,
|
|
1349
|
+
identity: 'example-user',
|
|
1350
|
+
mutations: [{create: nextDoc}],
|
|
1351
|
+
timestamp,
|
|
1352
|
+
transactionId,
|
|
1353
|
+
transactionCurrentEvent,
|
|
1354
|
+
transactionTotalEvents,
|
|
1355
|
+
transition: 'appear',
|
|
1356
|
+
visibility: 'query',
|
|
1357
|
+
resultRev: transactionId,
|
|
1358
|
+
})
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
for (const id of updated) {
|
|
1362
|
+
transactionCurrentEvent++
|
|
1363
|
+
const prevDoc = prior[id]!
|
|
1364
|
+
const nextDoc = next[id]!
|
|
1365
|
+
|
|
1366
|
+
mutationEvents.push({
|
|
1367
|
+
type: 'mutation',
|
|
1368
|
+
documentId: id,
|
|
1369
|
+
eventId: `${transactionId}#${id}`,
|
|
1370
|
+
identity: 'example-user',
|
|
1371
|
+
mutations: diffValue(prevDoc, nextDoc).map((patch): Mutation => ({patch: {...patch, id}})),
|
|
1372
|
+
timestamp,
|
|
1373
|
+
transactionCurrentEvent,
|
|
1374
|
+
transactionTotalEvents,
|
|
1375
|
+
transactionId,
|
|
1376
|
+
transition: 'update',
|
|
1377
|
+
visibility: 'query',
|
|
1378
|
+
previousRev: prevDoc._rev,
|
|
1379
|
+
resultRev: transactionId,
|
|
1380
|
+
})
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
for (const id of disappeared) {
|
|
1384
|
+
transactionCurrentEvent++
|
|
1385
|
+
const prevDoc = prior[id]!
|
|
1386
|
+
mutationEvents.push({
|
|
1387
|
+
type: 'mutation',
|
|
1388
|
+
documentId: id,
|
|
1389
|
+
eventId: `${transactionId}#${id}`,
|
|
1390
|
+
identity: 'example-user',
|
|
1391
|
+
mutations: [{delete: {id}}],
|
|
1392
|
+
timestamp,
|
|
1393
|
+
transactionId,
|
|
1394
|
+
transactionCurrentEvent,
|
|
1395
|
+
transactionTotalEvents,
|
|
1396
|
+
transition: 'disappear',
|
|
1397
|
+
visibility: 'query',
|
|
1398
|
+
previousRev: prevDoc._rev,
|
|
1399
|
+
})
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
documents = next
|
|
1403
|
+
|
|
1404
|
+
for (const mutationEvent of mutationEvents) {
|
|
1405
|
+
sharedListener.events.next(mutationEvent)
|
|
1406
|
+
}
|
|
941
1407
|
}
|
|
942
1408
|
|
|
943
1409
|
vi.mocked(createFetchDocument).mockReturnValue(
|
|
@@ -949,6 +1415,33 @@ beforeEach(() => {
|
|
|
949
1415
|
),
|
|
950
1416
|
)
|
|
951
1417
|
|
|
1418
|
+
mutateSubmission.mockImplementation(
|
|
1419
|
+
async (
|
|
1420
|
+
mutations: Mutation[],
|
|
1421
|
+
options: BaseMutationOptions = {},
|
|
1422
|
+
): Promise<MultipleMutationResult> => {
|
|
1423
|
+
const transactionId = options.transactionId ?? crypto.randomUUID()
|
|
1424
|
+
const timestamp = new Date().toISOString()
|
|
1425
|
+
const prior = {...documents}
|
|
1426
|
+
let next = {...documents}
|
|
1427
|
+
next = processMutations({documents: next, mutations, transactionId, timestamp})
|
|
1428
|
+
|
|
1429
|
+
if (!options.dryRun) {
|
|
1430
|
+
emitDatasetChangeEvents(prior, next, transactionId, timestamp)
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
1434
|
+
|
|
1435
|
+
return {
|
|
1436
|
+
transactionId,
|
|
1437
|
+
documentIds: Object.entries(next)
|
|
1438
|
+
.filter(([, v]) => !!v)
|
|
1439
|
+
.map(([k]) => k),
|
|
1440
|
+
results: [],
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
)
|
|
1444
|
+
|
|
952
1445
|
const isNonNullable = <T>(t: T): t is NonNullable<T> => !!t
|
|
953
1446
|
|
|
954
1447
|
const fetch = vi.fn(
|
|
@@ -1029,7 +1522,7 @@ beforeEach(() => {
|
|
|
1029
1522
|
continue
|
|
1030
1523
|
}
|
|
1031
1524
|
case 'sanity.action.document.edit': {
|
|
1032
|
-
const documentSource = next[i.draftId] ?? next[i.publishedId]
|
|
1525
|
+
const documentSource = (i.draftId && next[i.draftId]) ?? next[i.publishedId]
|
|
1033
1526
|
if (!documentSource) {
|
|
1034
1527
|
throw new Error(
|
|
1035
1528
|
`Could not find a document to edit from \`draftId\` \`${i.draftId}\` or \`publishedId\` ${i.publishedId}`,
|
|
@@ -1039,8 +1532,8 @@ beforeEach(() => {
|
|
|
1039
1532
|
next = processMutations({
|
|
1040
1533
|
documents: next,
|
|
1041
1534
|
mutations: [
|
|
1042
|
-
{createIfNotExists: {...documentSource, _id: i.draftId}},
|
|
1043
|
-
{patch: {id: i.draftId
|
|
1535
|
+
{createIfNotExists: {...documentSource, _id: i.draftId!}},
|
|
1536
|
+
{patch: {id: i.draftId!, ...i.patch}},
|
|
1044
1537
|
],
|
|
1045
1538
|
transactionId,
|
|
1046
1539
|
timestamp,
|
|
@@ -1121,114 +1614,15 @@ beforeEach(() => {
|
|
|
1121
1614
|
continue
|
|
1122
1615
|
}
|
|
1123
1616
|
default: {
|
|
1124
|
-
throw new Error(
|
|
1617
|
+
throw new Error(
|
|
1618
|
+
`Unsupported action for mock backend: ${(i as {actionType: string}).actionType}`,
|
|
1619
|
+
)
|
|
1125
1620
|
}
|
|
1126
1621
|
}
|
|
1127
1622
|
}
|
|
1128
1623
|
|
|
1129
1624
|
if (!dryRun) {
|
|
1130
|
-
|
|
1131
|
-
Object.entries(documents)
|
|
1132
|
-
.filter(([, value]) => !!value)
|
|
1133
|
-
.map(([key]) => key),
|
|
1134
|
-
)
|
|
1135
|
-
const resultingIds = new Set(
|
|
1136
|
-
Object.entries(next)
|
|
1137
|
-
.filter(([, value]) => !!value)
|
|
1138
|
-
.map(([key]) => key),
|
|
1139
|
-
)
|
|
1140
|
-
const allKeys = new Set([...existingIds, ...resultingIds])
|
|
1141
|
-
|
|
1142
|
-
const {appeared, disappeared, updated} = Array.from(allKeys).reduce<{
|
|
1143
|
-
updated: string[]
|
|
1144
|
-
appeared: string[]
|
|
1145
|
-
disappeared: string[]
|
|
1146
|
-
}>(
|
|
1147
|
-
(acc, id) => {
|
|
1148
|
-
if (existingIds.has(id) && resultingIds.has(id)) {
|
|
1149
|
-
acc.updated.push(id)
|
|
1150
|
-
} else if (!existingIds.has(id) && resultingIds.has(id)) {
|
|
1151
|
-
acc.appeared.push(id)
|
|
1152
|
-
} else if (!resultingIds.has(id) && existingIds.has(id)) {
|
|
1153
|
-
acc.disappeared.push(id)
|
|
1154
|
-
}
|
|
1155
|
-
return acc
|
|
1156
|
-
},
|
|
1157
|
-
{updated: [], appeared: [], disappeared: []},
|
|
1158
|
-
)
|
|
1159
|
-
|
|
1160
|
-
const transactionTotalEvents = appeared.length + disappeared.length + updated.length
|
|
1161
|
-
let transactionCurrentEvent = 0
|
|
1162
|
-
|
|
1163
|
-
const mutationEvents: MutationEvent[] = []
|
|
1164
|
-
|
|
1165
|
-
for (const id of appeared) {
|
|
1166
|
-
transactionCurrentEvent++ // index seems to start at 1
|
|
1167
|
-
const nextDoc = next[id]!
|
|
1168
|
-
mutationEvents.push({
|
|
1169
|
-
type: 'mutation',
|
|
1170
|
-
documentId: id,
|
|
1171
|
-
eventId: `${transactionId}#${id}`,
|
|
1172
|
-
identity: 'example-user',
|
|
1173
|
-
mutations: [{create: nextDoc}],
|
|
1174
|
-
timestamp,
|
|
1175
|
-
transactionId,
|
|
1176
|
-
transactionCurrentEvent,
|
|
1177
|
-
transactionTotalEvents,
|
|
1178
|
-
transition: 'appear',
|
|
1179
|
-
visibility: 'query',
|
|
1180
|
-
resultRev: transactionId,
|
|
1181
|
-
})
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
for (const id of updated) {
|
|
1185
|
-
transactionCurrentEvent++
|
|
1186
|
-
const prevDoc = documents[id]!
|
|
1187
|
-
const nextDoc = next[id]!
|
|
1188
|
-
|
|
1189
|
-
mutationEvents.push({
|
|
1190
|
-
type: 'mutation',
|
|
1191
|
-
documentId: id,
|
|
1192
|
-
eventId: `${transactionId}#${id}`,
|
|
1193
|
-
identity: 'example-user',
|
|
1194
|
-
mutations: diffValue(prevDoc, nextDoc).map(
|
|
1195
|
-
(patch): Mutation => ({patch: {...patch, id}}),
|
|
1196
|
-
),
|
|
1197
|
-
timestamp,
|
|
1198
|
-
transactionCurrentEvent,
|
|
1199
|
-
transactionTotalEvents,
|
|
1200
|
-
transactionId,
|
|
1201
|
-
transition: 'update',
|
|
1202
|
-
visibility: 'query',
|
|
1203
|
-
previousRev: prevDoc._rev,
|
|
1204
|
-
resultRev: transactionId,
|
|
1205
|
-
})
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
for (const id of disappeared) {
|
|
1209
|
-
transactionCurrentEvent++
|
|
1210
|
-
const prevDoc = documents[id]!
|
|
1211
|
-
mutationEvents.push({
|
|
1212
|
-
type: 'mutation',
|
|
1213
|
-
documentId: id,
|
|
1214
|
-
eventId: `${transactionId}#${id}`,
|
|
1215
|
-
identity: 'example-user',
|
|
1216
|
-
mutations: [{delete: {id}}],
|
|
1217
|
-
timestamp,
|
|
1218
|
-
transactionId,
|
|
1219
|
-
transactionCurrentEvent,
|
|
1220
|
-
transactionTotalEvents,
|
|
1221
|
-
transition: 'disappear',
|
|
1222
|
-
visibility: 'query',
|
|
1223
|
-
previousRev: prevDoc._rev,
|
|
1224
|
-
})
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
documents = next
|
|
1228
|
-
|
|
1229
|
-
for (const mutationEvent of mutationEvents) {
|
|
1230
|
-
sharedListener.events.next(mutationEvent)
|
|
1231
|
-
}
|
|
1625
|
+
emitDatasetChangeEvents(documents, next, transactionId, timestamp)
|
|
1232
1626
|
}
|
|
1233
1627
|
|
|
1234
1628
|
// add a tick for realism
|
|
@@ -1255,6 +1649,7 @@ beforeEach(() => {
|
|
|
1255
1649
|
action: (...args: Parameters<typeof action>) => from(action(...args)),
|
|
1256
1650
|
fetch: (...args: Parameters<typeof fetch>) => from(fetch(...args)),
|
|
1257
1651
|
request: (...args: Parameters<typeof request>) => from(request(...args)),
|
|
1652
|
+
mutate: (...args: Parameters<typeof mutateSubmission>) => from(mutateSubmission(...args)),
|
|
1258
1653
|
},
|
|
1259
1654
|
} as SanityClient
|
|
1260
1655
|
client$.next(client)
|