@sanity/sdk 2.10.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 +200 -28
- package/dist/_chunks-es/_internal.js +3 -14
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +7 -14
- 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 +168 -88
- package/dist/index.js.map +1 -1
- package/package.json +7 -9
- package/src/_exports/_internal.ts +1 -0
- package/src/_exports/index.ts +25 -2
- package/src/agent/agentActions.ts +21 -25
- package/src/client/clientStore.test.ts +10 -46
- package/src/client/clientStore.ts +7 -14
- package/src/comlink/node/actions/getOrCreateNode.test.ts +5 -2
- package/src/comlink/node/actions/releaseNode.test.ts +3 -3
- package/src/config/sanityConfig.ts +0 -1
- package/src/document/documentStore.ts +2 -7
- package/src/document/sharedListener.ts +3 -5
- 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/presenceStore.test.ts +5 -5
- package/src/preview/previewProjectionUtils.ts +2 -3
- 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/subscribeToStateAndFetchBatches.ts +4 -9
- 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.ts +2 -3
- package/src/releases/releasesStore.test.ts +1 -1
- package/src/releases/releasesStore.ts +2 -2
- package/src/store/createSanityInstance.ts +3 -3
- 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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {switchMap} from 'rxjs'
|
|
2
|
+
|
|
3
|
+
import {getClientState} from '../client/clientStore'
|
|
4
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
5
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
6
|
+
import {createFetcherStore} from '../utils/createFetcherStore'
|
|
7
|
+
|
|
8
|
+
const API_VERSION = 'v2025-02-19'
|
|
9
|
+
|
|
10
|
+
/** @public */
|
|
11
|
+
export interface OrganizationMember {
|
|
12
|
+
sanityUserId: string
|
|
13
|
+
isCurrentUser: boolean
|
|
14
|
+
user: {
|
|
15
|
+
id: string
|
|
16
|
+
displayName: string
|
|
17
|
+
familyName: string
|
|
18
|
+
givenName: string
|
|
19
|
+
middleName: string | null
|
|
20
|
+
imageUrl: string | null
|
|
21
|
+
email: string
|
|
22
|
+
loginProvider: string
|
|
23
|
+
}
|
|
24
|
+
roles: Array<{
|
|
25
|
+
name: string
|
|
26
|
+
title: string
|
|
27
|
+
description?: string
|
|
28
|
+
}>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The base fields returned from `/organizations/<id>` for every organization.
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export interface OrganizationBase {
|
|
36
|
+
id: string
|
|
37
|
+
name: string
|
|
38
|
+
slug: string | null
|
|
39
|
+
createdAt: string
|
|
40
|
+
createdByUserId: string
|
|
41
|
+
updatedAt: string
|
|
42
|
+
deletedAt: string | null
|
|
43
|
+
dashboardStatus: 'enabled' | 'disabled'
|
|
44
|
+
aiFeaturesStatus: 'enabled' | 'disabled'
|
|
45
|
+
mediaLibraryStatus: 'enabled' | 'disabled'
|
|
46
|
+
requestAccessStatus: 'allowed' | 'disabled'
|
|
47
|
+
telemetryConsentStatus: 'allowed' | 'msa_denied' | 'customer_denied'
|
|
48
|
+
oauthAppsStatus: 'allowed' | 'blocked'
|
|
49
|
+
defaultRoleName: string
|
|
50
|
+
domains: string[] | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @public */
|
|
54
|
+
export interface OrganizationOptions<
|
|
55
|
+
IncludeMembers extends boolean = false,
|
|
56
|
+
IncludeFeatures extends boolean = false,
|
|
57
|
+
> {
|
|
58
|
+
includeMembers?: IncludeMembers
|
|
59
|
+
includeFeatures?: IncludeFeatures
|
|
60
|
+
organizationId: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* An `Organization` with `members` and/or `features` conditionally included
|
|
65
|
+
* based on the query options used to fetch it.
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export type Organization<
|
|
69
|
+
IncludeMembers extends boolean = false,
|
|
70
|
+
IncludeFeatures extends boolean = false,
|
|
71
|
+
> = OrganizationBase &
|
|
72
|
+
// `boolean extends T` is non-distributive — true only when T is the wide
|
|
73
|
+
// `boolean`, in which case the field is optional. Literal `true`/`false`
|
|
74
|
+
// fall through to the strict branch.
|
|
75
|
+
(boolean extends IncludeMembers
|
|
76
|
+
? {members?: OrganizationMember[]}
|
|
77
|
+
: IncludeMembers extends true
|
|
78
|
+
? {members: OrganizationMember[]}
|
|
79
|
+
: unknown) &
|
|
80
|
+
(boolean extends IncludeFeatures
|
|
81
|
+
? {features?: string[]}
|
|
82
|
+
: IncludeFeatures extends true
|
|
83
|
+
? {features: string[]}
|
|
84
|
+
: unknown)
|
|
85
|
+
|
|
86
|
+
function resolveOrganizationId(options?: OrganizationOptions<boolean, boolean>) {
|
|
87
|
+
const organizationId = options?.organizationId
|
|
88
|
+
if (!organizationId) {
|
|
89
|
+
throw new Error('An organizationId is required to use the organization API.')
|
|
90
|
+
}
|
|
91
|
+
return organizationId
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeOrganizationOptions(options?: OrganizationOptions<boolean, boolean>) {
|
|
95
|
+
return {
|
|
96
|
+
includeMembers: options?.includeMembers ?? false,
|
|
97
|
+
includeFeatures: options?.includeFeatures ?? false,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @internal */
|
|
102
|
+
export function getOrganizationCacheKey(
|
|
103
|
+
_instance: SanityInstance,
|
|
104
|
+
options?: OrganizationOptions<boolean, boolean>,
|
|
105
|
+
): string {
|
|
106
|
+
const organizationId = resolveOrganizationId(options)
|
|
107
|
+
const {includeMembers, includeFeatures} = normalizeOrganizationOptions(options)
|
|
108
|
+
const membersKey = includeMembers ? ':members' : ''
|
|
109
|
+
const featuresKey = includeFeatures ? ':features' : ''
|
|
110
|
+
return `organization:${organizationId}${membersKey}${featuresKey}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const organization = createFetcherStore({
|
|
114
|
+
name: 'Organization',
|
|
115
|
+
getKey: getOrganizationCacheKey,
|
|
116
|
+
fetcher: (instance) => (options?: OrganizationOptions<boolean, boolean>) => {
|
|
117
|
+
const organizationId = resolveOrganizationId(options)
|
|
118
|
+
|
|
119
|
+
return getClientState(instance, {
|
|
120
|
+
apiVersion: API_VERSION,
|
|
121
|
+
scope: 'global',
|
|
122
|
+
}).observable.pipe(
|
|
123
|
+
switchMap((client) => {
|
|
124
|
+
const normalized = normalizeOrganizationOptions(options)
|
|
125
|
+
const query = Object.fromEntries(
|
|
126
|
+
Object.entries(normalized)
|
|
127
|
+
.filter(([, value]) => value !== undefined)
|
|
128
|
+
.map(([key, value]) => [key, String(value)]),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return client.observable.request({
|
|
132
|
+
uri: `/organizations/${organizationId}`,
|
|
133
|
+
query,
|
|
134
|
+
tag: 'organization.get',
|
|
135
|
+
})
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Public signature for the organization state source. The conditional generics
|
|
143
|
+
* cannot flow through `BoundStoreAction`, so we declare the signature here
|
|
144
|
+
* and assign the (already-correct) runtime function to it.
|
|
145
|
+
*/
|
|
146
|
+
type GetOrganizationState = <
|
|
147
|
+
IncludeMembers extends boolean = false,
|
|
148
|
+
IncludeFeatures extends boolean = false,
|
|
149
|
+
>(
|
|
150
|
+
instance: SanityInstance,
|
|
151
|
+
options: OrganizationOptions<IncludeMembers, IncludeFeatures>,
|
|
152
|
+
) => StateSource<Organization<IncludeMembers, IncludeFeatures> | undefined>
|
|
153
|
+
|
|
154
|
+
type ResolveOrganization = <
|
|
155
|
+
IncludeMembers extends boolean = false,
|
|
156
|
+
IncludeFeatures extends boolean = false,
|
|
157
|
+
>(
|
|
158
|
+
instance: SanityInstance,
|
|
159
|
+
options: OrganizationOptions<IncludeMembers, IncludeFeatures>,
|
|
160
|
+
) => Promise<Organization<IncludeMembers, IncludeFeatures>>
|
|
161
|
+
|
|
162
|
+
/** @public */
|
|
163
|
+
export const getOrganizationState: GetOrganizationState = organization.getState
|
|
164
|
+
|
|
165
|
+
/** @public */
|
|
166
|
+
export const resolveOrganization: ResolveOrganization = organization.resolveState
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {expectTypeOf, test} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {type OrganizationMember} from '../organization/organization'
|
|
4
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
5
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
6
|
+
import {getOrganizationsState, type Organizations, resolveOrganizations} from './organizations'
|
|
7
|
+
|
|
8
|
+
const instance = {} as SanityInstance
|
|
9
|
+
|
|
10
|
+
test('resolveOrganizations — default call: bare list shape', () => {
|
|
11
|
+
expectTypeOf(resolveOrganizations(instance)).resolves.toEqualTypeOf<Organizations<false, false>>()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('resolveOrganizations — includeMembers: true narrows the generic', () => {
|
|
15
|
+
expectTypeOf(resolveOrganizations(instance, {includeMembers: true})).resolves.toEqualTypeOf<
|
|
16
|
+
Organizations<true, false>
|
|
17
|
+
>()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('resolveOrganizations — includeFeatures: true narrows the generic', () => {
|
|
21
|
+
expectTypeOf(resolveOrganizations(instance, {includeFeatures: true})).resolves.toEqualTypeOf<
|
|
22
|
+
Organizations<false, true>
|
|
23
|
+
>()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('resolveOrganizations — both flags true', () => {
|
|
27
|
+
expectTypeOf(
|
|
28
|
+
resolveOrganizations(instance, {includeMembers: true, includeFeatures: true}),
|
|
29
|
+
).resolves.toEqualTypeOf<Organizations<true, true>>()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('resolveOrganizations — rejects non-boolean flag values', () => {
|
|
33
|
+
// @ts-expect-error — includeMembers must be a boolean
|
|
34
|
+
void resolveOrganizations(instance, {includeMembers: 'yes'})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('resolveOrganizations — includeImplicitMemberships does not change the data shape', () => {
|
|
38
|
+
expectTypeOf(
|
|
39
|
+
resolveOrganizations(instance, {includeImplicitMemberships: true}),
|
|
40
|
+
).resolves.toEqualTypeOf<Organizations<false, false>>()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('Organizations — list items expose the documented subset of keys', () => {
|
|
44
|
+
type Keys = keyof Organizations<false, false>[number]
|
|
45
|
+
expectTypeOf<Keys>().toEqualTypeOf<
|
|
46
|
+
| 'id'
|
|
47
|
+
| 'name'
|
|
48
|
+
| 'slug'
|
|
49
|
+
| 'createdAt'
|
|
50
|
+
| 'updatedAt'
|
|
51
|
+
| 'defaultRoleName'
|
|
52
|
+
| 'dashboardStatus'
|
|
53
|
+
| 'aiFeaturesStatus'
|
|
54
|
+
>()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('Organizations<true, false>[number] exposes members[]', () => {
|
|
58
|
+
type Item = Organizations<true, false>[number]
|
|
59
|
+
expectTypeOf<Item['members']>().toEqualTypeOf<OrganizationMember[]>()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('Organizations<false, true>[number] exposes features[]', () => {
|
|
63
|
+
type Item = Organizations<false, true>[number]
|
|
64
|
+
expectTypeOf<Item['features']>().toEqualTypeOf<string[]>()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('Organizations<true, true>[number] exposes both members[] and features[]', () => {
|
|
68
|
+
type Item = Organizations<true, true>[number]
|
|
69
|
+
expectTypeOf<Item['members']>().toEqualTypeOf<OrganizationMember[]>()
|
|
70
|
+
expectTypeOf<Item['features']>().toEqualTypeOf<string[]>()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('getOrganizationsState — default call returns the bare-base StateSource', () => {
|
|
74
|
+
expectTypeOf(getOrganizationsState(instance)).toEqualTypeOf<
|
|
75
|
+
StateSource<Organizations<false, false> | undefined>
|
|
76
|
+
>()
|
|
77
|
+
})
|
|
@@ -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
|
|
@@ -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',
|
|
@@ -2,7 +2,7 @@ import {type SanityClient} from '@sanity/client'
|
|
|
2
2
|
import {createImageUrlBuilder} from '@sanity/image-url'
|
|
3
3
|
|
|
4
4
|
import {getClient} from '../client/clientStore'
|
|
5
|
-
import {type DocumentResource
|
|
5
|
+
import {type DocumentResource} from '../config/sanityConfig'
|
|
6
6
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
7
7
|
import {isObject} from '../utils/object'
|
|
8
8
|
import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
|
|
@@ -80,8 +80,7 @@ export function transformProjectionToPreview(
|
|
|
80
80
|
// Get a client for the resource (if provided) or use the instance config
|
|
81
81
|
const client = getClient(instance, {
|
|
82
82
|
apiVersion: API_VERSION,
|
|
83
|
-
|
|
84
|
-
resource: resource && !isDatasetResource(resource) ? resource : undefined,
|
|
83
|
+
resource,
|
|
85
84
|
})
|
|
86
85
|
|
|
87
86
|
return {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {expectTypeOf, test} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
4
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
5
|
+
import {
|
|
6
|
+
getProjectState,
|
|
7
|
+
type Project,
|
|
8
|
+
type ProjectBase,
|
|
9
|
+
type ProjectMember,
|
|
10
|
+
resolveProject,
|
|
11
|
+
} from './project'
|
|
12
|
+
|
|
13
|
+
const instance = {} as SanityInstance
|
|
14
|
+
|
|
15
|
+
test('resolveProject — default call: members and features both included by default', () => {
|
|
16
|
+
expectTypeOf(resolveProject(instance)).resolves.toEqualTypeOf<Project<true, true>>()
|
|
17
|
+
type Result = Awaited<ReturnType<typeof resolveProject<true, true>>>
|
|
18
|
+
expectTypeOf<Result['members']>().toEqualTypeOf<ProjectMember[]>()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('resolveProject — includeMembers: false drops members from the type', () => {
|
|
22
|
+
expectTypeOf(resolveProject(instance, {includeMembers: false})).resolves.toEqualTypeOf<
|
|
23
|
+
Project<false, true>
|
|
24
|
+
>()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('resolveProject — includeFeatures: false drops features from the type', () => {
|
|
28
|
+
expectTypeOf(resolveProject(instance, {includeFeatures: false})).resolves.toEqualTypeOf<
|
|
29
|
+
Project<true, false>
|
|
30
|
+
>()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('resolveProject — both flags false → bare base shape', () => {
|
|
34
|
+
expectTypeOf(
|
|
35
|
+
resolveProject(instance, {includeMembers: false, includeFeatures: false}),
|
|
36
|
+
).resolves.toEqualTypeOf<Project<false, false>>()
|
|
37
|
+
type Result = Awaited<ReturnType<typeof resolveProject<false, false>>>
|
|
38
|
+
expectTypeOf<Result['id']>().toEqualTypeOf<string>()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('resolveProject — rejects non-boolean flag values', () => {
|
|
42
|
+
// @ts-expect-error — includeMembers must be a boolean
|
|
43
|
+
void resolveProject(instance, {includeMembers: 'yes'})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('resolveProject — projectId alone does not change the data shape', () => {
|
|
47
|
+
expectTypeOf(resolveProject(instance, {projectId: 'p'})).resolves.toEqualTypeOf<
|
|
48
|
+
Project<true, true>
|
|
49
|
+
>()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('resolveProject — non-literal boolean flag makes members optional', () => {
|
|
53
|
+
const includeMembers = false as boolean
|
|
54
|
+
expectTypeOf(resolveProject(instance, {includeMembers})).resolves.toEqualTypeOf<
|
|
55
|
+
Project<boolean, true>
|
|
56
|
+
>()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('getProjectState — default call returns members + features StateSource', () => {
|
|
60
|
+
expectTypeOf(getProjectState(instance)).toEqualTypeOf<
|
|
61
|
+
StateSource<Project<true, true> | undefined>
|
|
62
|
+
>()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('getProjectState — both flags false narrows to the bare base shape', () => {
|
|
66
|
+
expectTypeOf(
|
|
67
|
+
getProjectState(instance, {includeMembers: false, includeFeatures: false}),
|
|
68
|
+
).toEqualTypeOf<StateSource<Project<false, false> | undefined>>()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('Project — wide boolean for IncludeMembers makes members optional', () => {
|
|
72
|
+
expectTypeOf<Project<boolean, true>>().toEqualTypeOf<
|
|
73
|
+
ProjectBase & {members?: ProjectMember[]} & {features: string[]}
|
|
74
|
+
>()
|
|
75
|
+
expectTypeOf<Pick<Project<boolean, true>, 'members'>>().toEqualTypeOf<{
|
|
76
|
+
members?: ProjectMember[]
|
|
77
|
+
}>()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('Project — wide boolean for IncludeFeatures makes features optional', () => {
|
|
81
|
+
expectTypeOf<Project<true, boolean>>().toEqualTypeOf<
|
|
82
|
+
ProjectBase & {members: ProjectMember[]} & {features?: string[]}
|
|
83
|
+
>()
|
|
84
|
+
expectTypeOf<Pick<Project<true, boolean>, 'features'>>().toEqualTypeOf<{
|
|
85
|
+
features?: string[]
|
|
86
|
+
}>()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('Project — both wide booleans make both fields optional', () => {
|
|
90
|
+
expectTypeOf<Project<boolean, boolean>>().toEqualTypeOf<
|
|
91
|
+
ProjectBase & {members?: ProjectMember[]} & {features?: string[]}
|
|
92
|
+
>()
|
|
93
|
+
})
|