Skip to content

Commit 79bed2b

Browse files
committed
feat: start TanStack Query React integration
1 parent 0067817 commit 79bed2b

File tree

12 files changed

+1062
-56
lines changed

12 files changed

+1062
-56
lines changed

packages/tanstack-query/package.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,29 @@
2828
"@zenstackhq/runtime": "workspace:*"
2929
},
3030
"devDependencies": {
31+
"@testing-library/dom": "^10.4.1",
32+
"@testing-library/jest-dom": "^6.9.1",
33+
"@testing-library/react": "^16.3.0",
34+
"@types/react": "catalog:",
35+
"@types/react-dom": "catalog:",
3136
"@zenstackhq/eslint-config": "workspace:*",
32-
"@zenstackhq/typescript-config": "workspace:*"
37+
"@zenstackhq/typescript-config": "workspace:*",
38+
"jsdom": "^27.0.1"
3339
},
3440
"peerDependencies": {
35-
"@tanstack/react-query": "^5.0.0"
41+
"@tanstack/react-query": "^5.0.0",
42+
"react": "catalog:",
43+
"react-dom": "catalog:"
3644
},
3745
"peerDependenciesMeta": {
3846
"@tanstack/react-query": {
3947
"optional": true
48+
},
49+
"react": {
50+
"optional": true
51+
},
52+
"react-dom": {
53+
"optional": true
4054
}
4155
}
4256
}
Lines changed: 75 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
import type {
2-
DefaultError,
3-
UseMutationOptions,
4-
UseMutationResult,
5-
UseQueryOptions,
6-
UseQueryResult,
1+
import {
2+
useMutation,
3+
useQuery,
4+
type DefaultError,
5+
type UseMutationOptions,
6+
type UseMutationResult,
7+
type UseQueryOptions,
8+
type UseQueryResult,
79
} from '@tanstack/react-query';
810
import type { CreateArgs, FindArgs, ModelResult, SelectSubset } from '@zenstackhq/runtime';
9-
import type { GetModels, SchemaDef } from '@zenstackhq/runtime/schema';
11+
import { type GetModels, type SchemaDef } from '@zenstackhq/runtime/schema';
12+
import { useContext } from 'react';
13+
import { getQueryKey, type MutationMethod, type MutationOperation, type QueryOperation } from './runtime/common';
14+
import { RequestHandlerContext } from './runtime/react';
1015

11-
export type toHooks<Schema extends SchemaDef> = {
12-
[Model in GetModels<Schema> as Uncapitalize<Model>]: ToModelHooks<Schema, Model>;
16+
export type useHooks<Schema extends SchemaDef> = {
17+
[Model in GetModels<Schema> as Uncapitalize<Model>]: UseModelHooks<Schema, Model>;
1318
};
1419

15-
type ToModelHooks<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
16-
findMany<T extends FindArgs<Schema, Model, true>>(
20+
type UseModelHooks<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
21+
useFindFirst<T extends FindArgs<Schema, Model, true>>(
1722
args?: SelectSubset<T, FindArgs<Schema, Model, true>>,
18-
options?: Omit<UseQueryOptions<ModelResult<Schema, Model, T>[]>, 'queryKey'>,
19-
): UseQueryResult<ModelResult<Schema, Model, T>[]>;
20-
21-
findFirst<T extends FindArgs<Schema, Model, true>>(
22-
args?: SelectSubset<T, FindArgs<Schema, Model, true>>,
23-
options?: Omit<UseQueryOptions<ModelResult<Schema, Model, T>[]>, 'queryKey'>,
23+
options?: Omit<UseQueryOptions<ModelResult<Schema, Model, T>[]>, 'queryKey' | 'queryFn'>,
2424
): UseQueryResult<ModelResult<Schema, Model, T> | null>;
2525

26-
create<T extends CreateArgs<Schema, Model>>(
26+
useCreate<T extends CreateArgs<Schema, Model>>(
2727
options?: UseMutationOptions<ModelResult<Schema, Model, T>, DefaultError, T>,
2828
): UseMutationResult<ModelResult<Schema, Model, T>, DefaultError, T>;
2929
};
@@ -32,47 +32,77 @@ function uncapitalize(s: string) {
3232
return s.charAt(0).toLowerCase() + s.slice(1);
3333
}
3434

35-
export function toHooks<Schema extends SchemaDef>(schema: Schema): toHooks<Schema> {
35+
export function useHooks<Schema extends SchemaDef>(schema: Schema): useHooks<Schema> {
3636
return Object.entries(schema.models).reduce(
3737
(acc, [model, _]) =>
3838
Object.assign(acc, {
39-
[uncapitalize(model)]: toModelHooks(schema, model as GetModels<Schema>),
39+
[uncapitalize(model)]: useModelHooks(schema, model as GetModels<Schema>),
4040
}),
41-
{} as toHooks<Schema>,
41+
{} as useHooks<Schema>,
4242
);
4343
}
4444

45-
function toModelHooks<Schema extends SchemaDef, Model extends GetModels<Schema>>(schema: Schema, model: Model): any {
45+
function useModelHooks<Schema extends SchemaDef, Model extends GetModels<Schema>>(schema: Schema, model: Model): any {
4646
const modelDef = schema.models[model];
4747
if (!modelDef) {
4848
throw new Error(`Model ${model} not found in schema`);
4949
}
5050

5151
return {
52-
findMany: () => {
53-
return {
54-
data: [],
55-
isLoading: false,
56-
isError: false,
57-
};
58-
},
52+
useFindFirst: useModelQuery(schema, model, 'findFirst'),
53+
useCreate: useModelMutation(schema, model, 'create', 'POST'),
54+
};
55+
}
5956

60-
findFirst: () => {
61-
return {
62-
data: null,
63-
isLoading: false,
64-
isError: false,
65-
};
66-
},
57+
export function useModelQuery<
58+
Schema extends SchemaDef,
59+
Model extends GetModels<Schema>,
60+
>(
61+
schema: Schema,
62+
model: Model,
63+
operation: QueryOperation,
64+
) {
65+
const context = useContext(RequestHandlerContext);
66+
if (!context) {
67+
throw new Error('Missing context');
68+
}
69+
70+
const queryKey = getQueryKey(schema, model, operation, {});
71+
const query = useQuery({
72+
queryKey,
73+
queryFn: async () => {
74+
const response = await context.fetch!(context.endpoint!);
6775

68-
create: () => {
69-
return {
70-
mutate: async () => {
71-
return null;
72-
},
73-
isLoading: false,
74-
isError: false,
75-
};
76+
return response;
7677
},
77-
};
78+
});
79+
80+
return query;
7881
}
82+
83+
export function useModelMutation<
84+
Schema extends SchemaDef,
85+
Model extends GetModels<Schema>,
86+
>(
87+
schema: Schema,
88+
model: Model,
89+
operation: MutationOperation,
90+
method: MutationMethod,
91+
) {
92+
const context = useContext(RequestHandlerContext);
93+
if (!context) {
94+
throw new Error('Missing context');
95+
}
96+
97+
const mutation = useMutation({
98+
mutationFn: async () => {
99+
const response = await context.fetch!(context.endpoint!, {
100+
method,
101+
});
102+
103+
return response;
104+
}
105+
});
106+
107+
return mutation;
108+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { GetModels, SchemaDef } from '@zenstackhq/runtime/schema';
2+
import type { CrudOperation, FindArgs, AggregateArgs, CountArgs, CreateArgs, UpdateArgs, CreateManyArgs, UpdateManyArgs, CreateManyAndReturnArgs, DeleteArgs, DeleteManyArgs, FindUniqueArgs, GroupByArgs, UpdateManyAndReturnArgs, UpsertArgs } from '@zenstackhq/runtime';
3+
4+
/**
5+
* The default query endpoint.
6+
*/
7+
export const DEFAULT_QUERY_ENDPOINT = '/api/model';
8+
9+
/**
10+
* Prefix for TanStack Query keys.
11+
*/
12+
export const QUERY_KEY_PREFIX = 'zenstack';
13+
14+
/**
15+
* Function signature for `fetch`.
16+
*/
17+
export type FetchFn = (url: string, options?: RequestInit) => Promise<Response>;
18+
19+
/**
20+
* Context type for configuring the hooks.
21+
*/
22+
export type APIContext = {
23+
/**
24+
* The endpoint to use for the queries.
25+
*/
26+
endpoint?: string;
27+
28+
/**
29+
* A custom fetch function for sending the HTTP requests.
30+
*/
31+
fetch?: FetchFn;
32+
33+
/**
34+
* If logging is enabled.
35+
*/
36+
logging?: boolean;
37+
};
38+
39+
/**
40+
* Extra query options.
41+
*/
42+
export type ExtraQueryOptions = {
43+
/**
44+
* Whether this is an infinite query. Defaults to `false`.
45+
*/
46+
infinite?: boolean;
47+
48+
/**
49+
* Whether to opt-in to optimistic updates for this query. Defaults to `true`.
50+
*/
51+
optimisticUpdate?: boolean;
52+
};
53+
54+
export type CrudOperationTypeMap<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
55+
findFirst: FindArgs<Schema, Model, false>,
56+
findMany: FindArgs<Schema, Model, true>,
57+
findUnique: FindUniqueArgs<Schema, Model>,
58+
create: CreateArgs<Schema, Model>,
59+
createMany: CreateManyArgs<Schema, Model>,
60+
createManyAndReturn: CreateManyAndReturnArgs<Schema, Model>,
61+
upsert: UpsertArgs<Schema, Model>,
62+
update: UpdateArgs<Schema, Model>,
63+
updateMany: UpdateManyArgs<Schema, Model>,
64+
updateManyAndReturn: UpdateManyAndReturnArgs<Schema, Model>,
65+
delete: DeleteArgs<Schema, Model>,
66+
deleteMany: DeleteManyArgs<Schema, Model>,
67+
count: CountArgs<Schema, Model>,
68+
aggregate: AggregateArgs<Schema, Model>,
69+
groupBy: GroupByArgs<Schema, Model>,
70+
};
71+
72+
export type QueryOperation = Extract<
73+
CrudOperation,
74+
'findFirst' | 'findMany' | 'findUnique' | 'count' | 'aggregate' | 'groupBy'
75+
>;
76+
77+
export type MutationOperation = Exclude<CrudOperation, QueryOperation>;
78+
79+
export type MutationMethod = 'POST' | 'PUT' | 'DELETE';
80+
81+
export type QueryKey<
82+
Schema extends SchemaDef,
83+
Model extends GetModels<Schema>,
84+
Operation extends CrudOperation,
85+
> = [
86+
prefix: typeof QUERY_KEY_PREFIX,
87+
model: Model,
88+
operation: CrudOperation,
89+
args: CrudOperationTypeMap<Schema, Model>[Operation],
90+
extraOptions: ExtraQueryOptions,
91+
];
92+
93+
export function getQueryKey<
94+
Schema extends SchemaDef,
95+
Model extends GetModels<Schema>,
96+
Operation extends CrudOperation,
97+
>(
98+
schema: Schema,
99+
model: Model,
100+
operation: CrudOperation,
101+
args: CrudOperationTypeMap<Schema, Model>[Operation],
102+
103+
extraOptions: ExtraQueryOptions = {
104+
infinite: false,
105+
optimisticUpdate: true,
106+
},
107+
): QueryKey<Schema, Model, typeof operation> {
108+
const modelDef = schema.models[model];
109+
if (!modelDef) {
110+
throw new Error(`Model ${model} not found in schema`);
111+
}
112+
113+
return [QUERY_KEY_PREFIX, model, operation, args, extraOptions]
114+
}
115+
116+
export function isZenStackQueryKey(
117+
queryKey: readonly unknown[]
118+
): queryKey is QueryKey<SchemaDef, GetModels<SchemaDef>, CrudOperation> {
119+
if (queryKey.length < 5) {
120+
return false;
121+
}
122+
123+
if (queryKey[0] !== QUERY_KEY_PREFIX) {
124+
return false;
125+
}
126+
127+
return true;
128+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export {
2+
getQueryKey,
3+
type ExtraQueryOptions,
4+
type FetchFn,
5+
} from './common';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { DEFAULT_QUERY_ENDPOINT, type APIContext } from './common';
2+
import { createContext, useContext } from 'react';
3+
4+
/**
5+
* Context for configuring react hooks.
6+
*/
7+
export const RequestHandlerContext = createContext<APIContext>({
8+
endpoint: DEFAULT_QUERY_ENDPOINT,
9+
fetch,
10+
});
11+
12+
/**
13+
* Hooks context.
14+
*/
15+
export function getHooksContext() {
16+
const { endpoint, ...rest } = useContext(RequestHandlerContext);
17+
return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest };
18+
}
19+
20+
/**
21+
* Context provider.
22+
*/
23+
export const Provider = RequestHandlerContext.Provider;

0 commit comments

Comments
 (0)