@sanity/sdk 3.0.0-rc.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_exports/_internal.d.ts +25 -3
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +127 -9
- package/dist/_exports/_internal.js.map +1 -1
- package/dist/_exports/agent.d.ts +1 -1
- package/dist/_exports/agent.js +1 -1
- package/dist/_exports/comlink.d.ts +1 -1
- package/dist/_exports/comlink.js +2 -3
- package/dist/_exports/comlink.js.map +1 -1
- package/dist/{authStore-DKFX8fn7.d.ts → authStore-C-OEkGaM.d.ts} +30 -44
- package/dist/authStore-C-OEkGaM.d.ts.map +1 -0
- package/dist/{authStore-CtE9bLHW.js → clientStore-gUALQ0qW.js} +269 -11
- package/dist/{authStore-CtE9bLHW.js.map → clientStore-gUALQ0qW.js.map} +1 -1
- package/dist/{createSanityInstance-DUI1bGIg.d.ts → createSanityInstance-DCePpBYW.d.ts} +2 -2
- package/dist/{createSanityInstance-DUI1bGIg.d.ts.map → createSanityInstance-DCePpBYW.d.ts.map} +1 -1
- package/dist/{getNodeState-CR3__B2D.js → getNodeState-E0hjqFOo.js} +2 -3
- package/dist/{getNodeState-CR3__B2D.js.map → getNodeState-E0hjqFOo.js.map} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +168 -176
- package/dist/index.js.map +1 -1
- package/dist/{createStateSourceAction-BOKtqFNm.js → object-BwynGPRZ.js} +87 -3
- package/dist/object-BwynGPRZ.js.map +1 -0
- package/dist/{queryStore-DPf9sN3V.js → version-C90rNWaX.js} +48 -16
- package/dist/version-C90rNWaX.js.map +1 -0
- package/package.json +4 -4
- package/src/_exports/_internal.ts +1 -0
- package/src/_exports/index.ts +7 -4
- package/src/access/checkPermissions.test.ts +1 -1
- package/src/access/checkPermissions.ts +1 -1
- package/src/applications/applications.test.ts +4 -4
- package/src/applications/applications.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +2 -2
- package/src/auth/handleAuthCallback.ts +1 -1
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -1
- package/src/auth/refreshStampedToken.ts +1 -1
- package/src/auth/studioModeAuth.test.ts +1 -1
- package/src/auth/studioModeAuth.ts +1 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +2 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -1
- package/src/comments/addonDatasetStore.test.ts +2 -2
- package/src/comments/addonDatasetStore.ts +2 -2
- package/src/comments/commentActions.ts +6 -5
- package/src/comments/commentsStore.ts +4 -4
- package/src/datasets/datasets.test.ts +44 -9
- package/src/datasets/datasets.ts +24 -23
- package/src/document/applyDocumentActions.ts +2 -6
- package/src/document/documentConstants.ts +9 -0
- package/src/document/documentStore.concurrency.test.ts +2 -1
- package/src/document/documentStore.test.ts +87 -17
- package/src/document/documentStore.ts +5 -5
- package/src/document/permissions.ts +2 -1
- package/src/document/processActions.test.ts +2 -1
- package/src/document/processMutations.ts +3 -2
- package/src/document/reducers.test.ts +224 -0
- package/src/document/reducers.ts +110 -27
- package/src/favorites/favorites.test.ts +64 -24
- package/src/favorites/favorites.ts +73 -22
- package/src/installations/installations.test.ts +2 -2
- package/src/installations/installations.ts +2 -2
- package/src/organization/organization.test.ts +2 -2
- package/src/organization/organization.ts +1 -1
- package/src/organizations/organizations.test.ts +2 -2
- package/src/organizations/organizations.ts +1 -1
- package/src/presence/presenceStore.test.ts +15 -22
- package/src/presence/presenceStore.ts +3 -2
- package/src/project/project.test.ts +3 -3
- package/src/project/project.ts +1 -1
- package/src/projection/getProjectionState.test.ts +6 -4
- package/src/projection/getProjectionState.ts +2 -2
- package/src/projects/projects.test.ts +2 -2
- package/src/projects/projects.ts +1 -1
- package/src/query/queryStore.ts +3 -3
- package/src/store/createSanityInstance.test.ts +1 -1
- package/src/store/createSanityInstance.ts +4 -4
- package/src/store/createStoreInstance.test.ts +0 -5
- package/src/store/fetcherStore.test.ts +23 -1
- package/src/store/fetcherStore.ts +3 -3
- package/src/telemetry/telemetryManager.test.ts +1 -1
- package/src/telemetry/telemetryManager.ts +2 -2
- package/src/users/usersStore.test.ts +1 -1
- package/src/users/usersStore.ts +5 -5
- package/src/utils/ids.test.ts +40 -15
- package/src/utils/ids.ts +47 -2
- package/dist/authStore-DKFX8fn7.d.ts.map +0 -1
- package/dist/clientStore-BtE2-q2i.js +0 -90
- package/dist/clientStore-BtE2-q2i.js.map +0 -1
- package/dist/createStateSourceAction-BOKtqFNm.js.map +0 -1
- package/dist/getEnv-D5RGkQLu.js +0 -8
- package/dist/getEnv-D5RGkQLu.js.map +0 -1
- package/dist/logger-DAtJb5RQ.js +0 -173
- package/dist/logger-DAtJb5RQ.js.map +0 -1
- package/dist/object-CxXlSVg7.js +0 -83
- package/dist/object-CxXlSVg7.js.map +0 -1
- package/dist/queryStore-DPf9sN3V.js.map +0 -1
- package/dist/rolldown-runtime-vyAXikos.js +0 -9
- package/dist/telemetryManager-Dq_qCkeI.js +0 -127
- package/dist/telemetryManager-Dq_qCkeI.js.map +0 -1
- package/dist/version-BChjsi4u.js +0 -9
- package/dist/version-BChjsi4u.js.map +0 -1
- package/src/utils/createFetcherStore.test.ts +0 -171
- package/src/utils/createFetcherStore.ts +0 -284
|
@@ -57,7 +57,7 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
57
57
|
})
|
|
58
58
|
expect(mockRequest).toHaveBeenCalledWith({
|
|
59
59
|
method: 'GET',
|
|
60
|
-
|
|
60
|
+
url: '/users/me',
|
|
61
61
|
tag: 'users.get-current',
|
|
62
62
|
})
|
|
63
63
|
|
|
@@ -97,7 +97,7 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
97
97
|
})
|
|
98
98
|
expect(mockRequest).toHaveBeenCalledWith({
|
|
99
99
|
method: 'GET',
|
|
100
|
-
|
|
100
|
+
url: '/users/me',
|
|
101
101
|
tag: 'users.get-current',
|
|
102
102
|
})
|
|
103
103
|
|
|
@@ -59,7 +59,7 @@ describe('getAddonDatasetState', () => {
|
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
expect(observableRequest).toHaveBeenCalledWith({
|
|
62
|
-
|
|
62
|
+
url: '/projects/p/datasets?datasetProfile=comments&addonFor=d',
|
|
63
63
|
tag: 'comments.addon-dataset.list',
|
|
64
64
|
})
|
|
65
65
|
})
|
|
@@ -139,7 +139,7 @@ describe('provisionAddonDataset', () => {
|
|
|
139
139
|
|
|
140
140
|
await expect(provisionAddonDataset(instance, {})).resolves.toBe('d-comments')
|
|
141
141
|
expect(request).toHaveBeenCalledWith({
|
|
142
|
-
|
|
142
|
+
url: '/comments/d/setup',
|
|
143
143
|
method: 'POST',
|
|
144
144
|
tag: 'comments.addon-dataset.setup',
|
|
145
145
|
})
|
|
@@ -67,7 +67,7 @@ function requestAddonDataset(
|
|
|
67
67
|
): Observable<string | undefined> {
|
|
68
68
|
return client.observable
|
|
69
69
|
.request<{name: string}[] | undefined>({
|
|
70
|
-
|
|
70
|
+
url: `/projects/${projectId}/datasets?datasetProfile=${ADDON_DATASET_PROFILE}&addonFor=${dataset}`,
|
|
71
71
|
tag: 'comments.addon-dataset.list',
|
|
72
72
|
})
|
|
73
73
|
.pipe(map((datasets) => datasets?.[0]?.name))
|
|
@@ -182,7 +182,7 @@ export const provisionAddonDataset: (
|
|
|
182
182
|
})
|
|
183
183
|
|
|
184
184
|
const response = await client.request<{datasetName?: string} | undefined>({
|
|
185
|
-
|
|
185
|
+
url: `/comments/${resource.dataset}/setup`,
|
|
186
186
|
method: 'POST',
|
|
187
187
|
tag: 'comments.addon-dataset.setup',
|
|
188
188
|
})
|
|
@@ -16,6 +16,7 @@ import {bindActionByResource, type BoundResourceKey} from '../store/createAction
|
|
|
16
16
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
17
17
|
import {type StoreState} from '../store/createStoreState'
|
|
18
18
|
import {type StoreContext} from '../store/defineStore'
|
|
19
|
+
import {randomUuid} from '../utils/ids'
|
|
19
20
|
import {
|
|
20
21
|
assertDatasetResource,
|
|
21
22
|
getAddonDatasetState,
|
|
@@ -326,7 +327,7 @@ export const createComment: (
|
|
|
326
327
|
options,
|
|
327
328
|
buildCommentPayload({
|
|
328
329
|
authorId: requireCurrentUserId(instance),
|
|
329
|
-
commentId: options.commentId ??
|
|
330
|
+
commentId: options.commentId ?? randomUuid(),
|
|
330
331
|
contentSnapshot: options.contentSnapshot,
|
|
331
332
|
context: options.context,
|
|
332
333
|
documentRevisionId: options.documentRevisionId,
|
|
@@ -336,7 +337,7 @@ export const createComment: (
|
|
|
336
337
|
fieldPath,
|
|
337
338
|
selection: options.selection,
|
|
338
339
|
status: options.status ?? 'open',
|
|
339
|
-
threadId: options.threadId ??
|
|
340
|
+
threadId: options.threadId ?? randomUuid(),
|
|
340
341
|
...(isReleasePerspective(perspective) ? {documentVersionId: perspective.releaseName} : {}),
|
|
341
342
|
}),
|
|
342
343
|
)
|
|
@@ -386,7 +387,7 @@ export const replyToComment: (
|
|
|
386
387
|
options,
|
|
387
388
|
buildCommentPayload({
|
|
388
389
|
authorId: requireCurrentUserId(instance),
|
|
389
|
-
commentId: options.commentId ??
|
|
390
|
+
commentId: options.commentId ?? randomUuid(),
|
|
390
391
|
handle: options,
|
|
391
392
|
message: options.message,
|
|
392
393
|
// Replies to a reply belong to the thread's first comment, matching how
|
|
@@ -420,7 +421,7 @@ export const updateComment: (
|
|
|
420
421
|
{commentId, message}: UpdateCommentOptions,
|
|
421
422
|
) => {
|
|
422
423
|
const lastEditedAt = new Date().toISOString()
|
|
423
|
-
const transactionId =
|
|
424
|
+
const transactionId = randomUuid()
|
|
424
425
|
const previous = findCommentById(state, commentId)
|
|
425
426
|
|
|
426
427
|
if (previous) {
|
|
@@ -468,7 +469,7 @@ export const setCommentStatus: (
|
|
|
468
469
|
{state, instance, key}: StoreContext<CommentsStoreState, BoundResourceKey>,
|
|
469
470
|
{commentId, status}: SetCommentStatusOptions,
|
|
470
471
|
) => {
|
|
471
|
-
const transactionId =
|
|
472
|
+
const transactionId = randomUuid()
|
|
472
473
|
const previousComments: StoredComment[] = []
|
|
473
474
|
|
|
474
475
|
for (const entry of Object.values(state.get().entries)) {
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from '../store/createStateSourceAction'
|
|
30
30
|
import {type StoreState} from '../store/createStoreState'
|
|
31
31
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
32
|
-
import {
|
|
32
|
+
import {randomId} from '../utils/ids'
|
|
33
33
|
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
34
34
|
import {observeAddonDatasetClient} from './addonDatasetStore'
|
|
35
35
|
import {buildCommentThreads} from './buildCommentThreads'
|
|
@@ -360,7 +360,7 @@ export const getCommentsState: (
|
|
|
360
360
|
selector: selectComments,
|
|
361
361
|
onSubscribe: ({state, instance}, options: CommentsOptions) => {
|
|
362
362
|
const key = getCommentsKey(toCommentsKeyParts(instance, options))
|
|
363
|
-
const subscriptionId =
|
|
363
|
+
const subscriptionId = randomId(16)
|
|
364
364
|
state.set('addSubscriber', addSubscriber(key, subscriptionId))
|
|
365
365
|
|
|
366
366
|
return () => {
|
|
@@ -390,7 +390,7 @@ export const getCommentThreadsState: (
|
|
|
390
390
|
selector: selectCommentThreads,
|
|
391
391
|
onSubscribe: ({state, instance}, options: CommentsOptions) => {
|
|
392
392
|
const key = getCommentsKey(toCommentsKeyParts(instance, options))
|
|
393
|
-
const subscriptionId =
|
|
393
|
+
const subscriptionId = randomId(16)
|
|
394
394
|
state.set('addSubscriber', addSubscriber(key, subscriptionId))
|
|
395
395
|
|
|
396
396
|
return () => {
|
|
@@ -452,7 +452,7 @@ function resolveList<T>(
|
|
|
452
452
|
// fetch and this promise would never settle. Holding it only for the duration
|
|
453
453
|
// of the resolve also means a component that suspends and then errors before
|
|
454
454
|
// mounting does not leave a subscriber-less entry behind holding its error.
|
|
455
|
-
const subscriptionId =
|
|
455
|
+
const subscriptionId = randomId(16)
|
|
456
456
|
state.set('addSubscriber', addSubscriber(key, subscriptionId))
|
|
457
457
|
|
|
458
458
|
const release = () => state.set('removeSubscriber', removeSubscriber(key, subscriptionId))
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import {type SanityClient} from '@sanity/client'
|
|
2
2
|
import {of} from 'rxjs'
|
|
3
|
-
import {describe, it} from 'vitest'
|
|
3
|
+
import {afterEach, beforeEach, describe, it} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {getClientState} from '../client/clientStore'
|
|
6
|
-
import {createSanityInstance} from '../store/createSanityInstance'
|
|
6
|
+
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
7
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
|
-
import {
|
|
8
|
+
import {datasets} from './datasets'
|
|
9
9
|
|
|
10
10
|
vi.mock('../client/clientStore')
|
|
11
11
|
|
|
12
12
|
describe('datasets', () => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
let instance: SanityInstance
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
17
|
+
})
|
|
17
18
|
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
instance.dispose()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const mockClientWithList = (list: ReturnType<typeof vi.fn>) => {
|
|
18
24
|
const mockClient = {
|
|
19
25
|
observable: {
|
|
20
26
|
datasets: {list} as unknown as SanityClient['observable']['datasets'],
|
|
@@ -24,9 +30,38 @@ describe('datasets', () => {
|
|
|
24
30
|
vi.mocked(getClientState).mockReturnValue({
|
|
25
31
|
observable: of(mockClient),
|
|
26
32
|
} as StateSource<SanityClient>)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
it('calls the `client.observable.datasets.list` method on the client and returns the result', async () => {
|
|
36
|
+
const list = vi.fn().mockReturnValue(of([{id: 'a'}, {id: 'b'}]))
|
|
37
|
+
mockClientWithList(list)
|
|
27
38
|
|
|
28
|
-
const result = await
|
|
29
|
-
expect(result).toEqual(
|
|
39
|
+
const result = await datasets.resolveState(instance)
|
|
40
|
+
expect(result).toEqual([{id: 'a'}, {id: 'b'}])
|
|
30
41
|
expect(list).toHaveBeenCalled()
|
|
42
|
+
expect(getClientState).toHaveBeenCalledWith(
|
|
43
|
+
instance,
|
|
44
|
+
expect.objectContaining({projectId: 'p', useProjectHostname: true}),
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('reads datasets for an explicit projectId over the instance config', async () => {
|
|
49
|
+
const list = vi.fn().mockReturnValue(of([]))
|
|
50
|
+
mockClientWithList(list)
|
|
51
|
+
|
|
52
|
+
await datasets.resolveState(instance, {projectId: 'other'})
|
|
53
|
+
expect(getClientState).toHaveBeenCalledWith(
|
|
54
|
+
instance,
|
|
55
|
+
expect.objectContaining({projectId: 'other'}),
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('rejects when no projectId can be resolved', async () => {
|
|
60
|
+
const bare = createSanityInstance({})
|
|
61
|
+
// async wrapper: the missing-projectId error is thrown synchronously from getKey
|
|
62
|
+
await expect(async () => datasets.resolveState(bare)).rejects.toThrow(
|
|
63
|
+
'A projectId is required to use the datasets API.',
|
|
64
|
+
)
|
|
65
|
+
bare.dispose()
|
|
31
66
|
})
|
|
32
67
|
})
|
package/src/datasets/datasets.ts
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
|
+
import {type DatasetsResponse} from '@sanity/client'
|
|
1
2
|
import {switchMap} from 'rxjs'
|
|
2
3
|
|
|
3
4
|
import {getClientState} from '../client/clientStore'
|
|
4
5
|
import {type ProjectHandle} from '../config/sanityConfig'
|
|
5
|
-
import {
|
|
6
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
7
|
+
import {defineFetcher} from '../store/fetcherStore'
|
|
6
8
|
|
|
7
9
|
const API_VERSION = 'v2025-02-19'
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
function resolveProjectId(instance: SanityInstance, options?: ProjectHandle): string {
|
|
12
|
+
const projectId = options?.projectId ?? instance.config.projectId
|
|
13
|
+
if (!projectId) {
|
|
14
|
+
throw new Error('A projectId is required to use the datasets API.')
|
|
15
|
+
}
|
|
16
|
+
return projectId
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fetcher for a project's datasets (`GET /datasets`), on the shared fetcher
|
|
21
|
+
* cache.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export const datasets = defineFetcher<[options?: ProjectHandle], DatasetsResponse>({
|
|
26
|
+
name: 'datasets',
|
|
27
|
+
getKey: resolveProjectId,
|
|
28
|
+
fetch: (instance) => (options) =>
|
|
29
|
+
getClientState(instance, {
|
|
21
30
|
apiVersion: API_VERSION,
|
|
22
|
-
|
|
23
|
-
projectId: (options?.projectId ?? instance.config.projectId)!,
|
|
31
|
+
projectId: resolveProjectId(instance, options),
|
|
24
32
|
useProjectHostname: true,
|
|
25
|
-
}).observable.pipe(switchMap((client) => client.observable.datasets.list()))
|
|
26
|
-
},
|
|
33
|
+
}).observable.pipe(switchMap((client) => client.observable.datasets.list())),
|
|
27
34
|
})
|
|
28
|
-
|
|
29
|
-
/** @public */
|
|
30
|
-
export const getDatasetsState = datasets.getState
|
|
31
|
-
|
|
32
|
-
/** @public */
|
|
33
|
-
export const resolveDatasets = datasets.resolveState
|
|
@@ -5,6 +5,7 @@ import {type DocumentResource} from '../config/sanityConfig'
|
|
|
5
5
|
import {bindActionByResource} from '../store/createActionBinder'
|
|
6
6
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
7
7
|
import {type StoreContext} from '../store/defineStore'
|
|
8
|
+
import {randomUuid} from '../utils/ids'
|
|
8
9
|
import {type Action} from './actions'
|
|
9
10
|
import {documentStore, type DocumentStoreState} from './documentStore'
|
|
10
11
|
import {type DocumentTransactionSubmissionResult} from './events'
|
|
@@ -74,12 +75,7 @@ const boundApplyDocumentActions = bindActionByResource(documentStore, _applyDocu
|
|
|
74
75
|
/** @internal */
|
|
75
76
|
async function _applyDocumentActions(
|
|
76
77
|
{state}: StoreContext<DocumentStoreState>,
|
|
77
|
-
{
|
|
78
|
-
actions,
|
|
79
|
-
resource,
|
|
80
|
-
transactionId = crypto.randomUUID(),
|
|
81
|
-
disableBatching,
|
|
82
|
-
}: ApplyDocumentActionsOptions,
|
|
78
|
+
{actions, resource, transactionId = randomUuid(), disableBatching}: ApplyDocumentActionsOptions,
|
|
83
79
|
): Promise<ActionsResult> {
|
|
84
80
|
const {events} = state.get()
|
|
85
81
|
|
|
@@ -9,6 +9,15 @@ export const DOCUMENT_STATE_CLEAR_DELAY = 1000
|
|
|
9
9
|
export const INITIAL_OUTGOING_THROTTLE_TIME = 1000
|
|
10
10
|
export const API_VERSION = 'v2025-05-06'
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* How long a submitted transaction's revision is kept around waiting to be
|
|
14
|
+
* verified by its listener echo. After this, the transaction is no longer
|
|
15
|
+
* treated as in flight and a document held onto only for that verification can
|
|
16
|
+
* be evicted. Sized to match the listener's own chaining deadline, after which
|
|
17
|
+
* an echo that has not arrived is not going to.
|
|
18
|
+
*/
|
|
19
|
+
export const UNVERIFIED_REVISION_RETENTION_TIME = 30_000
|
|
20
|
+
|
|
12
21
|
/**
|
|
13
22
|
* Base delay (ms) before retrying a document listener after an `OutOfSyncError`.
|
|
14
23
|
* Backoff doubles on each successive retry, capped at {@link OUT_OF_SYNC_RETRY_MAX_DELAY}.
|
|
@@ -44,6 +44,7 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
|
44
44
|
import {getClientState} from '../client/clientStore'
|
|
45
45
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
46
46
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
47
|
+
import {randomUuid} from '../utils/ids'
|
|
47
48
|
import {editDocument, publishDocument} from './actions'
|
|
48
49
|
import {applyDocumentActions} from './applyDocumentActions'
|
|
49
50
|
import {getDocumentState, getDocumentSyncStatus} from './documentStore'
|
|
@@ -273,7 +274,7 @@ function createMockClient(server: MockContentLake): SanityClient {
|
|
|
273
274
|
|
|
274
275
|
const action = vi.fn(async (input: HttpAction | HttpAction[], options?: BaseActionOptions) => {
|
|
275
276
|
const actions = Array.isArray(input) ? input : [input]
|
|
276
|
-
return server.submitActions(actions, options?.transactionId ??
|
|
277
|
+
return server.submitActions(actions, options?.transactionId ?? randomUuid())
|
|
277
278
|
})
|
|
278
279
|
|
|
279
280
|
const request = vi.fn(async () => {
|
|
@@ -36,6 +36,7 @@ import {getClientState} from '../client/clientStore'
|
|
|
36
36
|
import {createDocumentHandle} from '../config/handles'
|
|
37
37
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
38
38
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
39
|
+
import {randomUuid} from '../utils/ids'
|
|
39
40
|
import {
|
|
40
41
|
createDocument,
|
|
41
42
|
deleteDocument,
|
|
@@ -357,6 +358,75 @@ it('emits remote-patches events distinguishing own and foreign transactions', as
|
|
|
357
358
|
state2Unsubscribe()
|
|
358
359
|
})
|
|
359
360
|
|
|
361
|
+
it('labels an echo that arrives after the last subscriber left as its own', async () => {
|
|
362
|
+
const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article'})
|
|
363
|
+
const draftId = getDraftId(DocumentId(doc.documentId))
|
|
364
|
+
const state = getDocumentState(instance, doc)
|
|
365
|
+
const unsubscribe = state.subscribe()
|
|
366
|
+
await vi.waitUntil(() => state.getCurrent() !== undefined)
|
|
367
|
+
const previousRev = state.getCurrent()?._rev
|
|
368
|
+
|
|
369
|
+
// ack without emitting the listener echo, so the echo can be delivered later
|
|
370
|
+
// and out of band, which is the ordering the real API produces
|
|
371
|
+
const clientActionMockImplementation = vi.mocked(client.action).getMockImplementation()!
|
|
372
|
+
vi.mocked(client.action).mockImplementation(
|
|
373
|
+
async (_input, {transactionId = randomUuid()} = {}) => ({transactionId}),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
const patchEvents: DocumentRemotePatchesEvent[] = []
|
|
377
|
+
const unsubscribeEvents = subscribeDocumentEvents(instance, {
|
|
378
|
+
resource,
|
|
379
|
+
eventHandler: (e) => {
|
|
380
|
+
if (e.type === 'remote-patches') patchEvents.push(e)
|
|
381
|
+
},
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const edited = await applyDocumentActions(instance, {
|
|
385
|
+
actions: [editDocument(doc, {set: {title: 'edited while leaving'}})],
|
|
386
|
+
resource,
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// the last subscriber leaves before the ack comes back, which used to delete
|
|
390
|
+
// the document state (and with it the memory of our own transaction) as soon
|
|
391
|
+
// as the ack released the transaction's subscription
|
|
392
|
+
unsubscribe()
|
|
393
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
394
|
+
await edited.submitted()
|
|
395
|
+
|
|
396
|
+
const {events: listenerEvents} = (
|
|
397
|
+
createSharedListener as () => {
|
|
398
|
+
dispose: () => void
|
|
399
|
+
events: Subject<ListenEvent<SanityDocument>>
|
|
400
|
+
}
|
|
401
|
+
)()
|
|
402
|
+
listenerEvents.next({
|
|
403
|
+
type: 'mutation',
|
|
404
|
+
documentId: draftId,
|
|
405
|
+
eventId: `${edited.transactionId}#${draftId}`,
|
|
406
|
+
identity: 'example-user',
|
|
407
|
+
mutations: [{patch: {id: draftId, set: {title: 'edited while leaving'}}}],
|
|
408
|
+
timestamp: new Date().toISOString(),
|
|
409
|
+
transactionId: edited.transactionId,
|
|
410
|
+
transactionCurrentEvent: 1,
|
|
411
|
+
transactionTotalEvents: 1,
|
|
412
|
+
transition: 'update',
|
|
413
|
+
visibility: 'query',
|
|
414
|
+
previousRev,
|
|
415
|
+
resultRev: edited.transactionId,
|
|
416
|
+
} satisfies MutationEvent)
|
|
417
|
+
await vi.waitUntil(() => patchEvents.length > 0)
|
|
418
|
+
|
|
419
|
+
expect(patchEvents).toHaveLength(1)
|
|
420
|
+
expect(patchEvents[0]).toMatchObject({
|
|
421
|
+
documentId: draftId,
|
|
422
|
+
transactionId: edited.transactionId,
|
|
423
|
+
origin: 'local',
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
unsubscribeEvents()
|
|
427
|
+
vi.mocked(client.action).mockImplementation(clientActionMockImplementation)
|
|
428
|
+
})
|
|
429
|
+
|
|
360
430
|
it('handles concurrent edits and resolves conflicts', async () => {
|
|
361
431
|
const doc = createDocumentHandle({documentId: 'doc-concurrent', documentType: 'article'})
|
|
362
432
|
const state1 = getDocumentState(instance1, doc)
|
|
@@ -602,7 +672,7 @@ it('fetches documents if there are no active subscriptions for the actions appli
|
|
|
602
672
|
})
|
|
603
673
|
|
|
604
674
|
it('batches edit transaction into one outgoing transaction', async () => {
|
|
605
|
-
const doc = createDocumentHandle({documentId:
|
|
675
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
606
676
|
|
|
607
677
|
const unsubscribe = getDocumentState(instance, doc).subscribe()
|
|
608
678
|
|
|
@@ -678,7 +748,7 @@ it('submits liveEdit document edits through observable.mutate', async () => {
|
|
|
678
748
|
})
|
|
679
749
|
|
|
680
750
|
it('provides the consistency status via `getDocumentSyncStatus`', async () => {
|
|
681
|
-
const doc = createDocumentHandle({documentId:
|
|
751
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
682
752
|
|
|
683
753
|
const syncStatus = getDocumentSyncStatus(instance, doc)
|
|
684
754
|
expect(syncStatus.getCurrent()).toBeUndefined()
|
|
@@ -733,7 +803,7 @@ it('reverts failed outgoing transaction locally', async () => {
|
|
|
733
803
|
})
|
|
734
804
|
})
|
|
735
805
|
|
|
736
|
-
const documentId = DocumentId(
|
|
806
|
+
const documentId = DocumentId(randomUuid())
|
|
737
807
|
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
738
808
|
|
|
739
809
|
const {getCurrent, subscribe} = getDocumentState(instance, doc)
|
|
@@ -801,7 +871,7 @@ it('removes a queued transaction if it fails to apply', async () => {
|
|
|
801
871
|
})
|
|
802
872
|
})
|
|
803
873
|
|
|
804
|
-
const doc = createDocumentHandle({documentId:
|
|
874
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
805
875
|
const state = getDocumentState(instance, doc)
|
|
806
876
|
const unsubscribe = state.subscribe()
|
|
807
877
|
|
|
@@ -906,8 +976,8 @@ it('fetches dataset ACL and updates grants in the document store state', async (
|
|
|
906
976
|
]
|
|
907
977
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
908
978
|
|
|
909
|
-
const book = createDocumentHandle({documentId:
|
|
910
|
-
const author = createDocumentHandle({documentId:
|
|
979
|
+
const book = createDocumentHandle({documentId: randomUuid(), documentType: 'book'})
|
|
980
|
+
const author = createDocumentHandle({documentId: randomUuid(), documentType: 'author'})
|
|
911
981
|
|
|
912
982
|
expect(await resolvePermissions(instance, {actions: [createDocument(book)]})).toEqual({
|
|
913
983
|
allowed: true,
|
|
@@ -919,12 +989,12 @@ it('fetches dataset ACL and updates grants in the document store state', async (
|
|
|
919
989
|
})
|
|
920
990
|
|
|
921
991
|
it('does not send credentials with the dataset ACL request', async () => {
|
|
922
|
-
const doc = createDocumentHandle({documentId:
|
|
992
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
923
993
|
await resolvePermissions(instance, {actions: [createDocument(doc)]})
|
|
924
994
|
|
|
925
995
|
const aclCall = vi
|
|
926
996
|
.mocked(client.request)
|
|
927
|
-
.mock.calls.find(([options]) => options.
|
|
997
|
+
.mock.calls.find(([options]) => options.url?.endsWith('/acl'))
|
|
928
998
|
expect(aclCall).toBeDefined()
|
|
929
999
|
expect(aclCall![0]).not.toHaveProperty('withCredentials')
|
|
930
1000
|
})
|
|
@@ -942,7 +1012,7 @@ it('retries transient dataset ACL fetch failures instead of failing fatally', as
|
|
|
942
1012
|
}),
|
|
943
1013
|
)
|
|
944
1014
|
|
|
945
|
-
const doc = createDocumentHandle({documentId:
|
|
1015
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
946
1016
|
const result = await resolvePermissions(instance, {actions: [createDocument(doc)]})
|
|
947
1017
|
|
|
948
1018
|
expect(result).toEqual({allowed: true})
|
|
@@ -968,7 +1038,7 @@ it('does not retry dataset ACL fetch failures caused by 4xx client errors', asyn
|
|
|
968
1038
|
}),
|
|
969
1039
|
)
|
|
970
1040
|
|
|
971
|
-
const doc = createDocumentHandle({documentId:
|
|
1041
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
972
1042
|
const documentState = getDocumentState(instance, doc)
|
|
973
1043
|
const unsubscribe = documentState.subscribe()
|
|
974
1044
|
|
|
@@ -1001,7 +1071,7 @@ it('retries dataset ACL fetch failures caused by 429 rate-limit errors', async (
|
|
|
1001
1071
|
}),
|
|
1002
1072
|
)
|
|
1003
1073
|
|
|
1004
|
-
const doc = createDocumentHandle({documentId:
|
|
1074
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
1005
1075
|
const result = await resolvePermissions(instance, {actions: [createDocument(doc)]})
|
|
1006
1076
|
|
|
1007
1077
|
expect(result).toEqual({allowed: true})
|
|
@@ -1018,7 +1088,7 @@ it('fetches ACL for MediaLibraryResource', async () => {
|
|
|
1018
1088
|
const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
|
|
1019
1089
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
1020
1090
|
|
|
1021
|
-
const doc = createDocumentHandle({documentId:
|
|
1091
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
1022
1092
|
const mediaLibraryResource = {mediaLibraryId: 'test-media-library'}
|
|
1023
1093
|
|
|
1024
1094
|
const result = await resolvePermissions(mediaLibraryInstance, {
|
|
@@ -1036,7 +1106,7 @@ it('fetches ACL for CanvasResource', async () => {
|
|
|
1036
1106
|
const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
|
|
1037
1107
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
1038
1108
|
|
|
1039
|
-
const doc = createDocumentHandle({documentId:
|
|
1109
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
1040
1110
|
const canvasResource = {canvasId: 'test-canvas'}
|
|
1041
1111
|
|
|
1042
1112
|
const result = await resolvePermissions(canvasInstance, {
|
|
@@ -1049,7 +1119,7 @@ it('fetches ACL for CanvasResource', async () => {
|
|
|
1049
1119
|
})
|
|
1050
1120
|
|
|
1051
1121
|
it('returns a promise that resolves when a document has been loaded in the store (useful for suspense)', async () => {
|
|
1052
|
-
const documentId = DocumentId(
|
|
1122
|
+
const documentId = DocumentId(randomUuid())
|
|
1053
1123
|
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
1054
1124
|
|
|
1055
1125
|
expect(await resolveDocument(instance, doc)).toBe(null)
|
|
@@ -1074,7 +1144,7 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
1074
1144
|
const handler = vi.fn()
|
|
1075
1145
|
const unsubscribe = subscribeDocumentEvents(instance, {resource, eventHandler: handler})
|
|
1076
1146
|
|
|
1077
|
-
const documentId = DocumentId(
|
|
1147
|
+
const documentId = DocumentId(randomUuid())
|
|
1078
1148
|
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
1079
1149
|
expect(handler).toHaveBeenCalledTimes(0)
|
|
1080
1150
|
|
|
@@ -1505,7 +1575,7 @@ beforeEach(() => {
|
|
|
1505
1575
|
mutations: Mutation[],
|
|
1506
1576
|
options: BaseMutationOptions = {},
|
|
1507
1577
|
): Promise<MultipleMutationResult> => {
|
|
1508
|
-
const transactionId = options.transactionId ??
|
|
1578
|
+
const transactionId = options.transactionId ?? randomUuid()
|
|
1509
1579
|
const timestamp = new Date().toISOString()
|
|
1510
1580
|
const prior = {...documents}
|
|
1511
1581
|
let next = {...documents}
|
|
@@ -1578,7 +1648,7 @@ beforeEach(() => {
|
|
|
1578
1648
|
const action = vi.fn(
|
|
1579
1649
|
async (
|
|
1580
1650
|
input: HttpAction | HttpAction[],
|
|
1581
|
-
{transactionId =
|
|
1651
|
+
{transactionId = randomUuid(), dryRun}: BaseActionOptions,
|
|
1582
1652
|
): Promise<SingleActionResult | MultipleActionResult> => {
|
|
1583
1653
|
const actions = Array.isArray(input) ? input : [input]
|
|
1584
1654
|
let next: DocumentSet = {...documents}
|
|
@@ -550,13 +550,13 @@ const subscribeToClientAndFetchDatasetAcl = ({
|
|
|
550
550
|
}: StoreContext<DocumentStoreState, BoundResourceKey>) => {
|
|
551
551
|
const clientOptions: ClientOptions = {apiVersion: API_VERSION, resource}
|
|
552
552
|
|
|
553
|
-
let
|
|
553
|
+
let url: string
|
|
554
554
|
if (resource && isDatasetResource(resource)) {
|
|
555
|
-
|
|
555
|
+
url = `/projects/${resource.projectId}/datasets/${resource.dataset}/acl`
|
|
556
556
|
} else if (resource && isMediaLibraryResource(resource)) {
|
|
557
|
-
|
|
557
|
+
url = `/media-libraries/${resource.mediaLibraryId}/acl`
|
|
558
558
|
} else if (resource && isCanvasResource(resource)) {
|
|
559
|
-
|
|
559
|
+
url = `/canvases/${resource.canvasId}/acl`
|
|
560
560
|
} else {
|
|
561
561
|
throw new Error(`Received invalid resource: ${JSON.stringify(resource)}`)
|
|
562
562
|
}
|
|
@@ -566,7 +566,7 @@ const subscribeToClientAndFetchDatasetAcl = ({
|
|
|
566
566
|
switchMap((client) =>
|
|
567
567
|
client.observable
|
|
568
568
|
.request<DatasetAcl>({
|
|
569
|
-
|
|
569
|
+
url,
|
|
570
570
|
tag: 'acl.get',
|
|
571
571
|
})
|
|
572
572
|
.pipe(
|
|
@@ -5,6 +5,7 @@ import {createSelector} from 'reselect'
|
|
|
5
5
|
|
|
6
6
|
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
7
7
|
import {type SelectorContext} from '../store/createStateSourceAction'
|
|
8
|
+
import {randomId} from '../utils/ids'
|
|
8
9
|
import {MultiKeyWeakMap} from '../utils/MultiKeyWeakMap'
|
|
9
10
|
import {type DocumentAction} from './actions'
|
|
10
11
|
import {ActionError, PermissionActionError, processActions} from './processActions/processActions'
|
|
@@ -190,7 +191,7 @@ const _calculatePermissions = createSelector(
|
|
|
190
191
|
try {
|
|
191
192
|
processActions({
|
|
192
193
|
actions,
|
|
193
|
-
transactionId:
|
|
194
|
+
transactionId: randomId(16),
|
|
194
195
|
working: documents,
|
|
195
196
|
base: documents,
|
|
196
197
|
timestamp,
|
|
@@ -2,6 +2,7 @@ import {type CreateMutation, type Reference, type SanityDocument} from '@sanity/
|
|
|
2
2
|
import {parse} from 'groq-js'
|
|
3
3
|
import {describe, expect, it} from 'vitest'
|
|
4
4
|
|
|
5
|
+
import {randomUuid} from '../utils/ids'
|
|
5
6
|
import {type Action, type DocumentAction} from './actions'
|
|
6
7
|
import {ActionError, processActions} from './processActions/processActions'
|
|
7
8
|
import {type DocumentSet, processMutations} from './processMutations'
|
|
@@ -973,7 +974,7 @@ describe('processActions', () => {
|
|
|
973
974
|
})
|
|
974
975
|
|
|
975
976
|
it('should strengthen `_strengthenOnPublish` references', () => {
|
|
976
|
-
const _ref =
|
|
977
|
+
const _ref = randomUuid()
|
|
977
978
|
const referenceToStrength: Reference = {
|
|
978
979
|
_ref,
|
|
979
980
|
_type: 'reference',
|