Skip to content

Commit

Permalink
feat: Nuxt server adapter and tanstack-query for "vue" hooks generati…
Browse files Browse the repository at this point in the history
…on (#757)
  • Loading branch information
ymc9 authored Oct 13, 2023
1 parent 22b1bf9 commit 033d95d
Show file tree
Hide file tree
Showing 12 changed files with 3,731 additions and 219 deletions.
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/[email protected]'],
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

0 comments on commit 033d95d

Please sign in to comment.