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