Skip to content

Commit 8b5e9b8

Browse files
committed
feat: require project ID for project-level fetchers
1 parent 8d9b001 commit 8b5e9b8

File tree

8 files changed

+62
-237
lines changed

8 files changed

+62
-237
lines changed

packages/core/src/datasets/datasets.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('datasets', () => {
2525
observable: of(mockClient),
2626
} as StateSource<SanityClient>)
2727

28-
const result = await resolveDatasets(instance)
28+
const result = await resolveDatasets(instance, {projectId: 'p'})
2929
expect(result).toEqual(datasets)
3030
expect(list).toHaveBeenCalled()
3131
})

packages/core/src/datasets/datasets.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
import {switchMap} from 'rxjs'
22

33
import {getClientState} from '../client/clientStore'
4-
import {type ProjectHandle} from '../config/sanityConfig'
54
import {createFetcherStore} from '../utils/createFetcherStore'
65

76
const API_VERSION = 'v2025-02-19'
87

8+
type DatasetOptions = {
9+
projectId: string
10+
}
11+
912
/** @public */
1013
export const datasets = createFetcherStore({
1114
name: 'Datasets',
12-
getKey: (instance, options?: ProjectHandle) => {
13-
const projectId = options?.projectId ?? instance.config.projectId
14-
if (!projectId) {
15-
throw new Error('A projectId is required to use the project API.')
16-
}
17-
return projectId
18-
},
19-
fetcher: (instance) => (options?: ProjectHandle) => {
20-
return getClientState(instance, {
21-
apiVersion: API_VERSION,
22-
// non-null assertion is fine because we check above
23-
projectId: (options?.projectId ?? instance.config.projectId)!,
24-
useProjectHostname: true,
25-
}).observable.pipe(switchMap((client) => client.observable.datasets.list()))
26-
},
15+
getKey: (_, {projectId}: DatasetOptions) => projectId,
16+
fetcher:
17+
(instance) =>
18+
({projectId}: DatasetOptions) => {
19+
return getClientState(instance, {
20+
apiVersion: API_VERSION,
21+
// non-null assertion is fine because we check above
22+
projectId,
23+
useProjectHostname: true,
24+
}).observable.pipe(switchMap((client) => client.observable.datasets.list()))
25+
},
2726
})
2827

2928
/** @public */

packages/core/src/project/project.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,25 @@
11
import {switchMap} from 'rxjs'
22

33
import {getClientState} from '../client/clientStore'
4-
import {type ProjectHandle} from '../config/sanityConfig'
54
import {createFetcherStore} from '../utils/createFetcherStore'
65

76
const API_VERSION = 'v2025-02-19'
87

8+
type ProjectOptions = {
9+
projectId: string
10+
}
11+
912
const project = createFetcherStore({
1013
name: 'Project',
11-
getKey: (instance, options?: ProjectHandle) => {
12-
const projectId = options?.projectId ?? instance.config.projectId
13-
if (!projectId) {
14-
throw new Error('A projectId is required to use the project API.')
15-
}
16-
return projectId
17-
},
14+
getKey: (_, {projectId}: ProjectOptions) => projectId,
1815
fetcher:
1916
(instance) =>
20-
(options: ProjectHandle = {}) => {
21-
const projectId = options.projectId ?? instance.config.projectId
22-
17+
({projectId}: ProjectOptions) => {
2318
return getClientState(instance, {
2419
apiVersion: API_VERSION,
2520
scope: 'global',
2621
projectId,
27-
}).observable.pipe(
28-
switchMap((client) =>
29-
client.observable.projects.getById(
30-
// non-null assertion is fine with the above throwing
31-
(projectId ?? instance.config.projectId)!,
32-
),
33-
),
34-
)
22+
}).observable.pipe(switchMap((client) => client.observable.projects.getById(projectId)))
3523
},
3624
})
3725

packages/react/src/hooks/datasets/useDatasets.test.ts

Lines changed: 0 additions & 80 deletions
This file was deleted.

