@sanity/sdk 2.18.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 +233 -12
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -1
- package/dist/_chunks-es/_internal.js +1 -0
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +284 -6
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/telemetryManager.js +2 -2
- package/dist/_exports/_internal.d.ts +2 -2
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +3 -1
- package/dist/index.d.ts +599 -185
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +492 -337
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/_exports/_internal.ts +11 -0
- package/src/_exports/index.ts +57 -12
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- 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/getOrganizationVerificationState.test.ts +7 -7
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/document/documentStore.concurrency.test.ts +313 -22
- 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 +18 -84
- package/src/organization/organization.test.ts +11 -11
- package/src/organization/organization.ts +26 -54
- package/src/organizations/organizations.test-d.ts +11 -32
- package/src/organizations/organizations.test.ts +8 -8
- package/src/organizations/organizations.ts +29 -54
- package/src/project/project.test-d.ts +19 -75
- package/src/project/project.test.ts +57 -10
- package/src/project/project.ts +33 -52
- package/src/projects/projects.test-d.ts +15 -21
- package/src/projects/projects.test.ts +8 -8
- package/src/projects/projects.ts +25 -49
- 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/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/includeShape.ts +36 -0
- package/src/utils/setCleanupTimeout.ts +21 -8
- package/dist/_chunks-es/version.js +0 -7
- package/dist/_chunks-es/version.js.map +0 -1
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
type OrganizationOptions,
|
|
8
8
|
} from '../organization/organization'
|
|
9
9
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import {defineFetcher} from '../store/fetcherStore'
|
|
11
|
+
import {buildQuery} from '../utils/buildQuery'
|
|
12
12
|
|
|
13
13
|
const API_VERSION = 'v2025-02-19'
|
|
14
14
|
|
|
@@ -57,7 +57,7 @@ export interface OrganizationsOptions<
|
|
|
57
57
|
includeImplicitMemberships?: boolean
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function
|
|
60
|
+
function normalizeOrganizationsOptions(options?: OrganizationsOptions<boolean, boolean>) {
|
|
61
61
|
return {
|
|
62
62
|
includeImplicitMemberships: options?.includeImplicitMemberships ?? false,
|
|
63
63
|
includeMembers: options?.includeMembers ?? false,
|
|
@@ -71,62 +71,37 @@ export function getOrganizationsCacheKey(
|
|
|
71
71
|
options?: OrganizationsOptions<boolean, boolean>,
|
|
72
72
|
): string {
|
|
73
73
|
const {includeMembers, includeFeatures, includeImplicitMemberships} =
|
|
74
|
-
|
|
74
|
+
normalizeOrganizationsOptions(options)
|
|
75
75
|
const membersKey = includeMembers ? ':members' : ''
|
|
76
76
|
const featuresKey = includeFeatures ? ':features' : ''
|
|
77
77
|
const implicitKey = includeImplicitMemberships ? ':implicit' : ''
|
|
78
78
|
return `organizations${membersKey}${featuresKey}${implicitKey}`
|
|
79
79
|
}
|
|
80
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
81
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
82
|
+
* Fetcher for the current user's organizations (`GET /organizations`), on the
|
|
83
|
+
* shared fetcher cache.
|
|
84
|
+
*
|
|
85
|
+
* @internal
|
|
111
86
|
*/
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
>(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
) =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
})
|
|
@@ -2,92 +2,36 @@ import {expectTypeOf, test} from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
4
4
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
type Project,
|
|
8
|
-
type ProjectBase,
|
|
9
|
-
type ProjectMember,
|
|
10
|
-
resolveProject,
|
|
11
|
-
} from './project'
|
|
5
|
+
import {type FetcherSnapshot} from '../store/fetcherStore'
|
|
6
|
+
import {type Project, project, type ProjectMember} from './project'
|
|
12
7
|
|
|
13
8
|
const instance = {} as SanityInstance
|
|
14
9
|
|
|
15
|
-
test('
|
|
16
|
-
expectTypeOf(
|
|
17
|
-
|
|
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>
|
|
10
|
+
test('project.resolveState — resolves the wide-boolean detail shape', () => {
|
|
11
|
+
expectTypeOf(project.resolveState(instance, {projectId: 'p'})).resolves.toEqualTypeOf<
|
|
12
|
+
Project<boolean, boolean>
|
|
56
13
|
>()
|
|
57
14
|
})
|
|
58
15
|
|
|
59
|
-
test('
|
|
60
|
-
expectTypeOf(
|
|
61
|
-
|
|
62
|
-
>()
|
|
16
|
+
test('Project — literal flags narrow members and features', () => {
|
|
17
|
+
expectTypeOf<Project<true, true>['members']>().toEqualTypeOf<ProjectMember[]>()
|
|
18
|
+
expectTypeOf<Project<true, true>['features']>().toEqualTypeOf<string[]>()
|
|
19
|
+
expectTypeOf<keyof Project<false, false> & ('members' | 'features')>().toEqualTypeOf<never>()
|
|
63
20
|
})
|
|
64
21
|
|
|
65
|
-
test('
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
).toEqualTypeOf<
|
|
22
|
+
test('Project<boolean, boolean> — members and features are optional', () => {
|
|
23
|
+
type Item = Project<boolean, boolean>
|
|
24
|
+
expectTypeOf<Item['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
|
|
25
|
+
expectTypeOf<Item['features']>().toEqualTypeOf<string[] | undefined>()
|
|
69
26
|
})
|
|
70
27
|
|
|
71
|
-
test('
|
|
72
|
-
expectTypeOf
|
|
73
|
-
|
|
28
|
+
test('project.getState — exposes the snapshot envelope', () => {
|
|
29
|
+
expectTypeOf(project.getState(instance, {projectId: 'p'})).toEqualTypeOf<
|
|
30
|
+
StateSource<FetcherSnapshot<Project<boolean, boolean>>>
|
|
74
31
|
>()
|
|
75
|
-
expectTypeOf<Pick<Project<boolean, true>, 'members'>>().toEqualTypeOf<{
|
|
76
|
-
members?: ProjectMember[]
|
|
77
|
-
}>()
|
|
78
32
|
})
|
|
79
33
|
|
|
80
|
-
test('
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
>()
|
|
34
|
+
test('project — rejects non-boolean flag values', () => {
|
|
35
|
+
// @ts-expect-error includeMembers must be a boolean
|
|
36
|
+
void project.resolveState(instance, {includeMembers: 'yes'})
|
|
93
37
|
})
|
|
@@ -3,9 +3,10 @@ import {of} from 'rxjs'
|
|
|
3
3
|
import {afterEach, beforeEach, describe, it} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {getClientState} from '../client/clientStore'
|
|
6
|
+
import {projects} from '../projects/projects'
|
|
6
7
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
8
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
|
-
import {getProjectCacheKey,
|
|
9
|
+
import {getProjectCacheKey, project} from './project'
|
|
9
10
|
|
|
10
11
|
vi.mock('../client/clientStore')
|
|
11
12
|
|
|
@@ -21,8 +22,8 @@ describe('project', () => {
|
|
|
21
22
|
})
|
|
22
23
|
|
|
23
24
|
it('calls `client.observable.request` against `/projects/<id>` and returns the result', async () => {
|
|
24
|
-
const
|
|
25
|
-
const request = vi.fn().mockReturnValue(of(
|
|
25
|
+
const detail = {id: 'a'}
|
|
26
|
+
const request = vi.fn().mockReturnValue(of(detail))
|
|
26
27
|
|
|
27
28
|
const mockClient = {
|
|
28
29
|
observable: {request} as unknown as SanityClient['observable'],
|
|
@@ -32,12 +33,12 @@ describe('project', () => {
|
|
|
32
33
|
observable: of(mockClient),
|
|
33
34
|
} as StateSource<SanityClient>)
|
|
34
35
|
|
|
35
|
-
const result = await
|
|
36
|
-
expect(result).toEqual(
|
|
36
|
+
const result = await project.resolveState(instance, {projectId: 'a'})
|
|
37
|
+
expect(result).toEqual(detail)
|
|
37
38
|
expect(request).toHaveBeenCalledWith({
|
|
38
39
|
uri: '/projects/a',
|
|
39
40
|
query: {includeMembers: 'true', includeFeatures: 'true'},
|
|
40
|
-
tag: '
|
|
41
|
+
tag: 'projects.get',
|
|
41
42
|
})
|
|
42
43
|
})
|
|
43
44
|
|
|
@@ -51,7 +52,7 @@ describe('project', () => {
|
|
|
51
52
|
observable: of(mockClient),
|
|
52
53
|
} as StateSource<SanityClient>)
|
|
53
54
|
|
|
54
|
-
await
|
|
55
|
+
await project.resolveState(instance, {
|
|
55
56
|
projectId: 'a',
|
|
56
57
|
includeMembers: false,
|
|
57
58
|
includeFeatures: false,
|
|
@@ -63,7 +64,7 @@ describe('project', () => {
|
|
|
63
64
|
includeMembers: 'false',
|
|
64
65
|
includeFeatures: 'false',
|
|
65
66
|
},
|
|
66
|
-
tag: '
|
|
67
|
+
tag: 'projects.get',
|
|
67
68
|
})
|
|
68
69
|
})
|
|
69
70
|
|
|
@@ -77,13 +78,59 @@ describe('project', () => {
|
|
|
77
78
|
observable: of(mockClient),
|
|
78
79
|
} as StateSource<SanityClient>)
|
|
79
80
|
|
|
80
|
-
await
|
|
81
|
+
await project.resolveState(instance)
|
|
81
82
|
expect(request).toHaveBeenCalledWith({
|
|
82
83
|
uri: '/projects/p',
|
|
83
84
|
query: {includeMembers: 'true', includeFeatures: 'true'},
|
|
84
|
-
tag: '
|
|
85
|
+
tag: 'projects.get',
|
|
85
86
|
})
|
|
86
87
|
})
|
|
88
|
+
|
|
89
|
+
it('pre-seeds a memberless detail read from the cached default list without fetching', async () => {
|
|
90
|
+
const request = vi.fn().mockReturnValue(
|
|
91
|
+
of([
|
|
92
|
+
{id: 'p1', displayName: 'One'},
|
|
93
|
+
{id: 'p2', displayName: 'Two'},
|
|
94
|
+
]),
|
|
95
|
+
)
|
|
96
|
+
const mockClient = {
|
|
97
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
98
|
+
} as SanityClient
|
|
99
|
+
|
|
100
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
101
|
+
observable: of(mockClient),
|
|
102
|
+
} as StateSource<SanityClient>)
|
|
103
|
+
|
|
104
|
+
await projects.resolveState(instance)
|
|
105
|
+
expect(request).toHaveBeenCalledTimes(1)
|
|
106
|
+
|
|
107
|
+
const source = project.getState(instance, {projectId: 'p1', includeMembers: false})
|
|
108
|
+
const unsubscribe = source.subscribe()
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
110
|
+
|
|
111
|
+
expect(source.getCurrent()).toMatchObject({status: 'success', data: {id: 'p1'}})
|
|
112
|
+
expect(request).toHaveBeenCalledTimes(1)
|
|
113
|
+
unsubscribe()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('does not pre-seed a detail read that includes members', async () => {
|
|
117
|
+
const list = [{id: 'p1', displayName: 'One'}]
|
|
118
|
+
const detail = {id: 'p1', displayName: 'One', members: []}
|
|
119
|
+
const request = vi.fn().mockReturnValueOnce(of(list)).mockReturnValueOnce(of(detail))
|
|
120
|
+
const mockClient = {
|
|
121
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
122
|
+
} as SanityClient
|
|
123
|
+
|
|
124
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
125
|
+
observable: of(mockClient),
|
|
126
|
+
} as StateSource<SanityClient>)
|
|
127
|
+
|
|
128
|
+
await projects.resolveState(instance)
|
|
129
|
+
|
|
130
|
+
const result = await project.resolveState(instance, {projectId: 'p1'})
|
|
131
|
+
expect(result).toEqual(detail)
|
|
132
|
+
expect(request).toHaveBeenCalledTimes(2)
|
|
133
|
+
})
|
|
87
134
|
})
|
|
88
135
|
|
|
89
136
|
describe('project cache key generation', () => {
|
package/src/project/project.ts
CHANGED
|
@@ -2,9 +2,10 @@ import {switchMap} from 'rxjs'
|
|
|
2
2
|
|
|
3
3
|
import {getClientState} from '../client/clientStore'
|
|
4
4
|
import {type ProjectHandle} from '../config/sanityConfig'
|
|
5
|
+
import {projects} from '../projects/projects'
|
|
5
6
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import {defineFetcher} from '../store/fetcherStore'
|
|
8
|
+
import {buildQuery} from '../utils/buildQuery'
|
|
8
9
|
|
|
9
10
|
const API_VERSION = 'v2025-02-19'
|
|
10
11
|
|
|
@@ -111,57 +112,37 @@ export function getProjectCacheKey(
|
|
|
111
112
|
return `project:${projectId}${membersKey}${featuresKey}`
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Fetcher for a single project (`GET /projects/:id`), on the shared fetcher
|
|
117
|
+
* cache. Pre-seeds from the default {@link projects} list entry when it can
|
|
118
|
+
* satisfy the read — the default list carries features but not members, so
|
|
119
|
+
* only `includeMembers: false` reads seed.
|
|
120
|
+
*
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
123
|
+
export const project = defineFetcher<
|
|
124
|
+
[options?: ProjectOptions<boolean, boolean>],
|
|
125
|
+
Project<boolean, boolean>
|
|
126
|
+
>({
|
|
127
|
+
name: 'project',
|
|
116
128
|
getKey: getProjectCacheKey,
|
|
117
|
-
|
|
129
|
+
fetch: (instance) => (options) =>
|
|
130
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
131
|
+
switchMap((client) =>
|
|
132
|
+
client.observable.request<Project<boolean, boolean>>({
|
|
133
|
+
uri: `/projects/${resolveProjectId(instance, options)}`,
|
|
134
|
+
query: buildQuery(normalizeProjectOptions(options)),
|
|
135
|
+
tag: 'projects.get',
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
tags: (data) => [{type: 'project', id: data.id}],
|
|
140
|
+
initialData: (instance, options) => {
|
|
141
|
+
if (normalizeProjectOptions(options).includeMembers) return undefined
|
|
142
|
+
const snapshot = projects.getState(instance).getCurrent()
|
|
143
|
+
if (snapshot.status !== 'success') return undefined
|
|
118
144
|
const projectId = resolveProjectId(instance, options)
|
|
119
|
-
|
|
120
|
-
return
|
|
121
|
-
apiVersion: API_VERSION,
|
|
122
|
-
scope: 'global',
|
|
123
|
-
}).observable.pipe(
|
|
124
|
-
switchMap((client) => {
|
|
125
|
-
const normalized = normalizeProjectOptions(options)
|
|
126
|
-
const query = Object.fromEntries(
|
|
127
|
-
Object.entries(normalized)
|
|
128
|
-
.filter(([, value]) => value !== undefined)
|
|
129
|
-
.map(([key, value]) => [key, String(value)]),
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
return client.observable.request({
|
|
133
|
-
uri: `/projects/${projectId}`,
|
|
134
|
-
query,
|
|
135
|
-
tag: 'project.get',
|
|
136
|
-
})
|
|
137
|
-
}),
|
|
138
|
-
)
|
|
145
|
+
const match = snapshot.data.find((item) => item.id === projectId)
|
|
146
|
+
return match && {data: match, dataUpdatedAt: snapshot.dataUpdatedAt}
|
|
139
147
|
},
|
|
140
148
|
})
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Public signature for the project state source. The conditional generics
|
|
144
|
-
* cannot flow through `BoundStoreAction`, so we declare the signature here
|
|
145
|
-
* and assign the (already-correct) runtime function to it.
|
|
146
|
-
*/
|
|
147
|
-
type GetProjectState = <
|
|
148
|
-
IncludeMembers extends boolean = true,
|
|
149
|
-
IncludeFeatures extends boolean = true,
|
|
150
|
-
>(
|
|
151
|
-
instance: SanityInstance,
|
|
152
|
-
options?: ProjectOptions<IncludeMembers, IncludeFeatures>,
|
|
153
|
-
) => StateSource<Project<IncludeMembers, IncludeFeatures> | undefined>
|
|
154
|
-
|
|
155
|
-
type ResolveProject = <
|
|
156
|
-
IncludeMembers extends boolean = true,
|
|
157
|
-
IncludeFeatures extends boolean = true,
|
|
158
|
-
>(
|
|
159
|
-
instance: SanityInstance,
|
|
160
|
-
options?: ProjectOptions<IncludeMembers, IncludeFeatures>,
|
|
161
|
-
) => Promise<Project<IncludeMembers, IncludeFeatures>>
|
|
162
|
-
|
|
163
|
-
/** @public */
|
|
164
|
-
export const getProjectState: GetProjectState = project.getState
|
|
165
|
-
|
|
166
|
-
/** @public */
|
|
167
|
-
export const resolveProject: ResolveProject = project.resolveState
|
|
@@ -3,36 +3,30 @@ import {expectTypeOf, test} from 'vitest'
|
|
|
3
3
|
import {type Project, type ProjectMember} from '../project/project'
|
|
4
4
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
5
5
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
6
|
-
import {
|
|
6
|
+
import {type FetcherSnapshot} from '../store/fetcherStore'
|
|
7
|
+
import {projects} from './projects'
|
|
7
8
|
|
|
8
9
|
const instance = {} as SanityInstance
|
|
9
10
|
|
|
10
|
-
test('
|
|
11
|
-
expectTypeOf(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
test('resolveProjects — includeMembers: true adds members to the type', () => {
|
|
15
|
-
expectTypeOf(resolveProjects(instance, {includeMembers: true})).resolves.toEqualTypeOf<
|
|
16
|
-
Project<true, true>[]
|
|
11
|
+
test('projects.resolveState — resolves the wide-boolean list shape', () => {
|
|
12
|
+
expectTypeOf(projects.resolveState(instance)).resolves.toEqualTypeOf<
|
|
13
|
+
Project<boolean, boolean>[]
|
|
17
14
|
>()
|
|
18
|
-
type Result = Awaited<ReturnType<typeof resolveProjects<true, true>>>
|
|
19
|
-
expectTypeOf<Result[number]['members']>().toEqualTypeOf<ProjectMember[]>()
|
|
20
15
|
})
|
|
21
16
|
|
|
22
|
-
test('
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
>()
|
|
17
|
+
test('Project<boolean, boolean> — members and features are optional', () => {
|
|
18
|
+
type Item = Project<boolean, boolean>
|
|
19
|
+
expectTypeOf<Item['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
|
|
20
|
+
expectTypeOf<Item['features']>().toEqualTypeOf<string[] | undefined>()
|
|
26
21
|
})
|
|
27
22
|
|
|
28
|
-
test('
|
|
29
|
-
expectTypeOf(
|
|
30
|
-
Project<
|
|
23
|
+
test('projects.getState — exposes the snapshot envelope', () => {
|
|
24
|
+
expectTypeOf(projects.getState(instance)).toEqualTypeOf<
|
|
25
|
+
StateSource<FetcherSnapshot<Project<boolean, boolean>[]>>
|
|
31
26
|
>()
|
|
32
27
|
})
|
|
33
28
|
|
|
34
|
-
test('
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
>()
|
|
29
|
+
test('projects — rejects non-boolean flag values', () => {
|
|
30
|
+
// @ts-expect-error includeMembers must be a boolean
|
|
31
|
+
void projects.resolveState(instance, {includeMembers: 'yes'})
|
|
38
32
|
})
|
|
@@ -5,7 +5,7 @@ import {afterEach, beforeEach, describe, it} from 'vitest'
|
|
|
5
5
|
import {getClientState} from '../client/clientStore'
|
|
6
6
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
7
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
|
-
import {getProjectsCacheKey,
|
|
8
|
+
import {getProjectsCacheKey, projects} from './projects'
|
|
9
9
|
|
|
10
10
|
vi.mock('../client/clientStore')
|
|
11
11
|
|
|
@@ -21,8 +21,8 @@ describe('projects', () => {
|
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
it('calls `client.observable.request` against `/projects` and returns the result', async () => {
|
|
24
|
-
const
|
|
25
|
-
const request = vi.fn().mockReturnValue(of(
|
|
24
|
+
const list = [{id: 'a'}, {id: 'b'}]
|
|
25
|
+
const request = vi.fn().mockReturnValue(of(list))
|
|
26
26
|
|
|
27
27
|
const mockClient = {
|
|
28
28
|
observable: {request} as unknown as SanityClient['observable'],
|
|
@@ -32,12 +32,12 @@ describe('projects', () => {
|
|
|
32
32
|
observable: of(mockClient),
|
|
33
33
|
} as StateSource<SanityClient>)
|
|
34
34
|
|
|
35
|
-
const result = await
|
|
36
|
-
expect(result).toEqual(
|
|
35
|
+
const result = await projects.resolveState(instance)
|
|
36
|
+
expect(result).toEqual(list)
|
|
37
37
|
expect(request).toHaveBeenCalledWith({
|
|
38
38
|
uri: '/projects',
|
|
39
39
|
query: {includeMembers: 'false', includeFeatures: 'true', onlyExplicitMembership: 'false'},
|
|
40
|
-
tag: 'projects.
|
|
40
|
+
tag: 'projects.list',
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
43
|
|
|
@@ -51,7 +51,7 @@ describe('projects', () => {
|
|
|
51
51
|
observable: of(mockClient),
|
|
52
52
|
} as StateSource<SanityClient>)
|
|
53
53
|
|
|
54
|
-
await
|
|
54
|
+
await projects.resolveState(instance, {
|
|
55
55
|
organizationId: 'org123',
|
|
56
56
|
includeMembers: true,
|
|
57
57
|
includeFeatures: false,
|
|
@@ -65,7 +65,7 @@ describe('projects', () => {
|
|
|
65
65
|
includeFeatures: 'false',
|
|
66
66
|
onlyExplicitMembership: 'false',
|
|
67
67
|
},
|
|
68
|
-
tag: 'projects.
|
|
68
|
+
tag: 'projects.list',
|
|
69
69
|
})
|
|
70
70
|
})
|
|
71
71
|
})
|
package/src/projects/projects.ts
CHANGED
|
@@ -3,8 +3,8 @@ import {switchMap} from 'rxjs'
|
|
|
3
3
|
import {getClientState} from '../client/clientStore'
|
|
4
4
|
import {type Project} from '../project/project'
|
|
5
5
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import {defineFetcher} from '../store/fetcherStore'
|
|
7
|
+
import {buildQuery} from '../utils/buildQuery'
|
|
8
8
|
|
|
9
9
|
const API_VERSION = 'v2025-02-19'
|
|
10
10
|
|
|
@@ -42,54 +42,30 @@ export function getProjectsCacheKey(
|
|
|
42
42
|
return `projects${orgKey}${membersKey}${featuresKey}${explicitKey}`
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Fetcher for the current user's projects (`GET /projects`), on the shared
|
|
47
|
+
* fetcher cache.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export const projects = defineFetcher<
|
|
52
|
+
[options?: ProjectsOptions<boolean, boolean>],
|
|
53
|
+
Project<boolean, boolean>[]
|
|
54
|
+
>({
|
|
55
|
+
name: 'projects',
|
|
47
56
|
getKey: getProjectsCacheKey,
|
|
48
|
-
|
|
49
|
-
getClientState(instance, {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}).observable.pipe(
|
|
53
|
-
switchMap((client) => {
|
|
54
|
-
const normalized = normalizeProjectsOptions(options)
|
|
55
|
-
const query = Object.fromEntries(
|
|
56
|
-
Object.entries(normalized)
|
|
57
|
-
.filter(([, value]) => value !== undefined)
|
|
58
|
-
.map(([key, value]) => [key, String(value)]),
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
return client.observable.request({
|
|
57
|
+
fetch: (instance) => (options) =>
|
|
58
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
59
|
+
switchMap((client) =>
|
|
60
|
+
client.observable.request<Project<boolean, boolean>[]>({
|
|
62
61
|
uri: '/projects',
|
|
63
|
-
query,
|
|
64
|
-
tag: 'projects.
|
|
65
|
-
})
|
|
66
|
-
|
|
62
|
+
query: buildQuery(normalizeProjectsOptions(options)),
|
|
63
|
+
tag: 'projects.list',
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
67
66
|
),
|
|
67
|
+
tags: (data) => [
|
|
68
|
+
{type: 'project', id: 'LIST'},
|
|
69
|
+
...data.map((project) => ({type: 'project', id: project.id})),
|
|
70
|
+
],
|
|
68
71
|
})
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Public signature for the projects state source. The conditional generics
|
|
72
|
-
* cannot flow through `BoundStoreAction`, so we declare the signature here
|
|
73
|
-
* and assign the (already-correct) runtime function to it.
|
|
74
|
-
*/
|
|
75
|
-
type GetProjectsState = <
|
|
76
|
-
IncludeMembers extends boolean = false,
|
|
77
|
-
IncludeFeatures extends boolean = true,
|
|
78
|
-
>(
|
|
79
|
-
instance: SanityInstance,
|
|
80
|
-
options?: ProjectsOptions<IncludeMembers, IncludeFeatures>,
|
|
81
|
-
) => StateSource<Project<IncludeMembers, IncludeFeatures>[] | undefined>
|
|
82
|
-
|
|
83
|
-
type ResolveProjects = <
|
|
84
|
-
IncludeMembers extends boolean = false,
|
|
85
|
-
IncludeFeatures extends boolean = true,
|
|
86
|
-
>(
|
|
87
|
-
instance: SanityInstance,
|
|
88
|
-
options?: ProjectsOptions<IncludeMembers, IncludeFeatures>,
|
|
89
|
-
) => Promise<Project<IncludeMembers, IncludeFeatures>[]>
|
|
90
|
-
|
|
91
|
-
/** @public */
|
|
92
|
-
export const getProjectsState: GetProjectsState = projects.getState
|
|
93
|
-
|
|
94
|
-
/** @public */
|
|
95
|
-
export const resolveProjects: ResolveProjects = projects.resolveState
|