@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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {of} from 'rxjs'
|
|
3
|
+
import {afterEach, beforeEach, describe, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {getClientState} from '../client/clientStore'
|
|
6
|
+
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
|
+
import {getOrganizationsCacheKey, organizations} from './organizations'
|
|
9
|
+
|
|
10
|
+
vi.mock('../client/clientStore')
|
|
11
|
+
|
|
12
|
+
describe('organizations', () => {
|
|
13
|
+
let instance: SanityInstance
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
instance.dispose()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('calls `client.observable.request` against `/organizations` and returns the result', async () => {
|
|
24
|
+
const list = [{id: 'org_1'}, {id: 'org_2'}]
|
|
25
|
+
const request = vi.fn().mockReturnValue(of(list))
|
|
26
|
+
|
|
27
|
+
const mockClient = {
|
|
28
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
29
|
+
} as SanityClient
|
|
30
|
+
|
|
31
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
32
|
+
observable: of(mockClient),
|
|
33
|
+
} as StateSource<SanityClient>)
|
|
34
|
+
|
|
35
|
+
const result = await organizations.resolveState(instance)
|
|
36
|
+
expect(result).toEqual(list)
|
|
37
|
+
expect(request).toHaveBeenCalledWith({
|
|
38
|
+
uri: '/organizations',
|
|
39
|
+
query: {
|
|
40
|
+
includeImplicitMemberships: 'false',
|
|
41
|
+
includeMembers: 'false',
|
|
42
|
+
includeFeatures: 'false',
|
|
43
|
+
},
|
|
44
|
+
tag: 'organizations.list',
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('serializes query params (booleans → strings) and respects flags', async () => {
|
|
49
|
+
const request = vi.fn().mockReturnValue(of([]))
|
|
50
|
+
const mockClient = {
|
|
51
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
52
|
+
} as SanityClient
|
|
53
|
+
|
|
54
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
55
|
+
observable: of(mockClient),
|
|
56
|
+
} as StateSource<SanityClient>)
|
|
57
|
+
|
|
58
|
+
await organizations.resolveState(instance, {
|
|
59
|
+
includeMembers: true,
|
|
60
|
+
includeFeatures: true,
|
|
61
|
+
includeImplicitMemberships: true,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(request).toHaveBeenCalledWith({
|
|
65
|
+
uri: '/organizations',
|
|
66
|
+
query: {
|
|
67
|
+
includeImplicitMemberships: 'true',
|
|
68
|
+
includeMembers: 'true',
|
|
69
|
+
includeFeatures: 'true',
|
|
70
|
+
},
|
|
71
|
+
tag: 'organizations.list',
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('organizations cache key generation', () => {
|
|
77
|
+
let instance: SanityInstance
|
|
78
|
+
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
instance = createSanityInstance({})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
instance.dispose()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('default call excludes all segments (all flags default-false)', () => {
|
|
88
|
+
expect(getOrganizationsCacheKey(instance)).toBe('organizations')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('treats undefined and the matching default as the same key', () => {
|
|
92
|
+
expect(getOrganizationsCacheKey(instance)).toBe(
|
|
93
|
+
getOrganizationsCacheKey(instance, {
|
|
94
|
+
includeMembers: false,
|
|
95
|
+
includeFeatures: false,
|
|
96
|
+
includeImplicitMemberships: false,
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('explicit includeMembers: true appends :members', () => {
|
|
102
|
+
expect(getOrganizationsCacheKey(instance, {includeMembers: true})).toBe('organizations:members')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('explicit includeFeatures: true appends :features', () => {
|
|
106
|
+
expect(getOrganizationsCacheKey(instance, {includeFeatures: true})).toBe(
|
|
107
|
+
'organizations:features',
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('explicit includeImplicitMemberships: true appends :implicit', () => {
|
|
112
|
+
expect(getOrganizationsCacheKey(instance, {includeImplicitMemberships: true})).toBe(
|
|
113
|
+
'organizations:implicit',
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('combines all segments in order', () => {
|
|
118
|
+
expect(
|
|
119
|
+
getOrganizationsCacheKey(instance, {
|
|
120
|
+
includeMembers: true,
|
|
121
|
+
includeFeatures: true,
|
|
122
|
+
includeImplicitMemberships: true,
|
|
123
|
+
}),
|
|
124
|
+
).toBe('organizations:members:features:implicit')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('produces distinct keys for each meaningful option permutation', () => {
|
|
128
|
+
const keys = new Set([
|
|
129
|
+
getOrganizationsCacheKey(instance),
|
|
130
|
+
getOrganizationsCacheKey(instance, {includeMembers: true}),
|
|
131
|
+
getOrganizationsCacheKey(instance, {includeFeatures: true}),
|
|
132
|
+
getOrganizationsCacheKey(instance, {includeImplicitMemberships: true}),
|
|
133
|
+
getOrganizationsCacheKey(instance, {includeMembers: true, includeFeatures: true}),
|
|
134
|
+
getOrganizationsCacheKey(instance, {
|
|
135
|
+
includeMembers: true,
|
|
136
|
+
includeImplicitMemberships: true,
|
|
137
|
+
}),
|
|
138
|
+
getOrganizationsCacheKey(instance, {
|
|
139
|
+
includeFeatures: true,
|
|
140
|
+
includeImplicitMemberships: true,
|
|
141
|
+
}),
|
|
142
|
+
getOrganizationsCacheKey(instance, {
|
|
143
|
+
includeMembers: true,
|
|
144
|
+
includeFeatures: true,
|
|
145
|
+
includeImplicitMemberships: true,
|
|
146
|
+
}),
|
|
147
|
+
])
|
|
148
|
+
expect(keys.size).toBe(8)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {switchMap} from 'rxjs'
|
|
2
|
+
|
|
3
|
+
import {getClientState} from '../client/clientStore'
|
|
4
|
+
import {
|
|
5
|
+
type OrganizationBase,
|
|
6
|
+
type OrganizationMember,
|
|
7
|
+
type OrganizationOptions,
|
|
8
|
+
} from '../organization/organization'
|
|
9
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
10
|
+
import {defineFetcher} from '../store/fetcherStore'
|
|
11
|
+
import {buildQuery} from '../utils/buildQuery'
|
|
12
|
+
|
|
13
|
+
const API_VERSION = 'v2025-02-19'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The list shape returned from `/organizations`, with `members` and/or
|
|
17
|
+
* `features` conditionally included based on the query options used.
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
export type Organizations<
|
|
21
|
+
IncludeMembers extends boolean = false,
|
|
22
|
+
IncludeFeatures extends boolean = false,
|
|
23
|
+
> = (Pick<
|
|
24
|
+
OrganizationBase,
|
|
25
|
+
| 'id'
|
|
26
|
+
| 'name'
|
|
27
|
+
| 'slug'
|
|
28
|
+
| 'createdAt'
|
|
29
|
+
| 'updatedAt'
|
|
30
|
+
| 'defaultRoleName'
|
|
31
|
+
| 'dashboardStatus'
|
|
32
|
+
| 'aiFeaturesStatus'
|
|
33
|
+
> &
|
|
34
|
+
// `boolean extends T` is non-distributive — true only when T is the wide
|
|
35
|
+
// `boolean`, in which case the field is optional. Literal `true`/`false`
|
|
36
|
+
// fall through to the strict branch.
|
|
37
|
+
(boolean extends IncludeMembers
|
|
38
|
+
? {members?: OrganizationMember[]}
|
|
39
|
+
: IncludeMembers extends true
|
|
40
|
+
? {members: OrganizationMember[]}
|
|
41
|
+
: unknown) &
|
|
42
|
+
(boolean extends IncludeFeatures
|
|
43
|
+
? {features?: string[]}
|
|
44
|
+
: IncludeFeatures extends true
|
|
45
|
+
? {features: string[]}
|
|
46
|
+
: unknown))[]
|
|
47
|
+
|
|
48
|
+
/** @public */
|
|
49
|
+
export interface OrganizationsOptions<
|
|
50
|
+
IncludeMembers extends boolean = false,
|
|
51
|
+
IncludeFeatures extends boolean = false,
|
|
52
|
+
> extends Omit<OrganizationOptions<IncludeMembers, IncludeFeatures>, 'organizationId'> {
|
|
53
|
+
/**
|
|
54
|
+
* When `true`, includes organisations the user has access to via
|
|
55
|
+
* project-level grants, not just direct organisation memberships.
|
|
56
|
+
*/
|
|
57
|
+
includeImplicitMemberships?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeOrganizationsOptions(options?: OrganizationsOptions<boolean, boolean>) {
|
|
61
|
+
return {
|
|
62
|
+
includeImplicitMemberships: options?.includeImplicitMemberships ?? false,
|
|
63
|
+
includeMembers: options?.includeMembers ?? false,
|
|
64
|
+
includeFeatures: options?.includeFeatures ?? false,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @internal */
|
|
69
|
+
export function getOrganizationsCacheKey(
|
|
70
|
+
_instance: SanityInstance,
|
|
71
|
+
options?: OrganizationsOptions<boolean, boolean>,
|
|
72
|
+
): string {
|
|
73
|
+
const {includeMembers, includeFeatures, includeImplicitMemberships} =
|
|
74
|
+
normalizeOrganizationsOptions(options)
|
|
75
|
+
const membersKey = includeMembers ? ':members' : ''
|
|
76
|
+
const featuresKey = includeFeatures ? ':features' : ''
|
|
77
|
+
const implicitKey = includeImplicitMemberships ? ':implicit' : ''
|
|
78
|
+
return `organizations${membersKey}${featuresKey}${implicitKey}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fetcher for the current user's organizations (`GET /organizations`), on the
|
|
83
|
+
* shared fetcher cache.
|
|
84
|
+
*
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
export const organizations = defineFetcher<
|
|
88
|
+
[options?: OrganizationsOptions<boolean, boolean>],
|
|
89
|
+
Organizations<boolean, boolean>
|
|
90
|
+
>({
|
|
91
|
+
name: 'organizations',
|
|
92
|
+
getKey: getOrganizationsCacheKey,
|
|
93
|
+
fetch: (instance) => (options) =>
|
|
94
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
95
|
+
switchMap((client) =>
|
|
96
|
+
client.observable.request<Organizations<boolean, boolean>>({
|
|
97
|
+
uri: '/organizations',
|
|
98
|
+
query: buildQuery(normalizeOrganizationsOptions(options)),
|
|
99
|
+
tag: 'organizations.list',
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
tags: (data) => [
|
|
104
|
+
{type: 'organization', id: 'LIST'},
|
|
105
|
+
...data.map((org) => ({type: 'organization', id: org.id})),
|
|
106
|
+
],
|
|
107
|
+
})
|
|
@@ -45,16 +45,18 @@ describe('createBifurTransport', () => {
|
|
|
45
45
|
|
|
46
46
|
beforeEach(() => {
|
|
47
47
|
vi.useFakeTimers()
|
|
48
|
+
vi.clearAllMocks()
|
|
48
49
|
mockBifurClient = {
|
|
49
50
|
listen: vi.fn(() => new Subject<never>()),
|
|
50
51
|
request: vi.fn(() => of(undefined)),
|
|
51
52
|
}
|
|
52
53
|
fromUrlMock.mockReturnValue(mockBifurClient)
|
|
53
54
|
|
|
55
|
+
// Default mock is a dataset client using project hostname
|
|
54
56
|
mockSanityClient = {
|
|
55
57
|
config: () => ({
|
|
56
58
|
dataset: 'test-dataset',
|
|
57
|
-
url: '
|
|
59
|
+
url: 'https://test-project.api.sanity.io/v2022-06-30',
|
|
58
60
|
requestTagPrefix: 'test-tag',
|
|
59
61
|
}),
|
|
60
62
|
withConfig: vi.fn().mockReturnThis(),
|
|
@@ -63,7 +65,7 @@ describe('createBifurTransport', () => {
|
|
|
63
65
|
token$ = new Subject<string | null>()
|
|
64
66
|
})
|
|
65
67
|
|
|
66
|
-
it('constructs the bifur client
|
|
68
|
+
it('constructs the bifur client URL for a dataset resource', () => {
|
|
67
69
|
createBifurTransport({
|
|
68
70
|
client: mockSanityClient,
|
|
69
71
|
token$,
|
|
@@ -71,13 +73,51 @@ describe('createBifurTransport', () => {
|
|
|
71
73
|
})
|
|
72
74
|
|
|
73
75
|
expect(fromUrlMock).toHaveBeenCalledWith(
|
|
74
|
-
'
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
'wss://test-project.api.sanity.io/v2022-06-30/socket/test-dataset?tag=test-tag',
|
|
77
|
+
{token$},
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('constructs the bifur client URL for a canvas resource', () => {
|
|
82
|
+
const canvasClient = {
|
|
83
|
+
config: () => ({
|
|
84
|
+
resource: {type: 'canvas', id: 'canvas-123'},
|
|
85
|
+
url: 'https://api.sanity.io/v2022-06-30',
|
|
86
|
+
requestTagPrefix: 'test-tag',
|
|
87
|
+
}),
|
|
88
|
+
withConfig: vi.fn().mockReturnThis(),
|
|
89
|
+
} as unknown as SanityClient
|
|
90
|
+
|
|
91
|
+
createBifurTransport({
|
|
92
|
+
client: canvasClient,
|
|
93
|
+
token$,
|
|
94
|
+
sessionId: 'session-id-123',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
expect(fromUrlMock).toHaveBeenCalledWith(
|
|
98
|
+
'wss://api.sanity.io/v2022-06-30/socket/canvases/canvas-123?tag=test-tag',
|
|
99
|
+
{token$},
|
|
78
100
|
)
|
|
79
101
|
})
|
|
80
102
|
|
|
103
|
+
it('throws when no canvas resource or dataset is configured', () => {
|
|
104
|
+
const invalidClient = {
|
|
105
|
+
config: () => ({
|
|
106
|
+
url: 'https://api.sanity.io/v2022-06-30',
|
|
107
|
+
requestTagPrefix: 'test-tag',
|
|
108
|
+
}),
|
|
109
|
+
withConfig: vi.fn().mockReturnThis(),
|
|
110
|
+
} as unknown as SanityClient
|
|
111
|
+
|
|
112
|
+
expect(() =>
|
|
113
|
+
createBifurTransport({
|
|
114
|
+
client: invalidClient,
|
|
115
|
+
token$,
|
|
116
|
+
sessionId: 'session-id-123',
|
|
117
|
+
}),
|
|
118
|
+
).toThrow('Unable to determine presence URL')
|
|
119
|
+
})
|
|
120
|
+
|
|
81
121
|
it('handles incoming rollCall events', () => {
|
|
82
122
|
const incomingBifurEvents$ = new Subject<IncomingBifurEvent>()
|
|
83
123
|
mockBifurClient.listen.mockReturnValue(incomingBifurEvents$)
|
|
@@ -36,8 +36,25 @@ type IncomingBifurEvent = RollCallEvent | BifurStateMessage | BifurDisconnectMes
|
|
|
36
36
|
|
|
37
37
|
function getBifurClient(client: SanityClient, token$: Observable<string | null>): BifurClient {
|
|
38
38
|
const bifurVersionedClient = client.withConfig({apiVersion: '2022-06-30'})
|
|
39
|
-
const {
|
|
40
|
-
|
|
39
|
+
const {
|
|
40
|
+
resource,
|
|
41
|
+
dataset,
|
|
42
|
+
url: baseUrl,
|
|
43
|
+
requestTagPrefix = 'sanity.sdk.presence',
|
|
44
|
+
} = bifurVersionedClient.config()
|
|
45
|
+
|
|
46
|
+
let resourcePath: string
|
|
47
|
+
if (resource?.type === 'canvas') {
|
|
48
|
+
resourcePath = `canvases/${resource.id}`
|
|
49
|
+
} else if (dataset) {
|
|
50
|
+
// Dataset clients use project hostname — dataset name alone is the socket path
|
|
51
|
+
resourcePath = dataset
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error(`Unable to determine presence URL: no canvas resource or dataset configured`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const url = `${baseUrl}/socket/${resourcePath}`.replace(/^http/, 'ws')
|
|
57
|
+
|
|
41
58
|
const urlWithTag = `${url}?tag=${requestTagPrefix}`
|
|
42
59
|
|
|
43
60
|
return fromUrl(urlWithTag, {token$})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {type SanityClient} from '@sanity/client'
|
|
2
|
-
import {delay, firstValueFrom, of, Subject} from 'rxjs'
|
|
3
|
-
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
import {delay, firstValueFrom, type Observable, of, Subject} from 'rxjs'
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {getTokenState} from '../auth/authStore'
|
|
6
6
|
import {getClient} from '../client/clientStore'
|
|
@@ -9,7 +9,7 @@ import {type SanityUser} from '../users/types'
|
|
|
9
9
|
import {getUserState} from '../users/usersStore'
|
|
10
10
|
import {createBifurTransport} from './bifurTransport'
|
|
11
11
|
import {getPresence} from './presenceStore'
|
|
12
|
-
import {type PresenceLocation, type TransportEvent} from './types'
|
|
12
|
+
import {type PresenceLocation, type TransportEvent, type TransportMessage} from './types'
|
|
13
13
|
|
|
14
14
|
vi.mock('../auth/authStore')
|
|
15
15
|
vi.mock('../client/clientStore')
|
|
@@ -21,8 +21,8 @@ describe('presenceStore', () => {
|
|
|
21
21
|
let mockClient: SanityClient
|
|
22
22
|
let mockTokenState: Subject<string | null>
|
|
23
23
|
let mockIncomingEvents: Subject<TransportEvent>
|
|
24
|
-
let mockDispatchMessage:
|
|
25
|
-
let mockGetUserState:
|
|
24
|
+
let mockDispatchMessage: Mock<(message: TransportMessage) => Observable<void>>
|
|
25
|
+
let mockGetUserState: Mock<typeof getUserState>
|
|
26
26
|
|
|
27
27
|
const mockUser: SanityUser = {
|
|
28
28
|
sanityUserId: 'u123',
|
|
@@ -48,6 +48,9 @@ describe('presenceStore', () => {
|
|
|
48
48
|
|
|
49
49
|
mockClient = {
|
|
50
50
|
withConfig: vi.fn().mockReturnThis(),
|
|
51
|
+
observable: {
|
|
52
|
+
request: vi.fn(() => of({organizationId: 'test-org-id'})),
|
|
53
|
+
},
|
|
51
54
|
} as unknown as SanityClient
|
|
52
55
|
|
|
53
56
|
mockTokenState = new Subject<string | null>()
|
|
@@ -69,7 +72,7 @@ describe('presenceStore', () => {
|
|
|
69
72
|
mockGetUserState = vi.fn(() => of(mockUser))
|
|
70
73
|
vi.mocked(getUserState).mockImplementation(mockGetUserState)
|
|
71
74
|
|
|
72
|
-
instance = createSanityInstance()
|
|
75
|
+
instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
|
|
73
76
|
})
|
|
74
77
|
|
|
75
78
|
afterEach(() => {
|
|
@@ -77,9 +80,8 @@ describe('presenceStore', () => {
|
|
|
77
80
|
})
|
|
78
81
|
|
|
79
82
|
describe('getPresence', () => {
|
|
80
|
-
const key = {resource: {projectId: 'test-project', dataset: 'test-dataset'}}
|
|
81
83
|
it('creates bifur transport with correct parameters', () => {
|
|
82
|
-
getPresence(instance
|
|
84
|
+
getPresence(instance)
|
|
83
85
|
|
|
84
86
|
expect(createBifurTransport).toHaveBeenCalledWith({
|
|
85
87
|
client: mockClient,
|
|
@@ -89,18 +91,18 @@ describe('presenceStore', () => {
|
|
|
89
91
|
})
|
|
90
92
|
|
|
91
93
|
it('sends rollCall message on initialization', () => {
|
|
92
|
-
getPresence(instance
|
|
94
|
+
getPresence(instance)
|
|
93
95
|
|
|
94
96
|
expect(mockDispatchMessage).toHaveBeenCalledWith({type: 'rollCall'})
|
|
95
97
|
})
|
|
96
98
|
|
|
97
99
|
it('returns empty array when no users present', () => {
|
|
98
|
-
const source = getPresence(instance
|
|
100
|
+
const source = getPresence(instance)
|
|
99
101
|
expect(source.getCurrent()).toEqual([])
|
|
100
102
|
})
|
|
101
103
|
|
|
102
104
|
it('handles state events from other users', async () => {
|
|
103
|
-
const source = getPresence(instance
|
|
105
|
+
const source = getPresence(instance)
|
|
104
106
|
|
|
105
107
|
// Subscribe to initialize the store
|
|
106
108
|
const unsubscribe = source.subscribe(() => {})
|
|
@@ -137,7 +139,7 @@ describe('presenceStore', () => {
|
|
|
137
139
|
})
|
|
138
140
|
|
|
139
141
|
it('ignores events from own session', async () => {
|
|
140
|
-
const source = getPresence(instance
|
|
142
|
+
const source = getPresence(instance)
|
|
141
143
|
const unsubscribe = source.subscribe(() => {})
|
|
142
144
|
|
|
143
145
|
await firstValueFrom(of(null).pipe(delay(10)))
|
|
@@ -159,7 +161,7 @@ describe('presenceStore', () => {
|
|
|
159
161
|
})
|
|
160
162
|
|
|
161
163
|
it('handles disconnect events', async () => {
|
|
162
|
-
const source = getPresence(instance
|
|
164
|
+
const source = getPresence(instance)
|
|
163
165
|
const unsubscribe = source.subscribe(() => {})
|
|
164
166
|
|
|
165
167
|
await firstValueFrom(of(null).pipe(delay(10)))
|
|
@@ -191,7 +193,7 @@ describe('presenceStore', () => {
|
|
|
191
193
|
})
|
|
192
194
|
|
|
193
195
|
it('fetches user data for present users', async () => {
|
|
194
|
-
const source = getPresence(instance
|
|
196
|
+
const source = getPresence(instance)
|
|
195
197
|
const unsubscribe = source.subscribe(() => {})
|
|
196
198
|
|
|
197
199
|
await firstValueFrom(of(null).pipe(delay(10)))
|
|
@@ -223,7 +225,7 @@ describe('presenceStore', () => {
|
|
|
223
225
|
})
|
|
224
226
|
|
|
225
227
|
it('handles presence events correctly', async () => {
|
|
226
|
-
const source = getPresence(instance
|
|
228
|
+
const source = getPresence(instance)
|
|
227
229
|
const unsubscribe = source.subscribe(() => {})
|
|
228
230
|
|
|
229
231
|
await firstValueFrom(of(null).pipe(delay(10)))
|
|
@@ -250,23 +252,92 @@ describe('presenceStore', () => {
|
|
|
250
252
|
|
|
251
253
|
expect(() => {
|
|
252
254
|
getPresence(instance, {resource: mediaLibraryResource})
|
|
253
|
-
}).toThrow('Presence is not supported for media library resources')
|
|
255
|
+
}).toThrow('Presence is not supported for media library resources.')
|
|
254
256
|
})
|
|
255
257
|
|
|
256
|
-
it('should
|
|
257
|
-
const
|
|
258
|
+
it('should work with a dataset resource', () => {
|
|
259
|
+
const datasetResource = {projectId: 'test-project', dataset: 'test-dataset'}
|
|
258
260
|
|
|
259
261
|
expect(() => {
|
|
260
|
-
getPresence(instance, {resource:
|
|
261
|
-
}).toThrow(
|
|
262
|
+
getPresence(instance, {resource: datasetResource})
|
|
263
|
+
}).not.toThrow()
|
|
262
264
|
})
|
|
263
265
|
|
|
264
|
-
it('should work with a
|
|
265
|
-
const
|
|
266
|
+
it('should work with a canvas resource', () => {
|
|
267
|
+
const canvasResource = {canvasId: 'canvas123'}
|
|
266
268
|
|
|
267
269
|
expect(() => {
|
|
268
|
-
getPresence(instance, {resource:
|
|
270
|
+
getPresence(instance, {resource: canvasResource})
|
|
269
271
|
}).not.toThrow()
|
|
270
272
|
})
|
|
273
|
+
|
|
274
|
+
it('creates a project-hostname client for dataset resources', () => {
|
|
275
|
+
getPresence(instance, {resource: {projectId: 'my-project', dataset: 'my-dataset'}})
|
|
276
|
+
|
|
277
|
+
expect(getClient).toHaveBeenCalledWith(instance, {
|
|
278
|
+
apiVersion: '2026-03-30',
|
|
279
|
+
projectId: 'my-project',
|
|
280
|
+
dataset: 'my-dataset',
|
|
281
|
+
useProjectHostname: true,
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('creates a resource client for canvas resources', () => {
|
|
286
|
+
const canvasResource = {canvasId: 'canvas123'}
|
|
287
|
+
getPresence(instance, {resource: canvasResource})
|
|
288
|
+
|
|
289
|
+
expect(getClient).toHaveBeenCalledWith(instance, {
|
|
290
|
+
apiVersion: '2026-03-30',
|
|
291
|
+
resource: canvasResource,
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('fetches organizationId from canvas endpoint for canvas resources', () => {
|
|
296
|
+
const canvasResource = {canvasId: 'canvas123'}
|
|
297
|
+
getPresence(instance, {resource: canvasResource})
|
|
298
|
+
|
|
299
|
+
expect(mockClient.observable.request).toHaveBeenCalledWith({
|
|
300
|
+
uri: '/canvases/canvas123',
|
|
301
|
+
tag: 'canvases.get',
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('does not fetch organizationId for dataset resources', () => {
|
|
306
|
+
getPresence(instance, {resource: {projectId: 'my-project', dataset: 'my-dataset'}})
|
|
307
|
+
|
|
308
|
+
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('fetches user data for canvas users', async () => {
|
|
312
|
+
const source = getPresence(instance, {resource: {canvasId: 'canvas123'}})
|
|
313
|
+
const unsubscribe = source.subscribe(() => {})
|
|
314
|
+
|
|
315
|
+
await firstValueFrom(of(null).pipe(delay(10)))
|
|
316
|
+
|
|
317
|
+
mockIncomingEvents.next({
|
|
318
|
+
type: 'state',
|
|
319
|
+
userId: 'user-1',
|
|
320
|
+
sessionId: 'other-session',
|
|
321
|
+
timestamp: '2023-01-01T12:00:00Z',
|
|
322
|
+
locations: [
|
|
323
|
+
{
|
|
324
|
+
type: 'document',
|
|
325
|
+
documentId: 'doc-1',
|
|
326
|
+
path: ['title'],
|
|
327
|
+
lastActiveAt: '2023-01-01T12:00:00Z',
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
await firstValueFrom(of(null).pipe(delay(50)))
|
|
333
|
+
|
|
334
|
+
expect(getUserState).toHaveBeenCalledWith(instance, {
|
|
335
|
+
userId: 'user-1',
|
|
336
|
+
resourceType: 'organization',
|
|
337
|
+
organizationId: 'test-org-id',
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
unsubscribe()
|
|
341
|
+
})
|
|
271
342
|
})
|
|
272
343
|
})
|