diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index c2c1fca10..2205e0b3b 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -34,6 +34,30 @@ "require": "./runtime/svelte.js", "default": "./runtime/svelte.js", "types": "./runtime/svelte.d.ts" + }, + "./runtime-v5": { + "import": "./runtime-v5/index.mjs", + "require": "./runtime-v5/index.js", + "default": "./runtime-v5/index.js", + "types": "./runtime-v5/index.d.ts" + }, + "./runtime-v5/react": { + "import": "./runtime-v5/react.mjs", + "require": "./runtime-v5/react.js", + "default": "./runtime-v5/react.js", + "types": "./runtime-v5/react.d.ts" + }, + "./runtime-v5/vue": { + "import": "./runtime-v5/vue.mjs", + "require": "./runtime-v5/vue.js", + "default": "./runtime-v5/vue.js", + "types": "./runtime-v5/vue.d.ts" + }, + "./runtime-v5/svelte": { + "import": "./runtime-v5/svelte.mjs", + "require": "./runtime-v5/svelte.js", + "default": "./runtime-v5/svelte.js", + "types": "./runtime-v5/svelte.d.ts" } }, "repository": { @@ -42,8 +66,8 @@ }, "scripts": { "clean": "rimraf dist", - "build": "pnpm lint && pnpm clean && tsc && tsup-node && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination '../../../../.build'", - "watch": "concurrently \"tsc --watch\" \"tsup-node --watch\"", + "build": "pnpm lint && pnpm clean && tsc && tsup-node --config ./tsup.config.ts && tsup-node --config ./tsup-v5.config.ts && node scripts/postbuild && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination '../../../../.build'", + "watch": "concurrently \"tsc --watch\" \"tsup-node --config ./tsup.config.ts --watch\" \"tsup-node --config ./tsup-v5.config.ts --watch\"", "lint": "eslint src --ext ts", "test": "ZENSTACK_TEST=1 jest", "prepublishOnly": "pnpm build", @@ -71,7 +95,9 @@ }, "devDependencies": { "@tanstack/react-query": "^4.29.7", + "@tanstack/react-query-v5": "npm:@tanstack/react-query@^5.0.0", "@tanstack/svelte-query": "^4.29.7", + "@tanstack/svelte-query-v5": "npm:@tanstack/svelte-query@^5.0.0", "@tanstack/vue-query": "^4.37.0", "@types/jest": "^29.5.0", "@types/node": "^18.0.0", @@ -82,6 +108,7 @@ "copyfiles": "^2.4.1", "jest": "^29.5.0", "react": "18.2.0", + "replace-in-file": "^7.0.1", "rimraf": "^3.0.2", "svelte": "^4.2.1", "swr": "^2.0.3", diff --git a/packages/plugins/tanstack-query/scripts/postbuild.js b/packages/plugins/tanstack-query/scripts/postbuild.js new file mode 100755 index 000000000..5285c8dc5 --- /dev/null +++ b/packages/plugins/tanstack-query/scripts/postbuild.js @@ -0,0 +1,24 @@ +// tsup doesn't replace npm dependency aliases in the dist files, so we have to do it manually + +const replace = require('replace-in-file'); + +console.log('Replacing @tanstack/react-query-v5'); +replace.sync({ + files: 'dist/runtime-v5/react*(.d.ts|.d.mts|.js|.mjs)', + from: /@tanstack\/react-query-v5/g, + to: '@tanstack/react-query', +}); + +console.log('Replacing @tanstack/svelte-query-v5'); +replace.sync({ + files: 'dist/runtime-v5/svelte*(.d.ts|.d.mts|.js|.mjs)', + from: /@tanstack\/svelte-query-v5/g, + to: '@tanstack/svelte-query', +}); + +console.log('Replacing @tanstack/vue-query-v5'); +replace.sync({ + files: 'dist/runtime-v5/vue*(.d.ts|.d.mts|.js|.mjs)', + from: /@tanstack\/vue-query-v5/g, + to: '@tanstack/vue-query', +}); diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 3b1432bb2..d5c45b87c 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -21,6 +21,7 @@ import { name } from '.'; const supportedTargets = ['react', 'vue', 'svelte']; type TargetFramework = (typeof supportedTargets)[number]; +type TanStackVersion = 'v4' | 'v5'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { let outDir = requireOption(options, 'output'); @@ -38,7 +39,12 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. ); } - generateIndex(project, outDir, models, target); + const version = typeof options.version === 'string' ? options.version : 'v4'; + if (version !== 'v4' && version !== 'v5') { + throw new PluginError(options.name, `Unsupported version "${version}": use "v4" or "v5"`); + } + + generateIndex(project, outDir, models, target, version); models.forEach((dataModel) => { const mapping = dmmf.mappings.modelOperations.find((op) => op.model === dataModel.name); @@ -46,7 +52,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. warnings.push(`Unable to find mapping for model ${dataModel.name}`); return; } - generateModelHooks(target, project, outDir, dataModel, mapping); + generateModelHooks(target, version, project, outDir, dataModel, mapping); }); await saveProject(project); @@ -55,6 +61,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. function generateQueryHook( target: TargetFramework, + version: TanStackVersion, sf: SourceFile, model: string, operation: string, @@ -71,7 +78,7 @@ function generateQueryHook( const inputType = `Prisma.SelectSubset`; const returnType = overrideReturnType ?? (returnArray ? `Array>` : `Prisma.${model}GetPayload`); - const optionsType = makeQueryOptions(target, returnType, infinite); + const optionsType = makeQueryOptions(target, returnType, infinite, version); const func = sf.addFunction({ name: `use${infinite ? 'Infinite' : ''}${capOperation}${model}`, @@ -89,9 +96,14 @@ function generateQueryHook( isExported: true, }); + if (version === 'v5' && infinite && ['react', 'svelte'].includes(target)) { + // initialPageParam and getNextPageParam options are required in v5 + func.addStatements([`options = options ?? { initialPageParam: undefined, getNextPageParam: () => null };`]); + } + func.addStatements([ makeGetContext(target), - `return ${infinite ? 'infiniteQuery' : 'query'}<${returnType}>('${model}', \`\${endpoint}/${lowerCaseFirst( + `return ${infinite ? 'infiniteQuery' : 'query'}('${model}', \`\${endpoint}/${lowerCaseFirst( model )}/${operation}\`, args, options, fetch);`, ]); @@ -217,6 +229,7 @@ function generateMutationHook( function generateModelHooks( target: TargetFramework, + version: TanStackVersion, project: Project, outDir: string, model: DataModel, @@ -235,7 +248,7 @@ function generateModelHooks( isTypeOnly: true, moduleSpecifier: prismaImport, }); - sf.addStatements(makeBaseImports(target)); + sf.addStatements(makeBaseImports(target, version)); // create is somehow named "createOne" in the DMMF // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -251,19 +264,31 @@ function generateModelHooks( // findMany if (mapping.findMany) { // regular findMany - generateQueryHook(target, sf, model.name, 'findMany', true, true); + generateQueryHook(target, version, sf, model.name, 'findMany', true, true); // infinite findMany - generateQueryHook(target, sf, model.name, 'findMany', true, true, undefined, undefined, undefined, true); + generateQueryHook( + target, + version, + sf, + model.name, + 'findMany', + true, + true, + undefined, + undefined, + undefined, + true + ); } // findUnique if (mapping.findUnique) { - generateQueryHook(target, sf, model.name, 'findUnique', false, false); + generateQueryHook(target, version, sf, model.name, 'findUnique', false, false); } // findFirst if (mapping.findFirst) { - generateQueryHook(target, sf, model.name, 'findFirst', false, true); + generateQueryHook(target, version, sf, model.name, 'findFirst', false, true); } // update @@ -301,6 +326,7 @@ function generateModelHooks( if (mapping.aggregate) { generateQueryHook( target, + version, sf, modelNameCap, 'aggregate', @@ -385,6 +411,7 @@ function generateModelHooks( generateQueryHook( target, + version, sf, model.name, 'groupBy', @@ -400,6 +427,7 @@ function generateModelHooks( { generateQueryHook( target, + version, sf, model.name, 'count', @@ -410,22 +438,25 @@ function generateModelHooks( } } -function generateIndex(project: Project, outDir: string, models: DataModel[], target: string) { +function generateIndex( + project: Project, + outDir: string, + models: DataModel[], + target: string, + version: TanStackVersion +) { const sf = project.createSourceFile(path.join(outDir, 'index.ts'), undefined, { overwrite: true }); sf.addStatements(models.map((d) => `export * from './${paramCase(d.name)}';`)); + const runtimeImportBase = makeRuntimeImportBase(version); switch (target) { case 'react': - sf.addStatements(`export { Provider } from '@zenstackhq/tanstack-query/runtime/react';`); + sf.addStatements(`export { Provider } from '${runtimeImportBase}/react';`); break; case 'vue': - sf.addStatements( - `export { VueQueryContextKey, provideHooksContext } from '@zenstackhq/tanstack-query/runtime/vue';` - ); + sf.addStatements(`export { VueQueryContextKey, provideHooksContext } from '${runtimeImportBase}/vue';`); break; case 'svelte': - sf.addStatements( - `export { SvelteQueryContextKey, setHooksContext } from '@zenstackhq/tanstack-query/runtime/svelte';` - ); + sf.addStatements(`export { SvelteQueryContextKey, setHooksContext } from '${runtimeImportBase}/svelte';`); break; } } @@ -443,23 +474,24 @@ function makeGetContext(target: TargetFramework) { } } -function makeBaseImports(target: TargetFramework) { +function makeBaseImports(target: TargetFramework, version: TanStackVersion) { + const runtimeImportBase = makeRuntimeImportBase(version); const shared = [ - `import { query, infiniteQuery, postMutation, putMutation, deleteMutation } from '@zenstackhq/tanstack-query/runtime/${target}';`, - `import type { PickEnumerable, CheckSelect } from '@zenstackhq/tanstack-query/runtime';`, + `import { query, infiniteQuery, postMutation, putMutation, deleteMutation } from '${runtimeImportBase}/${target}';`, + `import type { PickEnumerable, CheckSelect } from '${runtimeImportBase}';`, ]; switch (target) { case 'react': return [ `import { useContext } from 'react';`, - `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions } from '@tanstack/react-query';`, - `import { RequestHandlerContext } from '@zenstackhq/tanstack-query/runtime/${target}';`, + `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/react-query';`, + `import { RequestHandlerContext } from '${runtimeImportBase}/${target}';`, ...shared, ]; case 'vue': return [ - `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions } from '@tanstack/vue-query';`, - `import { getContext } from '@zenstackhq/tanstack-query/runtime/${target}';`, + `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/vue-query';`, + `import { getContext } from '${runtimeImportBase}/${target}';`, ...shared, ]; case 'svelte': @@ -467,7 +499,10 @@ function makeBaseImports(target: TargetFramework) { `import { getContext } from 'svelte';`, `import { derived } from 'svelte/store';`, `import type { MutationOptions, QueryOptions, CreateInfiniteQueryOptions } from '@tanstack/svelte-query';`, - `import { SvelteQueryContextKey, type RequestHandlerContext } from '@zenstackhq/tanstack-query/runtime/${target}';`, + ...(version === 'v5' + ? [`import type { InfiniteData, StoreOrVal } from '@tanstack/svelte-query';`] + : []), + `import { SvelteQueryContextKey, type RequestHandlerContext } from '${runtimeImportBase}/${target}';`, ...shared, ]; default: @@ -475,13 +510,24 @@ function makeBaseImports(target: TargetFramework) { } } -function makeQueryOptions(target: string, returnType: string, infinite: boolean) { +function makeQueryOptions(target: string, returnType: string, infinite: boolean, version: TanStackVersion) { switch (target) { case 'react': + return infinite + ? version === 'v4' + ? `Omit, 'queryKey'>` + : `Omit>, 'queryKey'>` + : `Omit, 'queryKey'>`; case 'vue': - return `Use${infinite ? 'Infinite' : ''}QueryOptions<${returnType}>`; + return `Omit, 'queryKey'>`; case 'svelte': - return `${infinite ? 'CreateInfinite' : ''}QueryOptions<${returnType}>`; + return infinite + ? version === 'v4' + ? `Omit, 'queryKey'>` + : `StoreOrVal>, 'queryKey'>>` + : version === 'v4' + ? `Omit, 'queryKey'>` + : `StoreOrVal, 'queryKey'>>`; default: throw new PluginError(name, `Unsupported target: ${target}`); } @@ -499,3 +545,7 @@ function makeMutationOptions(target: string, returnType: string, argsType: strin throw new PluginError(name, `Unsupported target: ${target}`); } } + +function makeRuntimeImportBase(version: TanStackVersion) { + return `@zenstackhq/tanstack-query/runtime${version === 'v5' ? '-v5' : ''}`; +} diff --git a/packages/plugins/tanstack-query/src/runtime-v5/index.ts b/packages/plugins/tanstack-query/src/runtime-v5/index.ts new file mode 100644 index 000000000..c7b30ba34 --- /dev/null +++ b/packages/plugins/tanstack-query/src/runtime-v5/index.ts @@ -0,0 +1,2 @@ +export * from '../runtime/prisma-types'; +export type { FetchFn } from '../runtime/common'; diff --git a/packages/plugins/tanstack-query/src/runtime-v5/react.ts b/packages/plugins/tanstack-query/src/runtime-v5/react.ts new file mode 100644 index 000000000..b95c2530b --- /dev/null +++ b/packages/plugins/tanstack-query/src/runtime-v5/react.ts @@ -0,0 +1,211 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, + type InfiniteData, + type MutateFunction, + type QueryClient, + type UseInfiniteQueryOptions, + type UseMutationOptions, + type UseQueryOptions, +} from '@tanstack/react-query-v5'; +import { createContext } from 'react'; +import { + DEFAULT_QUERY_ENDPOINT, + FetchFn, + QUERY_KEY_PREFIX, + fetcher, + makeUrl, + marshal, + type APIContext, +} from '../runtime/common'; + +/** + * Context for configuring react hooks. + */ +export const RequestHandlerContext = createContext({ + endpoint: DEFAULT_QUERY_ENDPOINT, + fetch: undefined, +}); + +/** + * Context provider. + */ +export const Provider = RequestHandlerContext.Provider; + +/** + * Creates a react-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 react-query options object + * @returns useQuery hook + */ +export function query( + model: string, + url: string, + args?: unknown, + options?: Omit, 'queryKey'>, + fetch?: FetchFn +) { + const reqUrl = makeUrl(url, args); + return useQuery({ + queryKey: [QUERY_KEY_PREFIX + model, url, args], + queryFn: () => fetcher(reqUrl, undefined, fetch, false), + ...options, + }); +} + +/** + * Creates a react-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 react-query infinite query options object + * @returns useInfiniteQuery hook + */ +export function infiniteQuery( + model: string, + url: string, + args: unknown, + options: Omit>, 'queryKey'>, + fetch?: FetchFn +) { + return useInfiniteQuery({ + queryKey: [QUERY_KEY_PREFIX + model, url, args], + queryFn: ({ pageParam }) => { + return fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); + }, + ...options, + }); +} + +/** + * Creates a POST mutation with react-query. + * + * @param model The name of the model under mutation. + * @param url The request URL. + * @param options The react-query options. + * @param invalidateQueries Whether to invalidate queries after mutation. + * @returns useMutation hooks + */ +export function postMutation( + model: string, + url: string, + options?: Omit, 'mutationFn'>, + fetch?: FetchFn, + invalidateQueries = true, + checkReadBack?: C +) { + const queryClient = useQueryClient(); + const mutationFn = (data: any) => + fetcher( + url, + { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), + }, + fetch, + checkReadBack + ) as Promise; + + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = useMutation(finalOptions); + return mutation; +} + +/** + * Creates a PUT mutation with react-query. + * + * @param model The name of the model under mutation. + * @param url The request URL. + * @param options The react-query options. + * @param invalidateQueries Whether to invalidate queries after mutation. + * @returns useMutation hooks + */ +export function putMutation( + model: string, + url: string, + options?: Omit, 'mutationFn'>, + fetch?: FetchFn, + invalidateQueries = true, + checkReadBack?: C +) { + const queryClient = useQueryClient(); + const mutationFn = (data: any) => + fetcher( + url, + { + method: 'PUT', + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), + }, + fetch, + checkReadBack + ) as Promise; + + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = useMutation(finalOptions); + return mutation; +} + +/** + * Creates a DELETE mutation with react-query. + * + * @param model The name of the model under mutation. + * @param url The request URL. + * @param options The react-query options. + * @param invalidateQueries Whether to invalidate queries after mutation. + * @returns useMutation hooks + */ +export function deleteMutation( + model: string, + url: string, + options?: Omit, 'mutationFn'>, + fetch?: FetchFn, + invalidateQueries = true, + checkReadBack?: C +) { + const queryClient = useQueryClient(); + const mutationFn = (data: any) => + fetcher( + makeUrl(url, data), + { + method: 'DELETE', + }, + fetch, + checkReadBack + ) as Promise; + + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = useMutation(finalOptions); + return mutation; +} + +function mergeOptions( + model: string, + options: Omit, 'mutationFn'> | undefined, + invalidateQueries: boolean, + mutationFn: MutateFunction, + queryClient: QueryClient +): UseMutationOptions { + const result = { ...options, mutationFn }; + if (options?.onSuccess || invalidateQueries) { + result.onSuccess = (...args) => { + if (invalidateQueries) { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY_PREFIX + model] }); + } + return options?.onSuccess?.(...args); + }; + } + return result; +} diff --git a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts new file mode 100644 index 000000000..8f25ff0ee --- /dev/null +++ b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts @@ -0,0 +1,243 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + createInfiniteQuery, + createMutation, + createQuery, + useQueryClient, + type CreateInfiniteQueryOptions, + type InfiniteData, + type MutateFunction, + type MutationOptions, + type QueryClient, + type StoreOrVal, +} from '@tanstack/svelte-query-v5'; +import { QueryOptions } from '@tanstack/vue-query'; +import { setContext } from 'svelte'; +import { Readable, derived } from 'svelte/store'; +import { APIContext, FetchFn, QUERY_KEY_PREFIX, fetcher, makeUrl, marshal } from '../runtime/common'; + +export { APIContext as RequestHandlerContext } from '../runtime/common'; + +/** + * Key for setting and getting the global query context. + */ +export const SvelteQueryContextKey = 'zenstack-svelte-query-context'; + +/** + * Set context for the generated TanStack Query hooks. + */ +export function setHooksContext(context: APIContext) { + setContext(SvelteQueryContextKey, context); +} + +/** + * Creates a svelte-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 svelte-query options object + * @returns useQuery hook + */ +export function query( + model: string, + url: string, + args?: unknown, + options?: StoreOrVal, 'queryKey'>>, + fetch?: FetchFn +) { + const reqUrl = makeUrl(url, args); + const queryKey = [QUERY_KEY_PREFIX + model, url, args]; + const queryFn = () => fetcher(reqUrl, undefined, fetch, false); + + let mergedOpt: any; + if (isStore(options)) { + // options is store + mergedOpt = derived([options], ([$opt]) => { + return { + queryKey, + queryFn, + ...($opt as object), + }; + }); + } else { + // options is value + mergedOpt = { + queryKey, + queryFn, + ...options, + }; + } + return createQuery(mergedOpt); +} + +/** + * Creates a svelte-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 svelte-query infinite query options object + * @returns useQuery hook + */ +export function infiniteQuery( + model: string, + url: string, + args: unknown, + options: StoreOrVal>, 'queryKey'>>, + fetch?: FetchFn +) { + const queryKey = [QUERY_KEY_PREFIX + model, url, args]; + const queryFn = ({ pageParam }: { pageParam: unknown }) => + fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); + + let mergedOpt: StoreOrVal>>; + if (isStore>>(options)) { + // options is store + mergedOpt = derived([options], ([$opt]) => { + return { + queryKey, + queryFn, + ...$opt, + }; + }); + } else { + // options is value + mergedOpt = { + queryKey, + queryFn, + ...options, + }; + } + return createInfiniteQuery>(mergedOpt); +} + +function isStore(opt: unknown): opt is Readable { + return typeof (opt as any)?.subscribe === 'function'; +} + +/** + * Creates a POST mutation with svelte-query. + * + * @param model The name of the model under mutation. + * @param url The request URL. + * @param options The svelte-query options. + * @param invalidateQueries Whether to invalidate queries after mutation. + * @returns useMutation hooks + */ +export function postMutation( + model: string, + url: string, + options?: Omit, 'mutationFn'>, + fetch?: FetchFn, + invalidateQueries = true, + checkReadBack?: C +) { + const queryClient = useQueryClient(); + const mutationFn = (data: any) => + fetcher( + url, + { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), + }, + fetch, + checkReadBack + ) as Promise; + + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = createMutation(finalOptions); + return mutation; +} + +/** + * Creates a PUT mutation with svelte-query. + * + * @param model The name of the model under mutation. + * @param url The request URL. + * @param options The svelte-query options. + * @param invalidateQueries Whether to invalidate queries after mutation. + * @returns useMutation hooks + */ +export function putMutation( + model: string, + url: string, + options?: Omit, 'mutationFn'>, + fetch?: FetchFn, + invalidateQueries = true, + checkReadBack?: C +) { + const queryClient = useQueryClient(); + const mutationFn = (data: any) => + fetcher( + url, + { + method: 'PUT', + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), + }, + fetch, + checkReadBack + ) as Promise; + + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = createMutation(finalOptions); + return mutation; +} + +/** + * Creates a DELETE mutation with svelte-query. + * + * @param model The name of the model under mutation. + * @param url The request URL. + * @param options The svelte-query options. + * @param invalidateQueries Whether to invalidate queries after mutation. + * @returns useMutation hooks + */ +export function deleteMutation( + model: string, + url: string, + options?: Omit, 'mutationFn'>, + fetch?: FetchFn, + invalidateQueries = true, + checkReadBack?: C +) { + const queryClient = useQueryClient(); + const mutationFn = (data: any) => + fetcher( + makeUrl(url, data), + { + method: 'DELETE', + }, + fetch, + checkReadBack + ) as Promise; + + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = createMutation(finalOptions); + return mutation; +} + +function mergeOptions( + model: string, + options: Omit, 'mutationFn'> | undefined, + invalidateQueries: boolean, + mutationFn: MutateFunction, + queryClient: QueryClient +): MutationOptions { + const result = { ...options, mutationFn }; + if (options?.onSuccess || invalidateQueries) { + result.onSuccess = (...args) => { + if (invalidateQueries) { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY_PREFIX + model] }); + } + return options?.onSuccess?.(...args); + }; + } + return result; +} diff --git a/packages/plugins/tanstack-query/src/runtime-v5/vue.ts b/packages/plugins/tanstack-query/src/runtime-v5/vue.ts new file mode 100644 index 000000000..5f4cbf406 --- /dev/null +++ b/packages/plugins/tanstack-query/src/runtime-v5/vue.ts @@ -0,0 +1 @@ +export * from '../runtime/vue'; diff --git a/packages/plugins/tanstack-query/src/runtime/react.ts b/packages/plugins/tanstack-query/src/runtime/react.ts index 1eca9f045..6c3a4739a 100644 --- a/packages/plugins/tanstack-query/src/runtime/react.ts +++ b/packages/plugins/tanstack-query/src/runtime/react.ts @@ -43,7 +43,13 @@ export const Provider = RequestHandlerContext.Provider; * @param options The react-query options object * @returns useQuery hook */ -export function query(model: string, url: string, args?: unknown, options?: UseQueryOptions, fetch?: FetchFn) { +export function query( + model: string, + url: string, + args?: unknown, + options?: Omit, 'queryKey'>, + fetch?: FetchFn +) { const reqUrl = makeUrl(url, args); return useQuery({ queryKey: [QUERY_KEY_PREFIX + model, url, args], @@ -65,7 +71,7 @@ export function infiniteQuery( model: string, url: string, args?: unknown, - options?: UseInfiniteQueryOptions, + options?: Omit, 'queryKey'>, fetch?: FetchFn ) { return useInfiniteQuery({ @@ -109,8 +115,8 @@ export function postMutation; - const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); - const mutation = useMutation(finalOptions); + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = useMutation(finalOptions); return mutation; } @@ -146,8 +152,8 @@ export function putMutation; - const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); - const mutation = useMutation(finalOptions); + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = useMutation(finalOptions); return mutation; } @@ -179,8 +185,8 @@ export function deleteMutation; - const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); - const mutation = useMutation(finalOptions); + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = useMutation(finalOptions); return mutation; } diff --git a/packages/plugins/tanstack-query/src/runtime/svelte.ts b/packages/plugins/tanstack-query/src/runtime/svelte.ts index 0f86292f5..24ea57448 100644 --- a/packages/plugins/tanstack-query/src/runtime/svelte.ts +++ b/packages/plugins/tanstack-query/src/runtime/svelte.ts @@ -36,7 +36,13 @@ export function setHooksContext(context: APIContext) { * @param options The svelte-query options object * @returns useQuery hook */ -export function query(model: string, url: string, args?: unknown, options?: QueryOptions, fetch?: FetchFn) { +export function query( + model: string, + url: string, + args?: unknown, + options?: Omit, 'queryKey'>, + fetch?: FetchFn +) { const reqUrl = makeUrl(url, args); return createQuery({ queryKey: [QUERY_KEY_PREFIX + model, url, args], @@ -58,7 +64,7 @@ export function infiniteQuery( model: string, url: string, args?: unknown, - options?: CreateInfiniteQueryOptions, + options?: Omit, 'queryKey'>, fetch?: FetchFn ) { return createInfiniteQuery({ @@ -100,8 +106,8 @@ export function postMutation; - const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); - const mutation = createMutation(finalOptions); + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = createMutation(finalOptions); return mutation; } @@ -137,8 +143,8 @@ export function putMutation; - const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); - const mutation = createMutation(finalOptions); + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = createMutation(finalOptions); return mutation; } @@ -170,8 +176,8 @@ export function deleteMutation; - const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); - const mutation = createMutation(finalOptions); + const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); + const mutation = createMutation(finalOptions); return mutation; } diff --git a/packages/plugins/tanstack-query/tests/plugin.test.ts b/packages/plugins/tanstack-query/tests/plugin.test.ts index d4c04f374..880058cd2 100644 --- a/packages/plugins/tanstack-query/tests/plugin.test.ts +++ b/packages/plugins/tanstack-query/tests/plugin.test.ts @@ -45,7 +45,7 @@ model Foo { } `; - it('react-query run plugin', async () => { + it('react-query run plugin v4', async () => { await loadSchema( ` plugin tanstack { @@ -59,24 +59,85 @@ ${sharedModel} { provider: 'postgresql', pushDb: false, - extraDependencies: [ - `${origDir}/dist`, - 'react@18.2.0', - '@types/react@18.2.0', - '@tanstack/react-query@4.29.7', - ], + extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@4.29.7'], + copyDependencies: [`${origDir}/dist`], compile: true, } ); }); - it('vue-query run plugin', async () => { + it('react-query run plugin v5', async () => { + await loadSchema( + ` +plugin tanstack { + provider = '${process.cwd()}/dist' + output = '$projectRoot/hooks' + target = 'react' + version = 'v5' +} + +${sharedModel} + `, + { + provider: 'postgresql', + pushDb: false, + extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@^5.0.0'], + copyDependencies: [`${origDir}/dist`], + compile: true, + } + ); + }); + + it('vue-query run plugin v4', async () => { + await loadSchema( + ` +plugin tanstack { + provider = '${process.cwd()}/dist' + output = '$projectRoot/hooks' + target = 'vue' +} + +${sharedModel} + `, + { + provider: 'postgresql', + pushDb: false, + extraDependencies: ['vue@^3.3.4', '@tanstack/vue-query@4.37.0'], + copyDependencies: [`${origDir}/dist`], + compile: true, + } + ); + }); + + it('vue-query run plugin v5', async () => { await loadSchema( ` plugin tanstack { provider = '${process.cwd()}/dist' output = '$projectRoot/hooks' target = 'vue' + version = 'v5' +} + +${sharedModel} + `, + { + provider: 'postgresql', + pushDb: false, + extraDependencies: ['vue@^3.3.4', '@tanstack/vue-query@latest'], + copyDependencies: [`${origDir}/dist`], + compile: true, + } + ); + }); + + it('svelte-query run plugin v4', async () => { + await loadSchema( + ` +plugin tanstack { + provider = '${process.cwd()}/dist' + output = '$projectRoot/hooks' + target = 'svelte' } ${sharedModel} @@ -84,19 +145,21 @@ ${sharedModel} { provider: 'postgresql', pushDb: false, - extraDependencies: [`${origDir}/dist`, 'vue@^3.3.4', '@tanstack/vue-query@4.37.0'], + extraDependencies: ['svelte@^3.0.0', '@tanstack/svelte-query@4.29.7'], + copyDependencies: [`${origDir}/dist`], compile: true, } ); }); - it('svelte-query run plugin', async () => { + it('svelte-query run plugin v5', async () => { await loadSchema( ` plugin tanstack { provider = '${process.cwd()}/dist' output = '$projectRoot/hooks' target = 'svelte' + version = 'v5' } ${sharedModel} @@ -104,7 +167,8 @@ ${sharedModel} { provider: 'postgresql', pushDb: false, - extraDependencies: [`${origDir}/dist`, 'svelte@^3.0.0', '@tanstack/svelte-query@4.29.7'], + extraDependencies: ['svelte@^3.0.0', '@tanstack/svelte-query@^5.0.0'], + copyDependencies: [`${origDir}/dist`], compile: true, } ); diff --git a/packages/plugins/tanstack-query/tsup-v5.config.ts b/packages/plugins/tanstack-query/tsup-v5.config.ts new file mode 100644 index 000000000..d619109b7 --- /dev/null +++ b/packages/plugins/tanstack-query/tsup-v5.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/runtime-v5/index.ts', 'src/runtime-v5/react.ts', 'src/runtime-v5/vue.ts', 'src/runtime-v5/svelte.ts'], + outDir: 'dist/runtime-v5', + splitting: false, + sourcemap: true, + clean: true, + dts: true, + format: ['cjs', 'esm'], +}); diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index d230d6c77..8c285bd38 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -96,6 +96,7 @@ export type SchemaLoadOptions = { pushDb?: boolean; fullZod?: boolean; extraDependencies?: string[]; + copyDependencies?: string[]; compile?: boolean; customSchemaFilePath?: string; output?: string; @@ -202,6 +203,11 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { run(`npm install ${dep}`); }); + opt.copyDependencies?.forEach((dep) => { + const pkgJson = JSON.parse(fs.readFileSync(path.join(dep, 'package.json'), { encoding: 'utf-8' })); + fs.cpSync(dep, path.join(projectRoot, 'node_modules', pkgJson.name), { recursive: true, force: true }); + }); + const PrismaClient = require(path.join(projectRoot, 'node_modules/.prisma/client')).PrismaClient; let prisma = new PrismaClient({ log: ['info', 'warn', 'error'] }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f4e96cb5..356c1db1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,9 +246,15 @@ importers: '@tanstack/react-query': specifier: ^4.29.7 version: 4.29.7(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-query-v5': + specifier: npm:@tanstack/react-query@^5.0.0 + version: /@tanstack/react-query@5.0.5(react@18.2.0) '@tanstack/svelte-query': specifier: ^4.29.7 version: 4.29.7(svelte@4.2.1) + '@tanstack/svelte-query-v5': + specifier: npm:@tanstack/svelte-query@^5.0.0 + version: /@tanstack/svelte-query@5.3.0(svelte@4.2.1) '@tanstack/vue-query': specifier: ^4.37.0 version: 4.37.0(vue@3.3.4) @@ -279,6 +285,9 @@ importers: react: specifier: 18.2.0 version: 18.2.0 + replace-in-file: + specifier: ^7.0.1 + version: 7.0.1 rimraf: specifier: ^3.0.2 version: 3.0.2 @@ -714,7 +723,7 @@ importers: devDependencies: '@sveltejs/kit': specifier: 1.21.0 - version: 1.21.0(svelte@4.0.5)(vite@4.4.11) + version: 1.21.0(svelte@4.2.1)(vite@4.4.11) '@types/body-parser': specifier: ^1.19.2 version: 1.19.2 @@ -4168,7 +4177,7 @@ packages: '@sinonjs/commons': 3.0.0 dev: true - /@sveltejs/kit@1.21.0(svelte@4.0.5)(vite@4.4.11): + /@sveltejs/kit@1.21.0(svelte@4.2.1)(vite@4.4.11): resolution: {integrity: sha512-CBsYoI34SjtOQp0eG85dmVnvTR3Pjs8VgAQhO0CgQja9BIorKl808F1X8EunPhCcyek5r5lKQE1Mmbi0RuzHqA==} engines: {node: ^16.14 || >=18} hasBin: true @@ -4177,7 +4186,7 @@ packages: svelte: ^3.54.0 || ^4.0.0-next.0 vite: ^4.0.0 dependencies: - '@sveltejs/vite-plugin-svelte': 2.4.2(svelte@4.0.5)(vite@4.4.11) + '@sveltejs/vite-plugin-svelte': 2.4.2(svelte@4.2.1)(vite@4.4.11) '@types/cookie': 0.5.1 cookie: 0.5.0 devalue: 4.3.2 @@ -4188,14 +4197,14 @@ packages: sade: 1.8.1 set-cookie-parser: 2.6.0 sirv: 2.0.3 - svelte: 4.0.5 + svelte: 4.2.1 undici: 5.22.1 vite: 4.4.11(@types/node@18.0.0) transitivePeerDependencies: - supports-color dev: true - /@sveltejs/vite-plugin-svelte-inspector@1.0.3(@sveltejs/vite-plugin-svelte@2.4.2)(svelte@4.0.5)(vite@4.4.11): + /@sveltejs/vite-plugin-svelte-inspector@1.0.3(@sveltejs/vite-plugin-svelte@2.4.2)(svelte@4.2.1)(vite@4.4.11): resolution: {integrity: sha512-Khdl5jmmPN6SUsVuqSXatKpQTMIifoQPDanaxC84m9JxIibWvSABJyHpyys0Z+1yYrxY5TTEQm+6elh0XCMaOA==} engines: {node: ^14.18.0 || >= 16} peerDependencies: @@ -4203,28 +4212,28 @@ packages: svelte: ^3.54.0 || ^4.0.0 vite: ^4.0.0 dependencies: - '@sveltejs/vite-plugin-svelte': 2.4.2(svelte@4.0.5)(vite@4.4.11) + '@sveltejs/vite-plugin-svelte': 2.4.2(svelte@4.2.1)(vite@4.4.11) debug: 4.3.4 - svelte: 4.0.5 + svelte: 4.2.1 vite: 4.4.11(@types/node@18.0.0) transitivePeerDependencies: - supports-color dev: true - /@sveltejs/vite-plugin-svelte@2.4.2(svelte@4.0.5)(vite@4.4.11): + /@sveltejs/vite-plugin-svelte@2.4.2(svelte@4.2.1)(vite@4.4.11): resolution: {integrity: sha512-ePfcC48ftMKhkT0OFGdOyycYKnnkT6i/buzey+vHRTR/JpQvuPzzhf1PtKqCDQfJRgoPSN2vscXs6gLigx/zGw==} engines: {node: ^14.18.0 || >= 16} peerDependencies: svelte: ^3.54.0 || ^4.0.0 vite: ^4.0.0 dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 1.0.3(@sveltejs/vite-plugin-svelte@2.4.2)(svelte@4.0.5)(vite@4.4.11) + '@sveltejs/vite-plugin-svelte-inspector': 1.0.3(@sveltejs/vite-plugin-svelte@2.4.2)(svelte@4.2.1)(vite@4.4.11) debug: 4.3.4 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.4 - svelte: 4.0.5 - svelte-hmr: 0.15.2(svelte@4.0.5) + svelte: 4.2.1 + svelte-hmr: 0.15.2(svelte@4.2.1) vite: 4.4.11(@types/node@18.0.0) vitefu: 0.2.4(vite@4.4.11) transitivePeerDependencies: @@ -4262,6 +4271,10 @@ packages: resolution: {integrity: sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==} dev: true + /@tanstack/query-core@5.0.5: + resolution: {integrity: sha512-MThCETMkHDHTnFZHp71L+SqTtD5d6XHftFCVR1xRJdWM3qGrlQ2VCXaj0SKVcyJej2e1Opa2c7iknu1llxCDNQ==} + dev: true + /@tanstack/react-query@4.28.0(react@18.2.0): resolution: {integrity: sha512-8cGBV5300RHlvYdS4ea+G1JcZIt5CIuprXYFnsWggkmGoC0b5JaqG0fIX3qwDL9PTNkKvG76NGThIWbpXivMrQ==} peerDependencies: @@ -4297,6 +4310,22 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: true + /@tanstack/react-query@5.0.5(react@18.2.0): + resolution: {integrity: sha512-ZG0Q4HZ0iuI8mWiZ2/MdVYPHbrmAVhMn7+gLOkxJh6zLIgCL4luSZlohzN5Xt4MjxfxxWioO1nemwpudaTsmQg==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@tanstack/query-core': 5.0.5 + react: 18.2.0 + dev: true + /@tanstack/svelte-query@4.29.7(svelte@4.2.1): resolution: {integrity: sha512-+GvOcGEiFZYj91luPcIIVakmDqXkS5tyWlBLf/zg9E9hohq7SiGug9C9DUWwvSBk80JefAOCioja5jrumrPp6Q==} peerDependencies: @@ -4306,6 +4335,15 @@ packages: svelte: 4.2.1 dev: true + /@tanstack/svelte-query@5.3.0(svelte@4.2.1): + resolution: {integrity: sha512-gGC2jh7VV2F2rZygLVbvGxYbwFIW7d3rrY8iuozDOHvBft/y8/rU9aNzm/vrOdsPt2TMF3IgS3diDlp+21z6Lw==} + peerDependencies: + svelte: '>=3 <5' + dependencies: + '@tanstack/query-core': 5.0.5 + svelte: 4.2.1 + dev: true + /@tanstack/vue-query@4.37.0(vue@3.3.4): resolution: {integrity: sha512-ZZvlEuXzCwM9QYS1GCf9FTo1XycBxPSvjJbIHXTU2iZuUla02EzTO7R9QOnML6FRQRzD3n5Eyn2v6zbhCHS/qA==} peerDependencies: @@ -13112,32 +13150,13 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - /svelte-hmr@0.15.2(svelte@4.0.5): + /svelte-hmr@0.15.2(svelte@4.2.1): resolution: {integrity: sha512-q/bAruCvFLwvNbeE1x3n37TYFb3mTBJ6TrCq6p2CoFbSTNhDE9oAtEfpy+wmc9So8AG0Tja+X0/mJzX9tSfvIg==} engines: {node: ^12.20 || ^14.13.1 || >= 16} peerDependencies: svelte: ^3.19.0 || ^4.0.0-next.0 dependencies: - svelte: 4.0.5 - dev: true - - /svelte@4.0.5: - resolution: {integrity: sha512-PHKPWP1wiWHBtsE57nCb8xiWB3Ht7/3Kvi3jac0XIxUM2rep8alO7YoAtgWeGD7++tFy46krilOrPW0mG3Dx+A==} - engines: {node: '>=16'} - dependencies: - '@ampproject/remapping': 2.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.18 - acorn: 8.10.0 - aria-query: 5.3.0 - axobject-query: 3.2.1 - code-red: 1.0.3 - css-tree: 2.3.1 - estree-walker: 3.0.3 - is-reference: 3.0.1 - locate-character: 3.0.0 - magic-string: 0.30.4 - periscopic: 3.1.0 + svelte: 4.2.1 dev: true /svelte@4.2.1: