Skip to content

feat: Nuxt server adapter and tanstack-query for "vue" hooks generation #757

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@ The following diagram gives a high-level architecture overview of ZenStack.
### Framework adapters

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

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

## Community
Expand Down
15 changes: 12 additions & 3 deletions packages/plugins/tanstack-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
"default": "./runtime/react.js",
"types": "./runtime/react.d.ts"
},
"./runtime/vue": {
"import": "./runtime/vue.mjs",
"require": "./runtime/vue.js",
"default": "./runtime/vue.js",
"types": "./runtime/vue.d.ts"
},
"./runtime/svelte": {
"import": "./runtime/svelte.mjs",
"require": "./runtime/svelte.js",
Expand Down Expand Up @@ -55,6 +61,7 @@
"@zenstackhq/runtime": "workspace:*",
"@zenstackhq/sdk": "workspace:*",
"change-case": "^4.1.2",
"cross-fetch": "^4.0.0",
"decimal.js": "^10.4.2",
"lower-case-first": "^2.0.2",
"semver": "^7.3.8",
Expand All @@ -63,8 +70,9 @@
"upper-case-first": "^2.0.2"
},
"devDependencies": {
"@tanstack/react-query": "4.29.7",
"@tanstack/svelte-query": "4.29.7",
"@tanstack/react-query": "^4.29.7",
"@tanstack/svelte-query": "^4.29.7",
"@tanstack/vue-query": "^4.37.0",
"@types/jest": "^29.5.0",
"@types/node": "^18.0.0",
"@types/react": "18.2.0",
Expand All @@ -77,6 +85,7 @@
"rimraf": "^3.0.2",
"swr": "^2.0.3",
"ts-jest": "^29.0.5",
"typescript": "^4.9.4"
"typescript": "^4.9.4",
"vue": "^3.3.4"
}
}
17 changes: 16 additions & 1 deletion packages/plugins/tanstack-query/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph';
import { upperCaseFirst } from 'upper-case-first';
import { name } from '.';

const supportedTargets = ['react', 'svelte'];
const supportedTargets = ['react', 'vue', 'svelte'];
type TargetFramework = (typeof supportedTargets)[number];

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

