Skip to content

Commit 033d95d

Browse files
authored
feat: Nuxt server adapter and tanstack-query for "vue" hooks generation (#757)
1 parent 22b1bf9 commit 033d95d

File tree

12 files changed

+3731
-219
lines changed

12 files changed

+3731
-219
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,10 @@ The following diagram gives a high-level architecture overview of ZenStack.
158158
### Framework adapters
159159

160160
- [Next.js](https://zenstack.dev/docs/reference/server-adapters/next) (including support for the new "app directory" in Next.js 13)
161+
- [Nuxt](https://zenstack.dev/docs/reference/server-adapters/nuxt)
161162
- [SvelteKit](https://zenstack.dev/docs/reference/server-adapters/sveltekit)
162163
- [Fastify](https://zenstack.dev/docs/reference/server-adapters/fastify)
163164
- [ExpressJS](https://zenstack.dev/docs/reference/server-adapters/express)
164-
- Nuxt.js (Future)
165165
- 🙋🏻 [Request for an adapter](https://go.zenstack.dev/chat)
166166

167167
### Prisma schema extensions
@@ -179,6 +179,7 @@ Check out the [Collaborative Todo App](https://zenstack-todo.vercel.app/) for a
179179
- [Next.js + SWR hooks](https://github.com/zenstackhq/sample-todo-nextjs)
180180
- [Next.js + TanStack Query](https://github.com/zenstackhq/sample-todo-nextjs-tanstack)
181181
- [Next.js + tRPC](https://github.com/zenstackhq/sample-todo-trpc)
182+
- [Nuxt + TanStack Query](https://github.com/zenstackhq/sample-todo-nuxt)
182183
- [SvelteKit + TanStack Query](https://github.com/zenstackhq/sample-todo-sveltekit)
183184

184185
## Community

packages/plugins/tanstack-query/package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
"default": "./runtime/react.js",
2424
"types": "./runtime/react.d.ts"
2525
},
26+
"./runtime/vue": {
27+
"import": "./runtime/vue.mjs",
28+
"require": "./runtime/vue.js",
29+
"default": "./runtime/vue.js",
30+
"types": "./runtime/vue.d.ts"
31+
},
2632
"./runtime/svelte": {
2733
"import": "./runtime/svelte.mjs",
2834
"require": "./runtime/svelte.js",
@@ -55,6 +61,7 @@
5561
"@zenstackhq/runtime": "workspace:*",
5662
"@zenstackhq/sdk": "workspace:*",
5763
"change-case": "^4.1.2",
64+
"cross-fetch": "^4.0.0",
5865
"decimal.js": "^10.4.2",
5966
"lower-case-first": "^2.0.2",
6067
"semver": "^7.3.8",
@@ -63,8 +70,9 @@
6370
"upper-case-first": "^2.0.2"
6471
},
6572
"devDependencies": {
66-
"@tanstack/react-query": "4.29.7",
67-
"@tanstack/svelte-query": "4.29.7",
73+
"@tanstack/react-query": "^4.29.7",
74+
"@tanstack/svelte-query": "^4.29.7",
75+
"@tanstack/vue-query": "^4.37.0",
6876
"@types/jest": "^29.5.0",
6977
"@types/node": "^18.0.0",
7078
"@types/react": "18.2.0",
@@ -77,6 +85,7 @@
7785
"rimraf": "^3.0.2",
7886
"swr": "^2.0.3",
7987
"ts-jest": "^29.0.5",
80-
"typescript": "^4.9.4"
88+
"typescript": "^4.9.4",
89+
"vue": "^3.3.4"
8190
}
8291
}

packages/plugins/tanstack-query/src/generator.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph';
1919
import { upperCaseFirst } from 'upper-case-first';
2020
import { name } from '.';
2121

22-
const supportedTargets = ['react', 'svelte'];
22+
const supportedTargets = ['react', 'vue', 'svelte'];
2323
type TargetFramework = (typeof supportedTargets)[number];
2424

2525
export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) {
@@ -158,6 +158,7 @@ function generateMutationHook(
158158

159159
switch (target) {
160160
case 'react':
161+
case 'vue':
161162
// override the mutateAsync function to return the correct type
162163
func.addVariableStatement({
163164
declarationKind: VariableDeclarationKind.Const,
@@ -416,6 +417,9 @@ function generateIndex(project: Project, outDir: string, models: DataModel[], ta
416417
case 'react':
417418
sf.addStatements(`export { Provider } from '@zenstackhq/tanstack-query/runtime/react';`);
418419
break;
420+
case 'vue':
421+
sf.addStatements(`export { VueQueryContextKey } from '@zenstackhq/tanstack-query/runtime/vue';`);
422+
break;
419423
case 'svelte':
420424
sf.addStatements(`export { SvelteQueryContextKey } from '@zenstackhq/tanstack-query/runtime/svelte';`);
421425
break;
@@ -426,6 +430,8 @@ function makeGetContext(target: TargetFramework) {
426430
switch (target) {
427431
case 'react':
428432
return 'const { endpoint, fetch } = useContext(RequestHandlerContext);';
433+
case 'vue':
434+
return 'const { endpoint, fetch } = getContext();';
429435
case 'svelte':
430436
return `const { endpoint, fetch } = getContext<RequestHandlerContext>(SvelteQueryContextKey);`;
431437
default:
@@ -446,6 +452,12 @@ function makeBaseImports(target: TargetFramework) {
446452
`import { RequestHandlerContext } from '@zenstackhq/tanstack-query/runtime/${target}';`,
447453
...shared,
448454
];
455+
case 'vue':
456+
return [
457+
`import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions } from '@tanstack/vue-query';`,
458+
`import { getContext } from '@zenstackhq/tanstack-query/runtime/${target}';`,
459+
...shared,
460+
];
449461
case 'svelte':
450462
return [
451463
`import { getContext } from 'svelte';`,
@@ -462,6 +474,7 @@ function makeBaseImports(target: TargetFramework) {
462474
function makeQueryOptions(target: string, returnType: string, infinite: boolean) {
463475
switch (target) {
464476
case 'react':
477+
case 'vue':
465478
return `Use${infinite ? 'Infinite' : ''}QueryOptions<${returnType}>`;
466479
case 'svelte':
467480
return `${infinite ? 'CreateInfinite' : ''}QueryOptions<${returnType}>`;
@@ -474,6 +487,8 @@ function makeMutationOptions(target: string, returnType: string, argsType: strin
474487
switch (target) {
475488
case 'react':
476489
return `UseMutationOptions<${returnType}, unknown, ${argsType}>`;
490+
case 'vue':
491+
return `UseMutationOptions<${returnType}, unknown, ${argsType}, unknown>`;
477492
case 'svelte':
478493
return `MutationOptions<${returnType}, unknown, ${argsType}>`;
479494
default:

packages/plugins/tanstack-query/src/runtime/common.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { serialize, deserialize } from '@zenstackhq/runtime/browser';
2+
import { deserialize, serialize } from '@zenstackhq/runtime/browser';
3+
import * as crossFetch from 'cross-fetch';
34

45
/**
56
* The default query endpoint.
@@ -37,7 +38,7 @@ export async function fetcher<R, C extends boolean>(
3738
fetch?: FetchFn,
3839
checkReadBack?: C
3940
): Promise<C extends true ? R | undefined : R> {
40-
const _fetch = fetch ?? window.fetch;
41+
const _fetch = fetch ?? crossFetch.fetch;
4142
const res = await _fetch(url, options);
4243
if (!res.ok) {
4344
const errData = unmarshal(await res.text());
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/* eslint-disable @typescript-eslint/ban-types */
2+
/* eslint-disable @typescript-eslint/no-explicit-any */
3+
import {
4+
useInfiniteQuery,
5+
useMutation,
6+
useQuery,
7+
useQueryClient,
8+
type MutateFunction,
9+
type QueryClient,
10+
type UseInfiniteQueryOptions,
11+
type UseMutationOptions,
12+
type UseQueryOptions,
13+
} from '@tanstack/vue-query';
14+
import { inject } from 'vue';
15+
import { DEFAULT_QUERY_ENDPOINT, FetchFn, QUERY_KEY_PREFIX, fetcher, makeUrl, marshal } from './common';
16+
import { RequestHandlerContext } from './svelte';
17+
18+
export { APIContext as RequestHandlerContext } from './common';
19+
20+
export const VueQueryContextKey = 'zenstack-vue-query-context';
21+
22+
export function getContext() {
23+
return inject<RequestHandlerContext>(VueQueryContextKey, {
24+
endpoint: DEFAULT_QUERY_ENDPOINT,
25+
fetch: undefined,
26+
});
27+
}
28+
29+
/**
30+
* Creates a vue-query query.
31+
*
32+
* @param model The name of the model under query.
33+
* @param url The request URL.
34+
* @param args The request args object, URL-encoded and appended as "?q=" parameter
35+
* @param options The vue-query options object
36+
* @returns useQuery hook
37+
*/
38+
export function query<R>(model: string, url: string, args?: unknown, options?: UseQueryOptions<R>, fetch?: FetchFn) {
39+
const reqUrl = makeUrl(url, args);
40+
return useQuery<R>({
41+
queryKey: [QUERY_KEY_PREFIX + model, url, args],
42+
queryFn: () => fetcher<R, false>(reqUrl, undefined, fetch, false),
43+
...options,
44+
});
45+
}
46+
47+
/**
48+
* Creates a vue-query infinite query.
49+
*
50+
* @param model The name of the model under query.
51+
* @param url The request URL.
52+
* @param args The initial request args object, URL-encoded and appended as "?q=" parameter
53+
* @param options The vue-query infinite query options object
54+
* @returns useInfiniteQuery hook
55+
*/
56+
export function infiniteQuery<R>(
57+
model: string,
58+
url: string,
59+
args?: unknown,
60+
options?: UseInfiniteQueryOptions<R>,
61+
fetch?: FetchFn
62+
) {
63+
return useInfiniteQuery<R>({
64+
queryKey: [QUERY_KEY_PREFIX + model, url, args],
65+
queryFn: ({ pageParam }) => {
66+
return fetcher<R, false>(makeUrl(url, pageParam ?? args), undefined, fetch, false);
67+
},
68+
...options,
69+
});
70+
}
71+
72+
/**
73+
* Creates a POST mutation with vue-query.
74+
*
75+
* @param model The name of the model under mutation.
76+
* @param url The request URL.
77+
* @param options The vue-query options.
78+
* @param invalidateQueries Whether to invalidate queries after mutation.
79+
* @returns useMutation hooks
80+
*/
81+
export function postMutation<T, R = any, C extends boolean = boolean, Result = C extends true ? R | undefined : R>(
82+
model: string,
83+
url: string,
84+
options?: Omit<UseMutationOptions<Result, unknown, T, unknown>, 'mutationFn'>,
85+
fetch?: FetchFn,
86+
invalidateQueries = true,
87+
checkReadBack?: C
88+
) {
89+
const queryClient = useQueryClient();
90+
const mutationFn = (data: any) =>
91+
fetcher<R, C>(
92+
url,
93+
{
94+
method: 'POST',
95+
headers: {
96+
'content-type': 'application/json',
97+
},
98+
body: marshal(data),
99+
},
100+
fetch,
101+
checkReadBack
102+
) as Promise<Result>;
103+
104+
// TODO: figure out the typing problem
105+
const finalOptions: any = mergeOptions<T, Result>(model, options, invalidateQueries, mutationFn, queryClient);
106+
const mutation = useMutation<Result, unknown, T>(finalOptions);
107+
return mutation;
108+
}
109+
110+
/**
111+
* Creates a PUT mutation with vue-query.
112+
*
113+
* @param model The name of the model under mutation.
114+
* @param url The request URL.
115+
* @param options The vue-query options.
116+
* @param invalidateQueries Whether to invalidate queries after mutation.
117+
* @returns useMutation hooks
118+
*/
119+
export function putMutation<T, R = any, C extends boolean = boolean, Result = C extends true ? R | undefined : R>(
120+
model: string,
121+
url: string,
122+
options?: Omit<UseMutationOptions<Result, unknown, T, unknown>, 'mutationFn'>,
123+
fetch?: FetchFn,
124+
invalidateQueries = true,
125+
checkReadBack?: C
126+
) {
127+
const queryClient = useQueryClient();
128+
const mutationFn = (data: any) =>
129+
fetcher<R, C>(
130+
url,
131+
{
132+
method: 'PUT',
133+
headers: {
134+
'content-type': 'application/json',
135+
},
136+
body: marshal(data),
137+
},
138+
fetch,
139+
checkReadBack
140+
) as Promise<Result>;
141+
142+
// TODO: figure out the typing problem
143+
const finalOptions: any = mergeOptions<T, Result>(model, options, invalidateQueries, mutationFn, queryClient);
144+
const mutation = useMutation<Result, unknown, T>(finalOptions);
145+
return mutation;
146+
}
147+
148+
/**
149+
* Creates a DELETE mutation with vue-query.
150+
*
151+
* @param model The name of the model under mutation.
152+
* @param url The request URL.
153+
* @param options The vue-query options.
154+
* @param invalidateQueries Whether to invalidate queries after mutation.
155+
* @returns useMutation hooks
156+
*/
157+
export function deleteMutation<T, R = any, C extends boolean = boolean, Result = C extends true ? R | undefined : R>(
158+
model: string,
159+
url: string,
160+
options?: Omit<UseMutationOptions<Result, unknown, T, unknown>, 'mutationFn'>,
161+
fetch?: FetchFn,
162+
invalidateQueries = true,
163+
checkReadBack?: C
164+
) {
165+
const queryClient = useQueryClient();
166+
const mutationFn = (data: any) =>
167+
fetcher<R, C>(
168+
makeUrl(url, data),
169+
{
170+
method: 'DELETE',
171+
},
172+
fetch,
173+
checkReadBack
174+
) as Promise<Result>;
175+
176+
// TODO: figure out the typing problem
177+
const finalOptions: any = mergeOptions<T, Result>(model, options, invalidateQueries, mutationFn, queryClient);
178+
const mutation = useMutation<Result, unknown, T>(finalOptions);
179+
return mutation;
180+
}
181+
182+
function mergeOptions<T, R = any>(
183+
model: string,
184+
options: Omit<UseMutationOptions<R, unknown, T, unknown>, 'mutationFn'> | undefined,
185+
invalidateQueries: boolean,
186+
mutationFn: MutateFunction<R, unknown, T>,
187+
queryClient: QueryClient
188+
): UseMutationOptions<R, unknown, T, unknown> {
189+
const result = { ...options, mutationFn };
190+
if (options?.onSuccess || invalidateQueries) {
191+
result.onSuccess = (...args) => {
192+
if (invalidateQueries) {
193+
queryClient.invalidateQueries([QUERY_KEY_PREFIX + model]);
194+
}
195+
return options?.onSuccess?.(...args);
196+
};
197+
}
198+
return result;
199+
}

packages/plugins/tanstack-query/tests/plugin.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,26 @@ ${sharedModel}
7070
);
7171
});
7272

73+
it('vue-query run plugin', async () => {
74+
await loadSchema(
75+
`
76+
plugin tanstack {
77+
provider = '${process.cwd()}/dist'
78+
output = '$projectRoot/hooks'
79+
target = 'vue'
80+
}
81+
82+
${sharedModel}
83+
`,
84+
{
85+
provider: 'postgresql',
86+
pushDb: false,
87+
extraDependencies: [`${origDir}/dist`, 'vue@^3.3.4', '@tanstack/vue-query@4.37.0'],
88+
compile: true,
89+
}
90+
);
91+
});
92+
7393
it('svelte-query run plugin', async () => {
7494
await loadSchema(
7595
`

packages/plugins/tanstack-query/tsup.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { defineConfig } from 'tsup';
22

33
export default defineConfig({
4-
entry: ['src/runtime/index.ts', 'src/runtime/react.ts', 'src/runtime/svelte.ts'],
4+
entry: ['src/runtime/index.ts', 'src/runtime/react.ts', 'src/runtime/vue.ts', 'src/runtime/svelte.ts'],
55
outDir: 'dist/runtime',
66
splitting: false,
77
sourcemap: true,

0 commit comments

Comments
 (0)