Skip to content

Commit 661c187

Browse files
committed
feat: support arbitrary sources in useQuery
This contains a non-breaking change to `useQuery` (and other hooks which uses this under the hood) of accepting a `source` parameter. This can be used to query datasets which are not part of the current context. However, this has lead to some breaking changes to the `core` package: - The QueryOptions no longer takes an _optional_ projectId/dataset, but rather a _required_ `source`. This will always represent the resource we'll be querying. - The same applies to `perspective`. This is part of the work of decoupling the perspective setting from the SanityInstace. - However, since we still want `useQuery` to have the same options we've now introduced a new UseQueryOptions.
1 parent 7029e15 commit 661c187

File tree

13 files changed

+191
-91
lines changed

13 files changed

+191
-91
lines changed

packages/core/src/_exports/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ export {
5252
export {
5353
type DatasetHandle,
5454
type DocumentHandle,
55+
type DocumentSource,
5556
type DocumentTypeHandle,
5657
type PerspectiveHandle,
5758
type ProjectHandle,
5859
type ReleasePerspective,
5960
type SanityConfig,
61+
sourceFor,
6062
} from '../config/sanityConfig'
6163
export {getDatasetsState, resolveDatasets} from '../datasets/datasets'
6264
export {

packages/core/src/config/sanityConfig.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,32 @@ export interface SanityConfig extends DatasetHandle, PerspectiveHandle {
8181
enabled: boolean
8282
}
8383
}
84+
85+
/**
86+
* Represents a source which can be used by various functionality.
87+
*
88+
* @see sourceFor For how to initialize a new source for a dataset.
89+
* @public
90+
*/
91+
export type DocumentSource = {
92+
[__sourceData]: {
93+
projectId: string
94+
dataset: string
95+
}
96+
}
97+
98+
/**
99+
* An internal symbol to avoid users to access data inside here.
100+
*
101+
* @internal
102+
*/
103+
export const __sourceData = Symbol('Sanity.DocumentSource')
104+
105+
/**
106+
* Creates a new {@link DocumentSource} object based on a projectId and dataset.
107+
*
108+
* @public
109+
*/
110+
export function sourceFor(data: {projectId: string; dataset: string}): DocumentSource {
111+
return {[__sourceData]: data}
112+
}

packages/core/src/preview/subscribeToStateAndFetchBatches.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
tap,
1515
} from 'rxjs'
1616

17+
import {sourceFor} from '../config/sanityConfig'
1718
import {getQueryState, resolveQuery} from '../query/queryStore'
1819
import {type BoundDatasetKey} from '../store/createActionBinder'
1920
import {type StoreContext} from '../store/defineStore'
@@ -66,8 +67,7 @@ export const subscribeToStateAndFetchBatches = ({
6667
params,
6768
tag: PREVIEW_TAG,
6869
perspective: PREVIEW_PERSPECTIVE,
69-
projectId,
70-
dataset,
70+
source: sourceFor({projectId, dataset}),
7171
})
7272
const source$ = defer(() => {
7373
if (getCurrent() === undefined) {
@@ -78,8 +78,7 @@ export const subscribeToStateAndFetchBatches = ({
7878
tag: PREVIEW_TAG,
7979
perspective: PREVIEW_PERSPECTIVE,
8080
signal: controller.signal,
81-
projectId,
82-
dataset,
81+
source: sourceFor({projectId, dataset}),
8382
}),
8483
).pipe(switchMap(() => observable))
8584
}

packages/core/src/projection/subscribeToStateAndFetchBatches.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
tap,
1717
} from 'rxjs'
1818