packages/react/src/hooks/datasets/useDatasets.ts

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import {type DatasetsResponse} from '@sanity/client'
2-
import {
3-
getDatasetsState,
4-
type ProjectHandle,
5-
resolveDatasets,
6-
type SanityInstance,
7-
type StateSource,
8-
} from '@sanity/sdk'
9-
import {identity} from 'rxjs'
2+
import {getDatasetsState} from '@sanity/sdk'
3+
import {useMemo} from 'react'
104

11-
import {createStateSourceHook} from '../helpers/createStateSourceHook'
5+
import {useSanityInstance} from '../context/useSanityInstance'
6+
import {useStoreState} from '../helpers/useStoreState'
127

138
type UseDatasets = {
149
/**
@@ -39,14 +34,10 @@ type UseDatasets = {
3934
* @public
4035
* @function
4136
*/
42-
export const useDatasets: UseDatasets = createStateSourceHook({
43-
getState: getDatasetsState as (
44-
instance: SanityInstance,
45-
projectHandle?: ProjectHandle,
46-
) => StateSource<DatasetsResponse>,
47-
shouldSuspend: (instance, projectHandle?: ProjectHandle) =>
48-
// remove `undefined` since we're suspending when that is the case
49-
getDatasetsState(instance, projectHandle).getCurrent() === undefined,
50-
suspender: resolveDatasets,
51-
getConfig: identity as (projectHandle?: ProjectHandle) => ProjectHandle,
52-
})
37+
export const useDatasets: UseDatasets = () => {
38+
const instance = useSanityInstance()
39+
const {projectId} = instance.config
40+
if (!projectId) throw new Error('useDatasets must be configured with projectId')
41+
const state = useMemo(() => getDatasetsState(instance, {projectId}), [instance, projectId])
42+
return useStoreState(state)
43+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {type StateSource} from '@sanity/sdk'
2+
import {useSyncExternalStore} from 'react'
3+
import {first, firstValueFrom} from 'rxjs'
4+
5+
/**
6+
* useStoreState is a hook around a StateSource which is initially undefined and then
7+
* eventually always defined.
8+
*/
9+
export function useStoreState<T>(state: StateSource<T | undefined>): T {
10+
const current = state.getCurrent()
11+
12+
if (current === undefined) {
13+
throw firstValueFrom(state.observable.pipe(first((i) => i !== undefined)))
14+
}
15+
16+
return useSyncExternalStore(state.subscribe, state.getCurrent as () => T)
17+
}

packages/react/src/hooks/projects/useProject.test.ts

Lines changed: 0 additions & 80 deletions
This file was deleted.

packages/react/src/hooks/projects/useProject.ts

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import {
2-
getProjectState,
3-
type ProjectHandle,
4-
resolveProject,
5-
type SanityInstance,
6-
type SanityProject,
7-
type StateSource,
8-
} from '@sanity/sdk'
9-
import {identity} from 'rxjs'
1+
import {getProjectState, type ProjectHandle, type SanityProject} from '@sanity/sdk'
2+
import {useMemo} from 'react'
103

11-
import {createStateSourceHook} from '../helpers/createStateSourceHook'
4+
import {useSanityInstance} from '../context/useSanityInstance'
5+
import {useStoreState} from '../helpers/useStoreState'
126

137
type UseProject = {
148
/**
@@ -38,14 +32,10 @@ type UseProject = {
3832
* @public
3933
* @function
4034
*/
41-
export const useProject: UseProject = createStateSourceHook({
42-
// remove `undefined` since we're suspending when that is the case
43-
getState: getProjectState as (
44-
instance: SanityInstance,
45-
projectHandle?: ProjectHandle,
46-
) => StateSource<SanityProject>,
47-
shouldSuspend: (instance, projectHandle) =>
48-
getProjectState(instance, projectHandle).getCurrent() === undefined,
49-
suspender: resolveProject,
50-
getConfig: identity,
51-
})
35+
export const useProject: UseProject = (options) => {
36+
const instance = useSanityInstance()
37+
const projectId = options?.projectId ?? instance.config.projectId
38+
if (!projectId) throw new Error('useProject must be configured with projectId')
39+
const state = useMemo(() => getProjectState(instance, {projectId}), [instance, projectId])
40+
return useStoreState(state)
41+
}

0 commit comments

Comments
 (0)