switch (target) {
case 'react':
case 'vue':
// override the mutateAsync function to return the correct type
func.addVariableStatement({
declarationKind: VariableDeclarationKind.Const,
Expand Down Expand Up @@ -416,6 +417,9 @@ function generateIndex(project: Project, outDir: string, models: DataModel[], ta
case 'react':
sf.addStatements(`export { Provider } from '@zenstackhq/tanstack-query/runtime/react';`);
break;
case 'vue':
sf.addStatements(`export { VueQueryContextKey } from '@zenstackhq/tanstack-query/runtime/vue';`);
break;
case 'svelte':
sf.addStatements(`export { SvelteQueryContextKey } from '@zenstackhq/tanstack-query/runtime/svelte';`);
break;
Expand All @@ -426,6 +430,8 @@ function makeGetContext(target: TargetFramework) {
switch (target) {
case 'react':
return 'const { endpoint, fetch } = useContext(RequestHandlerContext);';
case 'vue':
return 'const { endpoint, fetch } = getContext();';
case 'svelte':
return `const { endpoint, fetch } = getContext<RequestHandlerContext>(SvelteQueryContextKey);`;
default:
Expand All @@ -446,6 +452,12 @@ function makeBaseImports(target: TargetFramework) {
`import { RequestHandlerContext } from '@zenstackhq/tanstack-query/runtime/${target}';`,
...shared,
];
case 'vue':
return [
`import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions } from '@tanstack/vue-query';`,
`import { getContext } from '@zenstackhq/tanstack-query/runtime/${target}';`,
...shared,
];
case 'svelte':
return [
`import { getContext } from 'svelte';`,
Expand All @@ -462,6 +474,7 @@ function makeBaseImports(target: TargetFramework) {
function makeQueryOptions(target: string, returnType: string, infinite: boolean) {
switch (target) {
case 'react':
case 'vue':
return `Use${infinite ? 'Infinite' : ''}QueryOptions<${returnType}>`;
case 'svelte':
return `${infinite ? 'CreateInfinite' : ''}QueryOptions<${returnType}>`;
Expand All @@ -474,6 +487,8 @@ function makeMutationOptions(target: string, returnType: string, argsType: strin
switch (target) {
case 'react':
return `UseMutationOptions<${returnType}, unknown, ${argsType}>`;
case 'vue':
return `UseMutationOptions<${returnType}, unknown, ${argsType}, unknown>`;
case 'svelte':
return `MutationOptions<${returnType}, unknown, ${argsType}>`;
default:
Expand Down
5 changes: 3 additions & 2 deletions packages/plugins/tanstack-query/src/runtime/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { serialize, deserialize } from '@zenstackhq/runtime/browser';
import { deserialize, serialize } from '@zenstackhq/runtime/browser';
import * as crossFetch from 'cross-fetch';

/**
* The default query endpoint.
Expand Down Expand Up @@ -37,7 +38,7 @@ export async function fetcher<R, C extends boolean>(
fetch?: FetchFn,
checkReadBack?: C
): Promise<C extends true ? R | undefined : R> {
const _fetch = fetch ?? window.fetch;
const _fetch = fetch ?? crossFetch.fetch;
const res = await _fetch(url, options);
if (!res.ok) {
const errData = unmarshal(await res.text());
Expand Down
199 changes: 199 additions & 0 deletions packages/plugins/tanstack-query/src/runtime/vue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
type MutateFunction,
type QueryClient,
type UseInfiniteQueryOptions,
type UseMutationOptions,
type UseQueryOptions,
} from '@tanstack/vue-query';
import { inject } from 'vue';
import { DEFAULT_QUERY_ENDPOINT, FetchFn, QUERY_KEY_PREFIX, fetcher, makeUrl, marshal } from './common';
import { RequestHandlerContext } from './svelte';

export { APIContext as RequestHandlerContext } from './common';

export const VueQueryContextKey = 'zenstack-vue-query-context';

export function getContext() {
return inject<RequestHandlerContext>(VueQueryContextKey, {
endpoint: DEFAULT_QUERY_ENDPOINT,
fetch: undefined,
});
}

/**
* Creates a vue-query query.
*
* @param model The name of the model under query.
* @param url The request URL.
* @param args The request args object, URL-encoded and appended as "?q=" parameter
* @param options The vue-query options object
* @returns useQuery hook
*/
export function query<R>(model: string, url: string, args?: unknown, options?: UseQueryOptions<R>, fetch?: FetchFn) {
const reqUrl = makeUrl(url, args);
return useQuery<R>({
queryKey: [QUERY_KEY_PREFIX + model, url, args],
queryFn: () => fetcher<R, false>(reqUrl, undefined, fetch, false),
...options,
});
}

/**
* Creates a vue-query infinite query.
*
* @param model The name of the model under query.
* @param url The request URL.
* @param args The initial request args object, URL-encoded and appended as "?q=" parameter
* @param options The vue-query infinite query options object
* @returns useInfiniteQuery hook
*/
export function infiniteQuery<R>(
model: string,
url: string,
args?: unknown,
options?: UseInfiniteQueryOptions<R>,
fetch?: FetchFn
) {
return useInfiniteQuery<R>({
queryKey: [QUERY_KEY_PREFIX + model, url, args],
queryFn: ({ pageParam }) => {
return fetcher<R, false>(makeUrl(url, pageParam ?? args), undefined, fetch, false);
},
...options,
});
}

/**
* Creates a POST mutation with vue-query.
*
* @param model The name of the model under mutation.
* @param url The request URL.
* @param options The vue-query options.
* @param invalidateQueries Whether to invalidate queries after mutation.
* @returns useMutation hooks
*/
export function postMutation<T, R = any, C extends boolean = boolean, Result = C extends true ? R | undefined : R>(
model: string,
url: string,
options?: Omit<UseMutationOptions<Result, unknown, T, unknown>, 'mutationFn'>,
fetch?: FetchFn,
invalidateQueries = true,
checkReadBack?: C
) {
const queryClient = useQueryClient();
const mutationFn = (data: any) =>
fetcher<R, C>(
url,
{
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: marshal(data),
},
fetch,
checkReadBack
) as Promise<Result>;

// TODO: figure out the typing problem
const finalOptions: any = mergeOptions<T, Result>(model, options, invalidateQueries, mutationFn, queryClient);
const mutation = useMutation<Result, unknown, T>(finalOptions);
return mutation;
}

/**
* Creates a PUT mutation with vue-query.
*
* @param model The name of the model under mutation.
* @param url The request URL.
* @param options The vue-query options.
* @param invalidateQueries Whether to invalidate queries after mutation.
* @returns useMutation hooks
*/
export function putMutation<T, R = any, C extends boolean = boolean, Result = C extends true ? R | undefined : R>(
model: string,
url: string,
options?: Omit<UseMutationOptions<Result, unknown, T, unknown>, 'mutationFn'>,
fetch?: FetchFn,
invalidateQueries = true,
checkReadBack?: C
) {
const queryClient = useQueryClient();
const mutationFn = (data: any) =>
fetcher<R, C>(
url,
{
method: 'PUT',
headers: {
'content-type': 'application/json',
},
body: marshal(data),
},
fetch,
checkReadBack
) as Promise<Result>;

// TODO: figure out the typing problem
const finalOptions: any = mergeOptions<T, Result>(model, options, invalidateQueries, mutationFn, queryClient);
const mutation = useMutation<Result, unknown, T>(finalOptions);
return mutation;
}

/**
* Creates a DELETE mutation with vue-query.
*
* @param model The name of the model under mutation.
* @param url The request URL.
* @param options The vue-query options.
* @param invalidateQueries Whether to invalidate queries after mutation.
* @returns useMutation hooks
*/
export function deleteMutation<T, R = any, C extends boolean = boolean, Result = C extends true ? R | undefined : R>(
model: string,
url: string,
options?: Omit<UseMutationOptions<Result, unknown, T, unknown>, 'mutationFn'>,
fetch?: FetchFn,
invalidateQueries = true,
checkReadBack?: C
) {
const queryClient = useQueryClient();
const mutationFn = (data: any) =>
fetcher<R, C>(
makeUrl(url, data),
{
method: 'DELETE',
},
fetch,
checkReadBack
) as Promise<Result>;

// TODO: figure out the typing problem
const finalOptions: any = mergeOptions<T, Result>(model, options, invalidateQueries, mutationFn, queryClient);
const mutation = useMutation<Result, unknown, T>(finalOptions);
return mutation;
}

function mergeOptions<T, R = any>(
model: string,
options: Omit<UseMutationOptions<R, unknown, T, unknown>, 'mutationFn'> | undefined,
invalidateQueries: boolean,
mutationFn: MutateFunction<R, unknown, T>,
queryClient: QueryClient
): UseMutationOptions<R, unknown, T, unknown> {
const result = { ...options, mutationFn };
if (options?.onSuccess || invalidateQueries) {
result.onSuccess = (...args) => {
if (invalidateQueries) {
queryClient.invalidateQueries([QUERY_KEY_PREFIX + model]);
}
return options?.onSuccess?.(...args);
};
}
return result;
}
20 changes: 20 additions & 0 deletions packages/plugins/tanstack-query/tests/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ ${sharedModel}
);
});

it('vue-query run plugin', async () => {
await loadSchema(
`
plugin tanstack {
provider = '${process.cwd()}/dist'
output = '$projectRoot/hooks'
target = 'vue'
}

${sharedModel}
`,
{
provider: 'postgresql',
pushDb: false,
extraDependencies: [`${origDir}/dist`, 'vue@^3.3.4', '@tanstack/vue-query@4.37.0'],
compile: true,
}
);
});

it('svelte-query run plugin', async () => {
await loadSchema(
`
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/tanstack-query/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from 'tsup';

export default defineConfig({
entry: ['src/runtime/index.ts', 'src/runtime/react.ts', 'src/runtime/svelte.ts'],
entry: ['src/runtime/index.ts', 'src/runtime/react.ts', 'src/runtime/vue.ts', 'src/runtime/svelte.ts'],
outDir: 'dist/runtime',
splitting: false,
sourcemap: true,
Expand Down
Loading