19+
import {sourceFor} from '../config/sanityConfig'
1920
import {getQueryState, resolveQuery} from '../query/queryStore'
2021
import {type BoundDatasetKey} from '../store/createActionBinder'
2122
import {type StoreContext} from '../store/defineStore'
@@ -96,8 +97,7 @@ export const subscribeToStateAndFetchBatches = ({
9697
const {getCurrent, observable} = getQueryState<ProjectionQueryResult[]>(instance, {
9798
query,
9899
params,
99-
projectId,
100-
dataset,
100+
source: sourceFor({projectId, dataset}),
101101
tag: PROJECTION_TAG,
102102
perspective: PROJECTION_PERSPECTIVE,
103103
})
@@ -108,8 +108,7 @@ export const subscribeToStateAndFetchBatches = ({
108108
resolveQuery<ProjectionQueryResult[]>(instance, {
109109
query,
110110
params,
111-
projectId,
112-
dataset,
111+
source: sourceFor({projectId, dataset}),
113112
tag: PROJECTION_TAG,
114113
perspective: PROJECTION_PERSPECTIVE,
115114
signal: controller.signal,

packages/core/src/query/queryStore.test.ts

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {delay, filter, firstValueFrom, Observable, of, Subject} from 'rxjs'
33
import {beforeEach, describe, expect, it, vi} from 'vitest'
44

55
import {getClientState} from '../client/clientStore'
6+
import {sourceFor as getSource} from '../config/sanityConfig'
67
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
78
import {type StateSource} from '../store/createStateSourceAction'
89
import {getQueryState, resolveQuery} from './queryStore'
@@ -17,6 +18,7 @@ vi.mock('../client/clientStore', () => ({
1718
}))
1819

1920
describe('queryStore', () => {
21+
const source = getSource({projectId: 'test', dataset: 'test'})
2022
let instance: SanityInstance
2123
let liveEvents: Subject<LiveEvent>
2224
let fetch: SanityClient['observable']['fetch']
@@ -61,7 +63,7 @@ describe('queryStore', () => {
6163

6264
it('initializes query state and cleans up after unsubscribe', async () => {
6365
const query = '*[_type == "movie"]'
64-
const state = getQueryState(instance, {query})
66+
const state = getQueryState(instance, {query, source, perspective: 'drafts'})
6567

6668
// Initially undefined before subscription
6769
expect(state.getCurrent()).toBeUndefined()
@@ -90,7 +92,7 @@ describe('queryStore', () => {
9092

9193
it('maintains state when multiple subscribers exist', async () => {
9294
const query = '*[_type == "movie"]'
93-
const state = getQueryState(instance, {query})
95+
const state = getQueryState(instance, {query, source, perspective: 'drafts'})
9496

9597
// Add two subscribers
9698
const unsubscribe1 = state.subscribe()
@@ -127,13 +129,13 @@ describe('queryStore', () => {
127129
it('resolveQuery works without affecting subscriber cleanup', async () => {
128130
const query = '*[_type == "movie"]'
129131

130-
const state = getQueryState(instance, {query})
132+
const state = getQueryState(instance, {query, source, perspective: 'drafts'})
131133

132134
// Check that getQueryState starts undefined
133135
expect(state.getCurrent()).toBeUndefined()
134136

135137
// Use resolveQuery which should not add a subscriber
136-
const result = await resolveQuery(instance, {query})
138+
const result = await resolveQuery(instance, {query, source, perspective: 'drafts'})
137139
expect(result).toEqual([
138140
{_id: 'movie1', _type: 'movie', title: 'Movie 1'},
139141
{_id: 'movie2', _type: 'movie', title: 'Movie 2'},
@@ -160,7 +162,12 @@ describe('queryStore', () => {
160162
const abortController = new AbortController()
161163

162164
// Create a promise that will reject when aborted
163-
const queryPromise = resolveQuery(instance, {query, signal: abortController.signal})
165+
const queryPromise = resolveQuery(instance, {
166+
query,
167+
source,
168+
perspective: 'drafts',
169+
signal: abortController.signal,
170+
})
164171

165172
// Abort the request
166173
abortController.abort()
@@ -169,7 +176,9 @@ describe('queryStore', () => {
169176
await expect(queryPromise).rejects.toThrow('The operation was aborted.')
170177

171178
// Verify state is cleared after abort
172-
expect(getQueryState(instance, {query}).getCurrent()).toBeUndefined()
179+
expect(
180+
getQueryState(instance, {query, source, perspective: 'drafts'}).getCurrent(),
181+
).toBeUndefined()
173182
})
174183

175184
it('refetches query when receiving live event with matching sync tag', async () => {
@@ -188,7 +197,11 @@ describe('queryStore', () => {
188197
)
189198

190199
const query = '*[_type == "movie"]'
191-
const state = getQueryState<{_id: string; _type: string; title: string}[]>(instance, {query})
200+
const state = getQueryState<{_id: string; _type: string; title: string}[]>(instance, {
201+
query,
202+
source,
203+
perspective: 'drafts',
204+
})
192205

193206
const unsubscribe = state.subscribe()
194207
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -219,7 +232,7 @@ describe('queryStore', () => {
219232
)
220233

221234
const query = '*[_type == "movie"]'
222-
const state = getQueryState(instance, {query})
235+
const state = getQueryState(instance, {query, source, perspective: 'drafts'})
223236

224237
const unsubscribe = state.subscribe()
225238
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -252,7 +265,7 @@ describe('queryStore', () => {
252265
)
253266

254267
const query = '*[_type == "movie"]'
255-
const state = getQueryState(instance, {query})
268+
const state = getQueryState(instance, {query, source, perspective: 'drafts'})
256269

257270
const unsubscribe = state.subscribe()
258271
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -289,7 +302,7 @@ describe('queryStore', () => {
289302
)
290303

291304
const query = '*[_type == "movie"]'
292-
const state = getQueryState(instance, {query})
305+
const state = getQueryState(instance, {query, source, perspective: 'drafts'})
293306
const unsubscribe = state.subscribe()
294307

295308
// Verify error is thrown when accessing state
@@ -300,7 +313,7 @@ describe('queryStore', () => {
300313

301314
it('delays query state removal after unsubscribe', async () => {
302315
const query = '*[_type == "movie"]'
303-
const state = getQueryState(instance, {query})
316+
const state = getQueryState(instance, {query, source, perspective: 'drafts'})
304317
const unsubscribe = state.subscribe()
305318

306319
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -316,7 +329,7 @@ describe('queryStore', () => {
316329

317330
it('preserves query state if a new subscriber subscribes before cleanup delay', async () => {
318331
const query = '*[_type == "movie"]'
319-
const state = getQueryState(instance, {query})
332+
const state = getQueryState(instance, {query, source, perspective: 'drafts'})
320333
const unsubscribe1 = state.subscribe()
321334

322335
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
@@ -352,22 +365,16 @@ describe('queryStore', () => {
352365
SanityClient['observable']['fetch']
353366
>
354367
}) as SanityClient['observable']['fetch'])
355-
356-
const draftsInstance = createSanityInstance({
357-
projectId: 'test',
358-
dataset: 'test',
368+
// Same query/options, different implicit perspectives via instance.config
369+
const sDrafts = getQueryState<{_id: string}[]>(instance, {
370+
query: '*[_type == "movie"]',
371+
source,
359372
perspective: 'drafts',
360373
})
361-
const publishedInstance = createSanityInstance({
362-
projectId: 'test',
363-
dataset: 'test',
364-
perspective: 'published',
365-
})
366-
367-
// Same query/options, different implicit perspectives via instance.config
368-
const sDrafts = getQueryState<{_id: string}[]>(draftsInstance, {query: '*[_type == "movie"]'})
369-
const sPublished = getQueryState<{_id: string}[]>(publishedInstance, {
374+
const sPublished = getQueryState<{_id: string}[]>(instance, {
370375
query: '*[_type == "movie"]',
376+
source,
377+
perspective: 'published',
371378
})
372379

373380
const unsubDrafts = sDrafts.subscribe()
@@ -385,9 +392,6 @@ describe('queryStore', () => {
385392

386393
unsubDrafts()
387394
unsubPublished()
388-
389-
draftsInstance.dispose()
390-
publishedInstance.dispose()
391395
})
392396

393397
it('separates cache entries by explicit perspective in options', async () => {
@@ -403,10 +407,12 @@ describe('queryStore', () => {
403407

404408
const sDrafts = getQueryState<{_id: string}[]>(base, {
405409
query: '*[_type == "movie"]',
410+
source,
406411
perspective: 'drafts',
407412
})
408413
const sPublished = getQueryState<{_id: string}[]>(base, {
409414
query: '*[_type == "movie"]',
415+
source,
410416
perspective: 'published',
411417
})
412418

packages/core/src/query/queryStore.ts

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {CorsOriginError, type ResponseQueryOptions} from '@sanity/client'
1+
import {type ClientPerspective, CorsOriginError, type ResponseQueryOptions} from '@sanity/client'
22
import {type SanityQueryResult} from 'groq'
33
import {
44
catchError,
@@ -23,7 +23,7 @@ import {
2323
} from 'rxjs'
2424

2525
import {getClientState} from '../client/clientStore'
26-
import {type DatasetHandle} from '../config/sanityConfig'
26+
import {type DocumentSource, type ReleasePerspective} from '../config/sanityConfig'
2727
import {getPerspectiveState} from '../releases/getPerspectiveState'
2828
import {bindActionByDataset, type BoundDatasetKey} from '../store/createActionBinder'
2929
import {type SanityInstance} from '../store/createSanityInstance'
@@ -54,33 +54,28 @@ import {
5454
/**
5555
* @beta
5656
*/
57-
export interface QueryOptions<
58-
TQuery extends string = string,
59-
TDataset extends string = string,
60-
TProjectId extends string = string,
61-
> extends Pick<ResponseQueryOptions, 'useCdn' | 'cache' | 'next' | 'cacheMode' | 'tag'>,
62-
DatasetHandle<TDataset, TProjectId> {
57+
export interface QueryOptions<TQuery extends string = string>
58+
extends Pick<ResponseQueryOptions, 'useCdn' | 'cache' | 'next' | 'cacheMode' | 'tag'> {
6359
query: TQuery
6460
params?: Record<string, unknown>
61+
source: DocumentSource
62+
perspective: ClientPerspective | ReleasePerspective
6563
}
6664

6765
/**
6866
* @beta
6967
*/
70-
export interface ResolveQueryOptions<
71-
TQuery extends string = string,
72-
TDataset extends string = string,
73-
TProjectId extends string = string,
74-
> extends QueryOptions<TQuery, TDataset, TProjectId> {
68+
export interface ResolveQueryOptions<TQuery extends string = string> extends QueryOptions<TQuery> {
7569
signal?: AbortSignal
7670
}
7771

7872
const EMPTY_ARRAY: never[] = []
7973

8074
/** @beta */
81-
export const getQueryKey = (options: QueryOptions): string => JSON.stringify(options)
75+
export const getQueryKey = (options: Omit<QueryOptions, 'source'>): string =>
76+
JSON.stringify(options)
8277
/** @beta */
83-
export const parseQueryKey = (key: string): QueryOptions => JSON.parse(key)
78+
export const parseQueryKey = (key: string): Omit<QueryOptions, 'source'> => JSON.parse(key)
8479

8580
/**
8681
* Ensures the query key includes an effective perspective so that
@@ -276,7 +271,7 @@ export function getQueryState<
276271
TProjectId extends string = string,
277272
>(
278273
instance: SanityInstance,
279-
queryOptions: QueryOptions<TQuery, TDataset, TProjectId>,
274+
queryOptions: QueryOptions<TQuery>,
280275
): StateSource<SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`> | undefined>
281276

282277
/** @beta */
@@ -334,7 +329,7 @@ const _getQueryState = bindActionByDataset(
334329
*/
335330
export function getQueryErrorState(
336331
instance: SanityInstance,
337-
options: {projectId?: string; dataset?: string} = {},
332+
options: {source: DocumentSource},
338333
): StateSource<unknown | undefined> {
339334
return _getQueryErrorState(instance, options)
340335
}
@@ -370,7 +365,7 @@ export function resolveQuery<
370365
TProjectId extends string = string,
371366
>(
372367
instance: SanityInstance,
373-
queryOptions: ResolveQueryOptions<TQuery, TDataset, TProjectId>,
368+
queryOptions: ResolveQueryOptions<TQuery>,
374369
): Promise<SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>>
375370

376371
/** @beta */
@@ -428,10 +423,7 @@ const _resolveQuery = bindActionByDataset(
428423
* Clears the top-level query store error.
429424
* @beta
430425
*/
431-
export function clearQueryError(
432-
instance: SanityInstance,
433-
options: {projectId?: string; dataset?: string} = {},
434-
): void {
426+
export function clearQueryError(instance: SanityInstance, options: {source: DocumentSource}): void {
435427
return _clearQueryError(instance, options)
436428
}
437429

0 commit comments

Comments
 (0)