Skip to content
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

feat: tanstack-query v5 support #788

Merged
merged 1 commit into from
Oct 29, 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
31 changes: 29 additions & 2 deletions packages/plugins/tanstack-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
24 changes: 24 additions & 0 deletions packages/plugins/tanstack-query/scripts/postbuild.js
Original file line number Diff line number Diff line change
@@ -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',
});
106 changes: 78 additions & 28 deletions packages/plugins/tanstack-query/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(options, 'output');
Expand All @@ -38,15 +39,20 @@ 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);
if (!mapping) {
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);
Expand All @@ -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,
Expand All @@ -71,7 +78,7 @@ function generateQueryHook(
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType =
overrideReturnType ?? (returnArray ? `Array<Prisma.${model}GetPayload<T>>` : `Prisma.${model}GetPayload<T>`);
const optionsType = makeQueryOptions(target, returnType, infinite);
const optionsType = makeQueryOptions(target, returnType, infinite, version);

const func = sf.addFunction({
name: `use${infinite ? 'Infinite' : ''}${capOperation}${model}`,
Expand All @@ -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);`,
]);
Expand Down Expand Up @@ -217,6 +229,7 @@ function generateMutationHook(

function generateModelHooks(
target: TargetFramework,
version: TanStackVersion,
project: Project,
outDir: string,
model: DataModel,
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -301,6 +326,7 @@ function generateModelHooks(
if (mapping.aggregate) {
generateQueryHook(
target,
version,
sf,
modelNameCap,
'aggregate',
Expand Down Expand Up @@ -385,6 +411,7 @@ function generateModelHooks(

generateQueryHook(
target,
version,
sf,
model.name,
'groupBy',
Expand All @@ -400,6 +427,7 @@ function generateModelHooks(
{
generateQueryHook(
target,
version,
sf,
model.name,
'count',
Expand All @@ -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;
}
}
Expand All @@ -443,45 +474,60 @@ 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':
return [
`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:
throw new PluginError(name, `Unsupported target: ${target}`);
}
}

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<UseInfiniteQueryOptions<${returnType}>, 'queryKey'>`
: `Omit<UseInfiniteQueryOptions<${returnType}, unknown, InfiniteData<${returnType}>>, 'queryKey'>`
: `Omit<UseQueryOptions<${returnType}>, 'queryKey'>`;
case 'vue':
return `Use${infinite ? 'Infinite' : ''}QueryOptions<${returnType}>`;
return `Omit<Use${infinite ? 'Infinite' : ''}QueryOptions<${returnType}>, 'queryKey'>`;
case 'svelte':
return `${infinite ? 'CreateInfinite' : ''}QueryOptions<${returnType}>`;
return infinite
? version === 'v4'
? `Omit<CreateInfiniteQueryOptions<${returnType}>, 'queryKey'>`
: `StoreOrVal<Omit<CreateInfiniteQueryOptions<${returnType}, unknown, InfiniteData<${returnType}>>, 'queryKey'>>`
: version === 'v4'
? `Omit<QueryOptions<${returnType}>, 'queryKey'>`
: `StoreOrVal<Omit<QueryOptions<${returnType}>, 'queryKey'>>`;
default:
throw new PluginError(name, `Unsupported target: ${target}`);
}
Expand All @@ -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' : ''}`;
}
2 changes: 2 additions & 0 deletions packages/plugins/tanstack-query/src/runtime-v5/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from '../runtime/prisma-types';
export type { FetchFn } from '../runtime/common';
Loading
Loading