@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
package/src/document/reducers.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
|
|
2
2
|
import {type Mutation, type PatchOperations, type SanityDocumentLike} from '@sanity/types'
|
|
3
|
-
import {omit} from 'lodash-es'
|
|
4
3
|
|
|
5
4
|
import {type DocumentHandle} from '../config/sanityConfig'
|
|
6
5
|
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
7
6
|
import {type StoreContext} from '../store/defineStore'
|
|
8
7
|
import {insecureRandomId} from '../utils/ids'
|
|
8
|
+
import {omitProperty} from '../utils/object'
|
|
9
9
|
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
10
10
|
import {type DocumentAction} from './actions'
|
|
11
11
|
import {DOCUMENT_STATE_CLEAR_DELAY} from './documentConstants'
|
|
@@ -392,7 +392,7 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
392
392
|
local: documentId in working ? working[documentId] : local,
|
|
393
393
|
unverifiedRevisions:
|
|
394
394
|
prev.outgoing && prev.outgoing.transactionId in unverifiedRevisions
|
|
395
|
-
?
|
|
395
|
+
? omitProperty(unverifiedRevisions, prev.outgoing.transactionId)
|
|
396
396
|
: unverifiedRevisions,
|
|
397
397
|
}
|
|
398
398
|
return [documentId, next] as const
|
|
@@ -420,7 +420,7 @@ export function applyRemoteDocument(
|
|
|
420
420
|
const revisionToVerify = revision ? prevUnverifiedRevisions?.[revision] : undefined
|
|
421
421
|
let unverifiedRevisions = prevUnverifiedRevisions ?? EMPTY_REVISIONS
|
|
422
422
|
if (revision && revisionToVerify) {
|
|
423
|
-
unverifiedRevisions =
|
|
423
|
+
unverifiedRevisions = omitProperty(prevUnverifiedRevisions, revision)
|
|
424
424
|
}
|
|
425
425
|
|
|
426
426
|
// if this remote document is from a `'sync'` event (meaning that the whole
|
|
@@ -547,7 +547,7 @@ export function removeSubscriptionIdFromDocument(
|
|
|
547
547
|
|
|
548
548
|
if (!prevDocState) return prev
|
|
549
549
|
if (!subscriptions.length) {
|
|
550
|
-
return {...prev, documentStates:
|
|
550
|
+
return {...prev, documentStates: omitProperty(prev.documentStates, documentId)}
|
|
551
551
|
}
|
|
552
552
|
return {
|
|
553
553
|
...prev,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from 'rxjs'
|
|
15
15
|
|
|
16
16
|
import {getClientState} from '../client/clientStore'
|
|
17
|
-
import {type
|
|
17
|
+
import {type DocumentResource} from '../config/sanityConfig'
|
|
18
18
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
19
19
|
|
|
20
20
|
const API_VERSION = 'v2025-05-06'
|
|
@@ -26,13 +26,12 @@ export interface SharedListener {
|
|
|
26
26
|
|
|
27
27
|
export function createSharedListener(
|
|
28
28
|
instance: SanityInstance,
|
|
29
|
-
|
|
29
|
+
resource?: DocumentResource,
|
|
30
30
|
): SharedListener {
|
|
31
31
|
const dispose$ = new Subject<void>()
|
|
32
32
|
const events$ = getClientState(instance, {
|
|
33
33
|
apiVersion: API_VERSION,
|
|
34
|
-
|
|
35
|
-
source: source && !isDatasetSource(source) ? source : undefined,
|
|
34
|
+
resource,
|
|
36
35
|
}).observable.pipe(
|
|
37
36
|
switchMap((client) =>
|
|
38
37
|
// TODO: it seems like the client.listen method is not emitting disconnected
|
|
@@ -68,12 +67,11 @@ export function createSharedListener(
|
|
|
68
67
|
}
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
export function createFetchDocument(instance: SanityInstance,
|
|
70
|
+
export function createFetchDocument(instance: SanityInstance, resource?: DocumentResource) {
|
|
72
71
|
return function (documentId: string): Observable<SanityDocument | null> {
|
|
73
72
|
return getClientState(instance, {
|
|
74
73
|
apiVersion: API_VERSION,
|
|
75
|
-
|
|
76
|
-
source: source && !isDatasetSource(source) ? source : undefined,
|
|
74
|
+
resource,
|
|
77
75
|
}).observable.pipe(
|
|
78
76
|
switchMap((client) => {
|
|
79
77
|
// creates a observable request to the /doc/{documentId} endpoint for a given document id
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {expectTypeOf, test} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
4
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
5
|
+
import {
|
|
6
|
+
getOrganizationState,
|
|
7
|
+
type Organization,
|
|
8
|
+
type OrganizationBase,
|
|
9
|
+
type OrganizationMember,
|
|
10
|
+
resolveOrganization,
|
|
11
|
+
} from './organization'
|
|
12
|
+
|
|
13
|
+
const instance = {} as SanityInstance
|
|
14
|
+
|
|
15
|
+
test('resolveOrganization — default call: members and features both omitted by default', () => {
|
|
16
|
+
expectTypeOf(resolveOrganization(instance, {organizationId: 'org_1'})).resolves.toEqualTypeOf<
|
|
17
|
+
Organization<false, false>
|
|
18
|
+
>()
|
|
19
|
+
type Result = Awaited<ReturnType<typeof resolveOrganization<false, false>>>
|
|
20
|
+
expectTypeOf<Result['id']>().toEqualTypeOf<string>()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('resolveOrganization — includeMembers: true adds members to the type', () => {
|
|
24
|
+
expectTypeOf(
|
|
25
|
+
resolveOrganization(instance, {organizationId: 'org_1', includeMembers: true}),
|
|
26
|
+
).resolves.toEqualTypeOf<Organization<true, false>>()
|
|
27
|
+
type Result = Awaited<ReturnType<typeof resolveOrganization<true, false>>>
|
|
28
|
+
expectTypeOf<Result['members']>().toEqualTypeOf<OrganizationMember[]>()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('resolveOrganization — includeFeatures: true adds features to the type', () => {
|
|
32
|
+
expectTypeOf(
|
|
33
|
+
resolveOrganization(instance, {organizationId: 'org_1', includeFeatures: true}),
|
|
34
|
+
).resolves.toEqualTypeOf<Organization<false, true>>()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('resolveOrganization — both flags true → both arrays present', () => {
|
|
38
|
+
expectTypeOf(
|
|
39
|
+
resolveOrganization(instance, {
|
|
40
|
+
organizationId: 'org_1',
|
|
41
|
+
includeMembers: true,
|
|
42
|
+
includeFeatures: true,
|
|
43
|
+
}),
|
|
44
|
+
).resolves.toEqualTypeOf<Organization<true, true>>()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('resolveOrganization — rejects non-boolean flag values', () => {
|
|
48
|
+
// @ts-expect-error — includeMembers must be a boolean
|
|
49
|
+
void resolveOrganization(instance, {organizationId: 'org_1', includeMembers: 'yes'})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('resolveOrganization — requires organizationId', () => {
|
|
53
|
+
// @ts-expect-error — organizationId is required
|
|
54
|
+
void resolveOrganization(instance, {})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('resolveOrganization — non-literal boolean flag makes members optional', () => {
|
|
58
|
+
const includeMembers = false as boolean
|
|
59
|
+
expectTypeOf(
|
|
60
|
+
resolveOrganization(instance, {organizationId: 'org_1', includeMembers}),
|
|
61
|
+
).resolves.toEqualTypeOf<Organization<boolean, false>>()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('getOrganizationState — default call returns bare-base StateSource', () => {
|
|
65
|
+
expectTypeOf(getOrganizationState(instance, {organizationId: 'org_1'})).toEqualTypeOf<
|
|
66
|
+
StateSource<Organization<false, false> | undefined>
|
|
67
|
+
>()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('getOrganizationState — both flags true narrows the StateSource value type', () => {
|
|
71
|
+
expectTypeOf(
|
|
72
|
+
getOrganizationState(instance, {
|
|
73
|
+
organizationId: 'org_1',
|
|
74
|
+
includeMembers: true,
|
|
75
|
+
includeFeatures: true,
|
|
76
|
+
}),
|
|
77
|
+
).toEqualTypeOf<StateSource<Organization<true, true> | undefined>>()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('Organization — wide boolean for IncludeMembers makes members optional', () => {
|
|
81
|
+
expectTypeOf<Organization<boolean, true>>().toEqualTypeOf<
|
|
82
|
+
OrganizationBase & {members?: OrganizationMember[]} & {features: string[]}
|
|
83
|
+
>()
|
|
84
|
+
expectTypeOf<Pick<Organization<boolean, true>, 'members'>>().toEqualTypeOf<{
|
|
85
|
+
members?: OrganizationMember[]
|
|
86
|
+
}>()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('Organization — wide boolean for IncludeFeatures makes features optional', () => {
|
|
90
|
+
expectTypeOf<Organization<true, boolean>>().toEqualTypeOf<
|
|
91
|
+
OrganizationBase & {members: OrganizationMember[]} & {features?: string[]}
|
|
92
|
+
>()
|
|
93
|
+
expectTypeOf<Pick<Organization<true, boolean>, 'features'>>().toEqualTypeOf<{
|
|
94
|
+
features?: string[]
|
|
95
|
+
}>()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('Organization — both wide booleans make both fields optional', () => {
|
|
99
|
+
expectTypeOf<Organization<boolean, boolean>>().toEqualTypeOf<
|
|
100
|
+
OrganizationBase & {members?: OrganizationMember[]} & {features?: string[]}
|
|
101
|
+
>()
|
|
102
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
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 {getOrganizationCacheKey, resolveOrganization} from './organization'
|
|
9
|
+
|
|
10
|
+
vi.mock('../client/clientStore')
|
|
11
|
+
|
|
12
|
+
describe('organization', () => {
|
|
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/<id>` and returns the result', async () => {
|
|
24
|
+
const organization = {id: 'org_1'}
|
|
25
|
+
const request = vi.fn().mockReturnValue(of(organization))
|
|
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 resolveOrganization(instance, {organizationId: 'org_1'})
|
|
36
|
+
expect(result).toEqual(organization)
|
|
37
|
+
expect(request).toHaveBeenCalledWith({
|
|
38
|
+
uri: '/organizations/org_1',
|
|
39
|
+
query: {includeMembers: 'false', includeFeatures: 'false'},
|
|
40
|
+
tag: 'organization.get',
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('serializes query params (booleans → strings) and respects flags', async () => {
|
|
45
|
+
const request = vi.fn().mockReturnValue(of({id: 'org_1'}))
|
|
46
|
+
const mockClient = {
|
|
47
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
48
|
+
} as SanityClient
|
|
49
|
+
|
|
50
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
51
|
+
observable: of(mockClient),
|
|
52
|
+
} as StateSource<SanityClient>)
|
|
53
|
+
|
|
54
|
+
await resolveOrganization(instance, {
|
|
55
|
+
organizationId: 'org_1',
|
|
56
|
+
includeMembers: true,
|
|
57
|
+
includeFeatures: true,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
expect(request).toHaveBeenCalledWith({
|
|
61
|
+
uri: '/organizations/org_1',
|
|
62
|
+
query: {
|
|
63
|
+
includeMembers: 'true',
|
|
64
|
+
includeFeatures: 'true',
|
|
65
|
+
},
|
|
66
|
+
tag: 'organization.get',
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('throws when no organizationId is provided', async () => {
|
|
71
|
+
await expect(resolveOrganization(instance, {organizationId: ''} as never)).rejects.toThrow(
|
|
72
|
+
'An organizationId is required to use the organization API.',
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('organization cache key generation', () => {
|
|
78
|
+
let instance: SanityInstance
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
instance = createSanityInstance({})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
instance.dispose()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('default call excludes :members and :features (both default-false)', () => {
|
|
89
|
+
expect(getOrganizationCacheKey(instance, {organizationId: 'org_1'})).toBe('organization:org_1')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('treats undefined and the matching default as the same key', () => {
|
|
93
|
+
expect(getOrganizationCacheKey(instance, {organizationId: 'org_1'})).toBe(
|
|
94
|
+
getOrganizationCacheKey(instance, {
|
|
95
|
+
organizationId: 'org_1',
|
|
96
|
+
includeMembers: false,
|
|
97
|
+
includeFeatures: false,
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('explicit includeMembers: true appends :members', () => {
|
|
103
|
+
expect(getOrganizationCacheKey(instance, {organizationId: 'org_1', includeMembers: true})).toBe(
|
|
104
|
+
'organization:org_1:members',
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('explicit includeFeatures: true appends :features', () => {
|
|
109
|
+
expect(
|
|
110
|
+
getOrganizationCacheKey(instance, {organizationId: 'org_1', includeFeatures: true}),
|
|
111
|
+
).toBe('organization:org_1:features')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('combines all segments in order', () => {
|
|
115
|
+
expect(
|
|
116
|
+
getOrganizationCacheKey(instance, {
|
|
117
|
+
organizationId: 'org_1',
|
|
118
|
+
includeMembers: true,
|
|
119
|
+
includeFeatures: true,
|
|
120
|
+
}),
|
|
121
|
+
).toBe('organization:org_1:members:features')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('produces distinct keys for each meaningful option permutation', () => {
|
|
125
|
+
const keys = new Set([
|
|
126
|
+
getOrganizationCacheKey(instance, {organizationId: 'org_1'}),
|
|
127
|
+
getOrganizationCacheKey(instance, {organizationId: 'org_1', includeMembers: true}),
|
|
128
|
+
getOrganizationCacheKey(instance, {organizationId: 'org_1', includeFeatures: true}),
|
|
129
|
+
getOrganizationCacheKey(instance, {
|
|
130
|
+
organizationId: 'org_1',
|
|
131
|
+
includeMembers: true,
|
|
132
|
+
includeFeatures: true,
|
|
133
|
+
}),
|
|
134
|
+
getOrganizationCacheKey(instance, {organizationId: 'org_2'}),
|
|
135
|
+
])
|
|
136
|
+
expect(keys.size).toBe(5)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -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
|
+
})
|