Skip to content

Commit 4f7a66c

Browse files
committed
add strong typing enforcement for client helper and infinite query hooks
1 parent f0e0534 commit 4f7a66c

File tree

11 files changed

+685
-64
lines changed

11 files changed

+685
-64
lines changed

examples/vite/src/App.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ await setupWorker(...getMockHandlers()).start({
1414
});
1515

1616
const builder = new HttpQueryBuilder({
17-
queryClient,
1817
syncChannel: new BroadcastChannel('react-query-builder'),
1918
})
19+
.withClient(queryClient)
2020
.withBaseUrl(baseUrl)
2121
.withTagTypes<{
2222
post: PostData;
@@ -32,11 +32,9 @@ const postsQuery = builder
3232
.withPath('/posts')
3333
.withData<PostData[]>()
3434
.withSearch<{ page?: number }>()
35-
.withConfig({
36-
options: {
37-
getInitialPageParam: { search: { page: 0 } },
38-
getNextPageParam: (prev, __, lastVars) => (!prev?.length ? null : { search: { page: (lastVars?.search?.page || 0) + 1 } }),
39-
},
35+
.withPagination({
36+
getInitialPageParam: { search: { page: 0 } },
37+
getNextPageParam: (prev, __, lastVars) => (!prev?.length ? null : { search: { page: (lastVars?.search?.page || 0) + 1 } }),
4038
});
4139

4240
const postQuery = builder

packages/react-query-builder/src/builder/HttpQueryBuilder.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { QueryClient } from '@tanstack/react-query';
12
import type { RequestError } from '../http/errors';
23
import type { ExtractPathParams, HttpMethod } from '../http/types';
34
import type { WithOptional } from '../type-utils';
45
import { QueryBuilder } from './QueryBuilder';
56
import { createHttpMergeVarsFn, createHttpQueryFn, createHttpQueryKeySanitizer } from './http-utils';
6-
import type { BuilderConfig } from './types';
7+
import { BuilderPaginationOptions } from './options';
8+
import type { BuilderConfig, BuilderFlags } from './types';
79
import type { HttpBaseHeaders, HttpBaseParams, HttpBaseSearch, HttpBuilderVars } from './types';
810

911
export class HttpQueryBuilder<
@@ -15,8 +17,9 @@ export class HttpQueryBuilder<
1517
TData = unknown,
1618
TError = RequestError,
1719
TTags extends Record<string, unknown> = Record<string, unknown>,
20+
TFlags extends BuilderFlags = '',
1821
TKey extends [HttpBuilderVars] = [HttpBuilderVars<TParam, TSearch, TBody, THeader, TMeta>],
19-
> extends QueryBuilder<HttpBuilderVars<TParam, TSearch, TBody, THeader, TMeta>, TData, TError, TKey, TTags> {
22+
> extends QueryBuilder<HttpBuilderVars<TParam, TSearch, TBody, THeader, TMeta>, TData, TError, TKey, TTags, TFlags> {
2023
constructor(
2124
config?: WithOptional<BuilderConfig<HttpBuilderVars<TParam, TSearch, TBody, THeader, TMeta>, TData, TError, TKey>, 'queryFn'>,
2225
) {
@@ -26,33 +29,33 @@ export class HttpQueryBuilder<
2629
super({ mergeVars, queryFn, queryKeySanitizer, ...config });
2730
}
2831

29-
withBody<TBody$>(body?: TBody$): HttpQueryBuilder<TParam, TSearch, TBody$, THeader, TMeta, TData, TError, TTags> {
32+
withBody<TBody$>(body?: TBody$): HttpQueryBuilder<TParam, TSearch, TBody$, THeader, TMeta, TData, TError, TTags, TFlags> {
3033
if (!body) return this as any;
3134
return this.withVars({ body }) as any;
3235
}
3336

3437
withHeaders<THeaders$ extends HttpBaseHeaders>(
3538
headers?: THeaders$,
36-
): HttpQueryBuilder<TParam, TSearch, TBody, THeaders$, TMeta, TData, TError, TTags> {
39+
): HttpQueryBuilder<TParam, TSearch, TBody, THeaders$, TMeta, TData, TError, TTags, TFlags> {
3740
if (!headers) return this as any;
3841
return this.withVars({ headers }) as any;
3942
}
4043

4144
withParams<TParams$ extends HttpBaseParams>(
4245
params?: TParams$,
43-
): HttpQueryBuilder<TParams$, TSearch, TBody, THeader, TMeta, TData, TError, TTags> {
46+
): HttpQueryBuilder<TParams$, TSearch, TBody, THeader, TMeta, TData, TError, TTags, TFlags> {
4447
if (!params) return this as any;
4548
return this.withVars({ params }) as any;
4649
}
4750

4851
withSearch<TSearch$ extends HttpBaseSearch>(
4952
search?: TSearch$,
50-
): HttpQueryBuilder<TParam, TSearch$, TBody, THeader, TMeta, TData, TError, TTags> {
53+
): HttpQueryBuilder<TParam, TSearch$, TBody, THeader, TMeta, TData, TError, TTags, TFlags> {
5154
if (!search) return this as any;
5255
return this.withVars({ search }) as any;
5356
}
5457

55-
withMeta<TMeta$>(meta?: TMeta$): HttpQueryBuilder<TParam, TSearch, TBody, THeader, TMeta$, TData, TError, TTags> {
58+
withMeta<TMeta$>(meta?: TMeta$): HttpQueryBuilder<TParam, TSearch, TBody, THeader, TMeta$, TData, TError, TTags, TFlags> {
5659
if (!meta) return this as any;
5760
return this.withVars({ meta }) as any;
5861
}
@@ -61,7 +64,7 @@ export class HttpQueryBuilder<
6164
path: TPath$,
6265
): ExtractPathParams<TPath$> extends void
6366
? this
64-
: HttpQueryBuilder<ExtractPathParams<TPath$>, TSearch, TBody, THeader, TMeta, TData, TError, TTags> {
67+
: HttpQueryBuilder<ExtractPathParams<TPath$>, TSearch, TBody, THeader, TMeta, TData, TError, TTags, TFlags> {
6568
return this.withVars({ path }) as any;
6669
}
6770

@@ -73,8 +76,15 @@ export class HttpQueryBuilder<
7376
return this.withVars({ method }) as any;
7477
}
7578

76-
declare withData: <TData$>() => HttpQueryBuilder<TParam, TSearch, TBody, THeader, TMeta, TData$, TError, TTags, TKey>;
77-
declare withError: <TError$>() => HttpQueryBuilder<TParam, TSearch, TBody, THeader, TMeta, TData, TError$, TTags, TKey>;
79+
declare withData: <TData$>() => HttpQueryBuilder<TParam, TSearch, TBody, THeader, TMeta, TData$, TError, TTags, TFlags, TKey>;
80+
declare withError: <TError$>() => HttpQueryBuilder<TParam, TSearch, TBody, THeader, TMeta, TData, TError$, TTags, TFlags, TKey>;
81+
declare withClient: (
82+
queryClient: QueryClient,
83+
) => HttpQueryBuilder<TParam, TSearch, TBody, THeader, TMeta, TData, TError, TTags, TFlags | 'withClient', TKey>;
84+
85+
declare withPagination: (
86+
paginationConfig: BuilderPaginationOptions<HttpBuilderVars<TParam, TSearch, TBody, THeader, TMeta>, TData, TError, TKey>,
87+
) => HttpQueryBuilder<TParam, TSearch, TBody, THeader, TMeta, TData, TError, TTags, TFlags | 'withPagination', TKey>;
7888

7989
withTagTypes<TTag extends string, T = unknown>(): HttpQueryBuilder<
8090
TParam,
@@ -85,6 +95,7 @@ export class HttpQueryBuilder<
8595
TData,
8696
TError,
8797
TTags & Record<TTag, T>,
98+
TFlags,
8899
TKey
89100
>;
90101
withTagTypes<TTags$ extends Record<string, unknown>>(): HttpQueryBuilder<
@@ -96,6 +107,7 @@ export class HttpQueryBuilder<
96107
TData,
97108
TError,
98109
TTags$,
110+
TFlags,
99111
TKey
100112
>;
101113
withTagTypes(): this {

packages/react-query-builder/src/builder/QueryBuilder.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,39 @@
1+
import { QueryClient } from '@tanstack/react-query';
12
import { operateOnTags } from '../tags/operateOnTags';
23
import type { QueryTagObject, QueryTagOption, QueryUpdateTagObject } from '../tags/types';
34
import { QueryBuilderFrozen } from './QueryBuilderFrozen';
45
import { type MiddlewareFn, createMiddlewareFunction } from './createMiddlewareFunction';
56
import { type PreprocessorFn, createPreprocessorFunction, identityPreprocessor } from './createPreprocessorFunction';
67
import { createTagMiddleware } from './createTagMiddleware';
78
import { createUpdateMiddleware } from './createUpdateMiddleware';
8-
import type { BuilderConfig } from './types';
9+
import { BuilderPaginationOptions } from './options';
10+
import type { BuilderConfig, BuilderFlags } from './types';
911
import { mergeVars } from './utils';
1012

11-
export class QueryBuilder<TVars, TData, TError, TKey extends unknown[], TTags extends Record<string, unknown>> extends QueryBuilderFrozen<
13+
export class QueryBuilder<
1214
TVars,
1315
TData,
1416
TError,
15-
TKey,
16-
TTags
17-
> {
17+
TKey extends unknown[],
18+
TTags extends Record<string, unknown>,
19+
TFlags extends BuilderFlags = '',
20+
> extends QueryBuilderFrozen<TVars, TData, TError, TKey, TTags, TFlags> {
1821
withVars<TVars$ = TVars, const TReset extends boolean = false>(
1922
vars?: TVars$,
2023
resetVars = false as TReset,
21-
): QueryBuilder<TVars$, TData, TError, TKey, TTags> {
24+
): QueryBuilder<TVars$, TData, TError, TKey, TTags, TFlags> {
2225
if (!vars) return this as any;
2326

2427
return this.withConfig({
2528
vars: resetVars ? vars : mergeVars([this.config.vars, vars], this.config.mergeVars),
2629
}) as any;
2730
}
2831

29-
withData<TData$>(): QueryBuilder<TVars, TData$, TError, TKey, TTags> {
32+
withData<TData$>(): QueryBuilder<TVars, TData$, TError, TKey, TTags, TFlags> {
3033
return this as any;
3134
}
3235

33-
withError<TError$>(): QueryBuilder<TVars, TData, TError$, TKey, TTags> {
36+
withError<TError$>(): QueryBuilder<TVars, TData, TError$, TKey, TTags, TFlags> {
3437
return this as any;
3538
}
3639

@@ -41,10 +44,10 @@ export class QueryBuilder<TVars, TData, TError, TKey extends unknown[], TTags ex
4144
}
4245

4346
withPreprocessor(preprocessor: PreprocessorFn<TVars, TVars>): this;
44-
withPreprocessor<TVars$ = TVars>(preprocessor: PreprocessorFn<TVars$, TVars>): QueryBuilder<TVars$, TData, TError, TKey, TTags>;
47+
withPreprocessor<TVars$ = TVars>(preprocessor: PreprocessorFn<TVars$, TVars>): QueryBuilder<TVars$, TData, TError, TKey, TTags, TFlags>;
4548

46-
withPreprocessor<TVars$ = TVars>(preprocessor: PreprocessorFn<TVars$, TVars>): QueryBuilder<TVars$, TData, TError, TKey, TTags> {
47-
const newBuilder = this as unknown as QueryBuilder<TVars$, TData, TError, TKey, TTags>;
49+
withPreprocessor<TVars$ = TVars>(preprocessor: PreprocessorFn<TVars$, TVars>): QueryBuilder<TVars$, TData, TError, TKey, TTags, TFlags> {
50+
const newBuilder = this as unknown as QueryBuilder<TVars$, TData, TError, TKey, TTags, TFlags>;
4851

4952
return newBuilder.withConfig({
5053
preprocessorFn: createPreprocessorFunction(preprocessor, this.config.preprocessorFn || identityPreprocessor),
@@ -54,13 +57,13 @@ export class QueryBuilder<TVars, TData, TError, TKey extends unknown[], TTags ex
5457
withMiddleware(middleware: MiddlewareFn<TVars, TData, TError, TKey>): this;
5558
withMiddleware<TVars$ = TVars, TData$ = TData, TError$ = TError>(
5659
middleware: MiddlewareFn<TVars$, TData$, TError$, TKey>,
57-
): QueryBuilder<TVars$, TData$, TError$, TKey, TTags>;
60+
): QueryBuilder<TVars$, TData$, TError$, TKey, TTags, TFlags>;
5861

5962
withMiddleware<TVars$ = TVars, TData$ = TData, TError$ = TError>(
6063
middleware: MiddlewareFn<TVars$, TData$, TError$, TKey>,
6164
config?: Partial<BuilderConfig<TVars$, TData$, TError$, TKey>>,
62-
): QueryBuilder<TVars$, TData$, TError$, TKey, TTags> {
63-
const newBuilder = this as unknown as QueryBuilder<TVars$, TData$, TError$, TKey, TTags>;
65+
): QueryBuilder<TVars$, TData$, TError$, TKey, TTags, TFlags> {
66+
const newBuilder = this as unknown as QueryBuilder<TVars$, TData$, TError$, TKey, TTags, TFlags>;
6467

6568
return newBuilder.withConfig({
6669
...config,
@@ -88,13 +91,23 @@ export class QueryBuilder<TVars, TData, TError, TKey extends unknown[], TTags ex
8891
return this.withMiddleware(createUpdateMiddleware<TVars, TData, TError, TKey, TTags>(tags)) as unknown as this;
8992
}
9093

91-
withTagTypes<TTag extends string, T = unknown>(): QueryBuilder<TVars, TData, TError, TKey, TTags & Record<TTag, T>>;
92-
withTagTypes<TTags$ extends Record<string, unknown>>(): QueryBuilder<TVars, TData, TError, TKey, TTags$>;
94+
withTagTypes<TTag extends string, T = unknown>(): QueryBuilder<TVars, TData, TError, TKey, TTags & Record<TTag, T>, TFlags>;
95+
withTagTypes<TTags$ extends Record<string, unknown>>(): QueryBuilder<TVars, TData, TError, TKey, TTags$, TFlags>;
9396
withTagTypes(): this {
9497
return this as any;
9598
}
9699

97-
freeze(): QueryBuilderFrozen<TVars, TData, TError, TKey, TTags> {
100+
withClient(queryClient: QueryClient): QueryBuilder<TVars, TData, TError, TKey, TTags, TFlags | 'withClient'> {
101+
return this.withConfig({ queryClient }) as any;
102+
}
103+
104+
withPagination(
105+
paginationConfig: BuilderPaginationOptions<TVars, TData, TError, TKey>,
106+
): QueryBuilder<TVars, TData, TError, TKey, TTags, TFlags | 'withPagination'> {
107+
return this.withConfig({ options: paginationConfig }) as any;
108+
}
109+
110+
freeze(): QueryBuilderFrozen<TVars, TData, TError, TKey, TTags, TFlags> {
98111
return this;
99112
}
100113
}

packages/react-query-builder/src/builder/QueryBuilderClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class QueryBuilderClient<
2121
TFilters = QueryFilters<TData, TError, TData, TKey>,
2222
> {
2323
private declare _options: BuilderConfig<TVars, TData, TError, TKey>['options'];
24-
constructor(private builder: QueryBuilderFrozen<TVars, TData, TError, TKey, TTags>) {}
24+
constructor(private builder: QueryBuilderFrozen<TVars, TData, TError, TKey, TTags, any>) {}
2525

2626
readonly ensureData = (vars: TVars, opts?: typeof this._options) =>
2727
this.builder.config.queryClient?.ensureQueryData(this.builder.getQueryOptions(vars, opts));

packages/react-query-builder/src/builder/QueryBuilderFrozen.ts

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import type { FunctionType, TODO, WithRequired } from '../type-utils';
3232
import { QueryBuilderClient } from './QueryBuilderClient';
3333
import { QueryBuilderTagsManager } from './QueryBuilderTagsManager';
3434
import { type BuilderOptions, mergeBuilderOptions } from './options';
35-
import type { BuilderConfig, BuilderQueriesResult } from './types';
35+
import type { BuilderConfig, BuilderFlags, BuilderQueriesResult, HasClient, HasPagination } from './types';
3636
import { areKeysEqual, getRandomKey, mergeMutationOptions, mergeVars } from './utils';
3737

3838
export class QueryBuilderFrozen<
@@ -41,6 +41,7 @@ export class QueryBuilderFrozen<
4141
TError,
4242
TKey extends unknown[],
4343
TTags extends Record<string, unknown> = Record<string, unknown>,
44+
TFlags extends BuilderFlags = '',
4445
> {
4546
protected declare _options: BuilderOptions<TVars, TData, TError, TKey>;
4647

@@ -177,12 +178,7 @@ export class QueryBuilderFrozen<
177178

178179
//#region InfiniteQuery
179180

180-
getInfiniteQueryOptions: (
181-
vars: TVars,
182-
opts?: typeof this._options,
183-
) => UseInfiniteQueryOptions<TData, TError, InfiniteData<TData, Partial<TVars>>, TData, TKey, Partial<TVars>> & {
184-
queryFn: FunctionType;
185-
} = (vars, opts) => {
181+
getInfiniteQueryOptions = ((vars, opts) => {
186182
// TODO: eventually allow these options as well
187183
const {
188184
enabled,
@@ -204,25 +200,33 @@ export class QueryBuilderFrozen<
204200
initialPageParam: typeof getInitialPageParam === 'function' ? getInitialPageParam() : getInitialPageParam!,
205201
getNextPageParam: options.getNextPageParam!,
206202
};
207-
};
203+
}) as HasPagination<
204+
TFlags,
205+
(
206+
vars: TVars,
207+
opts?: typeof this._options,
208+
) => UseInfiniteQueryOptions<TData, TError, InfiniteData<TData, Partial<TVars>>, TData, TKey, Partial<TVars>> & {
209+
queryFn: FunctionType;
210+
}
211+
>;
208212

209-
useInfiniteQuery: (vars: TVars, opts?: typeof this._options) => UseInfiniteQueryResult<InfiniteData<TData, Partial<TVars>>, TError> = (
210-
vars,
211-
opts,
212-
) => {
213+
useInfiniteQuery = ((vars, opts) => {
213214
return useInfiniteQuery(this.getInfiniteQueryOptions(vars, opts), this.config.queryClient);
214-
};
215+
}) as HasPagination<
216+
TFlags,
217+
(vars: TVars, opts?: typeof this._options) => UseInfiniteQueryResult<InfiniteData<TData, Partial<TVars>>, TError>
218+
>;
215219

216-
usePrefetchInfiniteQuery: (vars: TVars, opts?: typeof this._options) => void = (vars, opts) => {
220+
usePrefetchInfiniteQuery = ((vars, opts) => {
217221
return usePrefetchInfiniteQuery(this.getInfiniteQueryOptions(vars, opts), this.config.queryClient);
218-
};
222+
}) as HasPagination<TFlags, (vars: TVars, opts?: typeof this._options) => void>;
219223

220-
useSuspenseInfiniteQuery: (
221-
vars: TVars,
222-
opts?: typeof this._options,
223-
) => UseSuspenseInfiniteQueryResult<InfiniteData<TData, Partial<TVars>>, TError> = (vars, opts) => {
224+
useSuspenseInfiniteQuery = ((vars, opts) => {
224225
return useSuspenseInfiniteQuery(this.getInfiniteQueryOptions(vars, opts), this.config.queryClient);
225-
};
226+
}) as HasPagination<
227+
TFlags,
228+
(vars: TVars, opts?: typeof this._options) => UseSuspenseInfiniteQueryResult<InfiniteData<TData, Partial<TVars>>, TError>
229+
>;
226230

227231
//#endregion
228232

@@ -301,8 +305,8 @@ export class QueryBuilderFrozen<
301305
//#endregion
302306

303307
private _client?: QueryBuilderClient<TVars, TData, TError, TKey, TTags>;
304-
get client() {
305-
return (this._client ??= new QueryBuilderClient(this));
308+
get client(): HasClient<TFlags, QueryBuilderClient<TVars, TData, TError, TKey, TTags>> {
309+
return (this._client ??= new QueryBuilderClient(this)) as any;
306310
}
307311

308312
private _tags?: QueryBuilderTagsManager<TVars, TData, TError, TKey, TTags>;

packages/react-query-builder/src/builder/options.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import type { GetNextPageParamFunction, GetPreviousPageParamFunction, UseMutatio
22
import type { FunctionType, WithOptional } from '../type-utils';
33
import { mergeQueryEnabled } from './utils';
44

5+
export type BuilderPaginationOptions<TVars, TData, TError, TKey extends unknown[]> = {
6+
getNextPageParam: GetNextPageParamFunction<Partial<TVars>, TData>;
7+
getPreviousPageParam?: GetPreviousPageParamFunction<Partial<TVars>, TData>;
8+
getInitialPageParam?: Partial<TVars> | (() => Partial<TVars>);
9+
};
10+
511
export type BuilderOptions<TVars, TData, TError, TKey extends unknown[]> = WithOptional<
612
UseQueryOptions<TData, TError, TData, TKey>,
713
'queryFn' | 'queryKey'
8-
> & {
9-
queryFn?: FunctionType;
10-
getInitialPageParam?: Partial<TVars> | (() => Partial<TVars>);
11-
getPreviousPageParam?: GetPreviousPageParamFunction<Partial<TVars>, TData>;
12-
getNextPageParam?: GetNextPageParamFunction<Partial<TVars>, TData>;
13-
} & Pick<
14+
> & { queryFn?: FunctionType } & Partial<BuilderPaginationOptions<TVars, TData, TError, TKey>> &
15+
Pick<
1416
UseMutationOptions<TData, TError, TVars>,
1517
'onError' | 'onMutate' | 'onSettled' | 'onSuccess' | 'gcTime' | 'mutationKey' | 'networkMode' | 'retry' | 'retryDelay' | 'throwOnError'
1618
>;

packages/react-query-builder/src/builder/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { QueryClient, QueryFunctionContext, QueryKey, UseQueryResult } from '@tanstack/react-query';
22
import type { HttpRequestOptions } from '../http/types';
3-
import type { Prettify } from '../type-utils';
3+
import type { HasFlag, Prettify } from '../type-utils';
44
import type { BuilderOptions } from './options';
55

66
export type BuilderConfig<TVars, TData, TError, TKey extends unknown[]> = {
@@ -55,3 +55,8 @@ export type BuilderQueryContext<TQueryKey extends unknown[]> = QueryFunctionCont
5555
export type BuilderQueryFn<TVars, TData, TError, TKey extends unknown[]> = (context: BuilderQueryContext<TKey>) => TData | Promise<TData>;
5656

5757
export type BuilderKeySanitizerFn<TKey extends unknown[]> = (key: TKey) => QueryKey;
58+
59+
export type HasClient<TFlags extends BuilderFlags, TTruthy = true> = HasFlag<TFlags, 'withClient', TTruthy>;
60+
export type HasPagination<TFlags extends BuilderFlags, TTruthy = true> = HasFlag<TFlags, 'withPagination', TTruthy>;
61+
62+
export type BuilderFlags = '' | 'withClient' | 'withPagination';

packages/react-query-builder/src/type-utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ export type FunctionType = (...args: any[]) => any;
1818
export type KeysOfValue<T, TCondition> = {
1919
[K in keyof T]: T[K] extends TCondition ? K : never;
2020
}[keyof T];
21+
22+
export type HasFlag<T extends string, TFlag extends string, TTrue = true, TFalse = never> = T extends `${string}${TFlag}${string}`
23+
? TTrue
24+
: TFalse;

0 commit comments

Comments
 (0)