@sanity/sdk 2.9.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks-dts/utils.d.ts +295 -69
- package/dist/_chunks-es/_internal.js +3 -14
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +129 -59
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/version.js +1 -1
- package/dist/_exports/_internal.d.ts +16 -2
- package/dist/_exports/_internal.js +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +275 -149
- package/dist/index.js.map +1 -1
- package/package.json +11 -15
- package/src/_exports/_internal.ts +1 -0
- package/src/_exports/index.ts +33 -2
- package/src/agent/agentActions.ts +21 -25
- package/src/client/clientStore.test.ts +24 -60
- package/src/client/clientStore.ts +49 -56
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/node/actions/getOrCreateNode.test.ts +5 -2
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +3 -3
- package/src/config/sanityConfig.ts +72 -13
- package/src/document/applyDocumentActions.test.ts +7 -7
- package/src/document/applyDocumentActions.ts +5 -5
- package/src/document/documentStore.test.ts +68 -62
- package/src/document/documentStore.ts +33 -38
- package/src/document/processActions.ts +2 -2
- package/src/document/reducers.ts +4 -4
- package/src/document/sharedListener.ts +5 -7
- package/src/organization/organization.test-d.ts +102 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +166 -0
- package/src/organizations/organizations.test-d.ts +77 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +132 -0
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +13 -1
- package/src/presence/presenceStore.test.ts +101 -5
- package/src/presence/presenceStore.ts +96 -24
- package/src/preview/getPreviewState.ts +1 -1
- package/src/preview/previewProjectionUtils.test.ts +4 -4
- package/src/preview/previewProjectionUtils.ts +6 -7
- package/src/preview/resolvePreview.ts +5 -1
- package/src/project/project.test-d.ts +93 -0
- package/src/project/project.test.ts +108 -10
- package/src/project/project.ts +152 -26
- package/src/projection/getProjectionState.ts +4 -4
- package/src/projection/projectionStore.test.ts +2 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +11 -15
- package/src/projects/projects.test-d.ts +38 -0
- package/src/projects/projects.test.ts +104 -38
- package/src/projects/projects.ts +74 -14
- package/src/query/queryStore.test.ts +12 -12
- package/src/query/queryStore.ts +10 -11
- package/src/query/reducers.ts +3 -3
- package/src/releases/getPerspectiveState.ts +5 -5
- package/src/releases/releasesStore.test.ts +6 -6
- package/src/releases/releasesStore.ts +9 -9
- package/src/store/createActionBinder.test.ts +31 -31
- package/src/store/createActionBinder.ts +43 -38
- package/src/store/createSanityInstance.ts +5 -6
- package/src/telemetry/devMode.test.ts +8 -0
- package/src/telemetry/devMode.ts +10 -9
- package/src/telemetry/initTelemetry.test.ts +0 -17
- package/src/telemetry/initTelemetry.ts +2 -12
- package/src/users/reducers.ts +3 -4
- package/src/utils/createFetcherStore.ts +6 -4
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
|
@@ -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, resolveOrganizations} 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 organizations = [{id: 'org_1'}, {id: 'org_2'}]
|
|
25
|
+
const request = vi.fn().mockReturnValue(of(organizations))
|
|
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 resolveOrganizations(instance)
|
|
36
|
+
expect(result).toEqual(organizations)
|
|
37
|
+
expect(request).toHaveBeenCalledWith({
|
|
38
|
+
uri: '/organizations',
|
|
39
|
+
query: {
|
|
40
|
+
includeImplicitMemberships: 'false',
|
|
41
|
+
includeMembers: 'false',
|
|
42
|
+
includeFeatures: 'false',
|
|
43
|
+
},
|
|
44
|
+
tag: 'organizations.get',
|
|
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 resolveOrganizations(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.get',
|
|
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,132 @@
|
|
|
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 {type StateSource} from '../store/createStateSourceAction'
|
|
11
|
+
import {createFetcherStore} from '../utils/createFetcherStore'
|
|
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 normalizeOrganizationOptions(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
|
+
normalizeOrganizationOptions(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
|
+
const organizations = createFetcherStore({
|
|
82
|
+
name: 'Organizations',
|
|
83
|
+
getKey: getOrganizationsCacheKey,
|
|
84
|
+
fetcher: (instance) => (options?: OrganizationsOptions<boolean, boolean>) => {
|
|
85
|
+
return getClientState(instance, {
|
|
86
|
+
apiVersion: API_VERSION,
|
|
87
|
+
scope: 'global',
|
|
88
|
+
}).observable.pipe(
|
|
89
|
+
switchMap((client) => {
|
|
90
|
+
const normalized = normalizeOrganizationOptions(options)
|
|
91
|
+
const query = Object.fromEntries(
|
|
92
|
+
Object.entries(normalized)
|
|
93
|
+
.filter(([, value]) => value !== undefined)
|
|
94
|
+
.map(([key, value]) => [key, String(value)]),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return client.observable.request({
|
|
98
|
+
uri: `/organizations`,
|
|
99
|
+
query,
|
|
100
|
+
tag: 'organizations.get',
|
|
101
|
+
})
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Public signature for the organization state source. The conditional generics
|
|
109
|
+
* cannot flow through `BoundStoreAction`, so we declare the signature here
|
|
110
|
+
* and assign the (already-correct) runtime function to it.
|
|
111
|
+
*/
|
|
112
|
+
type GetOrganizationsState = <
|
|
113
|
+
IncludeMembers extends boolean = false,
|
|
114
|
+
IncludeFeatures extends boolean = false,
|
|
115
|
+
>(
|
|
116
|
+
instance: SanityInstance,
|
|
117
|
+
options?: OrganizationsOptions<IncludeMembers, IncludeFeatures>,
|
|
118
|
+
) => StateSource<Organizations<IncludeMembers, IncludeFeatures> | undefined>
|
|
119
|
+
|
|
120
|
+
type ResolveOrganizations = <
|
|
121
|
+
IncludeMembers extends boolean = false,
|
|
122
|
+
IncludeFeatures extends boolean = false,
|
|
123
|
+
>(
|
|
124
|
+
instance: SanityInstance,
|
|
125
|
+
options?: OrganizationsOptions<IncludeMembers, IncludeFeatures>,
|
|
126
|
+
) => Promise<Organizations<IncludeMembers, IncludeFeatures>>
|
|
127
|
+
|
|
128
|
+
/** @public */
|
|
129
|
+
export const getOrganizationsState: GetOrganizationsState = organizations.getState
|
|
130
|
+
|
|
131
|
+
/** @public */
|
|
132
|
+
export const resolveOrganizations: ResolveOrganizations = organizations.resolveState
|
|
@@ -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$)
|
|
@@ -37,11 +37,23 @@ type IncomingBifurEvent = RollCallEvent | BifurStateMessage | BifurDisconnectMes
|
|
|
37
37
|
function getBifurClient(client: SanityClient, token$: Observable<string | null>): BifurClient {
|
|
38
38
|
const bifurVersionedClient = client.withConfig({apiVersion: '2022-06-30'})
|
|
39
39
|
const {
|
|
40
|
+
resource,
|
|
40
41
|
dataset,
|
|
41
42
|
url: baseUrl,
|
|
42
43
|
requestTagPrefix = 'sanity.sdk.presence',
|
|
43
44
|
} = bifurVersionedClient.config()
|
|
44
|
-
|
|
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')
|
|
45
57
|
|
|
46
58
|
const urlWithTag = `${url}?tag=${requestTagPrefix}`
|
|
47
59
|
|
|
@@ -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>()
|
|
@@ -243,5 +246,98 @@ describe('presenceStore', () => {
|
|
|
243
246
|
|
|
244
247
|
unsubscribe()
|
|
245
248
|
})
|
|
249
|
+
|
|
250
|
+
it('should throw an error when initialized with a media library resource', () => {
|
|
251
|
+
const mediaLibraryResource = {mediaLibraryId: 'ml123'}
|
|
252
|
+
|
|
253
|
+
expect(() => {
|
|
254
|
+
getPresence(instance, {resource: mediaLibraryResource})
|
|
255
|
+
}).toThrow('Presence is not supported for media library resources.')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should work with a dataset resource', () => {
|
|
259
|
+
const datasetResource = {projectId: 'test-project', dataset: 'test-dataset'}
|
|
260
|
+
|
|
261
|
+
expect(() => {
|
|
262
|
+
getPresence(instance, {resource: datasetResource})
|
|
263
|
+
}).not.toThrow()
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should work with a canvas resource', () => {
|
|
267
|
+
const canvasResource = {canvasId: 'canvas123'}
|
|
268
|
+
|
|
269
|
+
expect(() => {
|
|
270
|
+
getPresence(instance, {resource: canvasResource})
|
|
271
|
+
}).not.toThrow()
|
|
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
|
+
})
|
|
246
342
|
})
|
|
247
343
|
})
|