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(trpc): trpc v11 support #1656

Merged
merged 3 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
16 changes: 16 additions & 0 deletions packages/plugins/trpc/res/client/v11/next.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable */

import type { AnyTRPCRouter as AnyRouter } from '@trpc/server';
import type { NextPageContext } from 'next';
import { type CreateTRPCNext, createTRPCNext as _createTRPCNext } from '@trpc/next';
import type { DeepOverrideAtPath } from './utils';
import type { ClientType } from '../routers';

export function createTRPCNext<
TRouter extends AnyRouter,
TPath extends string | undefined = undefined,
TSSRContext extends NextPageContext = NextPageContext
>(opts: Parameters<typeof _createTRPCNext>[0]) {
const r: CreateTRPCNext<TRouter, TSSRContext> = _createTRPCNext<TRouter, TSSRContext>(opts);
return r as DeepOverrideAtPath<CreateTRPCNext<TRouter, TSSRContext>, ClientType<TRouter>, TPath>;
}
16 changes: 16 additions & 0 deletions packages/plugins/trpc/res/client/v11/react.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable */

import type { AnyTRPCRouter as AnyRouter } from '@trpc/server';
import type { CreateTRPCReactOptions } from '@trpc/react-query/shared';
import { type CreateTRPCReact, createTRPCReact as _createTRPCReact } from '@trpc/react-query';
import type { DeepOverrideAtPath } from './utils';
import type { ClientType } from '../routers';

export function createTRPCReact<
TRouter extends AnyRouter,
TPath extends string | undefined = undefined,
TSSRContext = unknown
>(opts?: CreateTRPCReactOptions<TRouter>) {
const r: CreateTRPCReact<TRouter, TSSRContext> = _createTRPCReact<TRouter, TSSRContext>(opts);
return r as DeepOverrideAtPath<CreateTRPCReact<TRouter, TSSRContext>, ClientType<TRouter>, TPath>;
}
32 changes: 32 additions & 0 deletions packages/plugins/trpc/res/client/v11/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* eslint-disable */

// inspired by: https://stackoverflow.com/questions/70632026/generic-to-recursively-modify-a-given-type-interface-in-typescript

type Primitive = string | Function | number | boolean | Symbol | undefined | null;
ymc9 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Recursively merges `T` and `R`. If there's a shared key, use `R`'s field type to overwrite `T`.
*/
export type DeepOverride<T, R> = T extends Primitive
? R
: R extends Primitive
? R
: {
[K in keyof T]: K extends keyof R ? DeepOverride<T[K], R[K]> : T[K];
} & {
[K in Exclude<keyof R, keyof T>]: R[K];
};

/**
* Traverse to `Path` (denoted by dot separated string literal type) in `T`, and starting from there,
* recursively merge with `R`.
*/
export type DeepOverrideAtPath<T, R, Path extends string | undefined = undefined> = Path extends undefined
? DeepOverride<T, R>
: Path extends `${infer P1}.${infer P2}`
? P1 extends keyof T
? Omit<T, P1> & Record<P1, DeepOverride<T[P1], DeepOverrideAtPath<T[P1], R, P2>>>
: never
: Path extends keyof T
? Omit<T, Path> & Record<Path, DeepOverride<T[Path], R>>
: never;
192 changes: 139 additions & 53 deletions packages/plugins/trpc/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,24 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
outDir = resolvePath(outDir, options);
ensureEmptyDir(outDir);

const version = typeof options.version === 'string' ? options.version : 'v10';
if (!['v10', 'v11'].includes(version)) {
throw new PluginError(name, `Unsupported tRPC version "${version}". Use "v10" (default) or "v11".`);
}

if (version === 'v11') {
// v11 require options for importing `createTRPCRouter` and `procedure`
const importCreateRouter = options.importCreateRouter as string;
if (!importCreateRouter) {
throw new PluginError(name, `Option "importCreateRouter" is required for tRPC v11`);
}

const importProcedure = options.importProcedure as string;
if (!importProcedure) {
throw new PluginError(name, `Option "importProcedure" is required for tRPC v11`);
}
}

const prismaClientDmmf = dmmf;

let modelOperations = prismaClientDmmf.mappings.modelOperations;
Expand All @@ -71,8 +89,10 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
generateClientHelpers,
model,
zodSchemasImport,
options
options,
version
);

createHelper(outDir);

await saveProject(project);
Expand All @@ -86,7 +106,8 @@ function createAppRouter(
generateClientHelpers: string[] | undefined,
zmodel: Model,
zodSchemasImport: string,
options: PluginOptions
options: PluginOptions,
version: string
) {
const indexFile = path.resolve(outDir, 'routers', `index.ts`);
const appRouter = project.createSourceFile(indexFile, undefined, {
Expand All @@ -96,31 +117,45 @@ function createAppRouter(
appRouter.addStatements('/* eslint-disable */');

const prismaImport = getPrismaClientImportSpec(path.dirname(indexFile), options);

if (version === 'v10') {
appRouter.addImportDeclarations([
{
namedImports: [
'unsetMarker',
'AnyRouter',
'AnyRootConfig',
'CreateRouterInner',
'Procedure',
'ProcedureBuilder',
'ProcedureParams',
'ProcedureRouterRecord',
'ProcedureType',
],
isTypeOnly: true,
moduleSpecifier: '@trpc/server',
},
]);
} else {
appRouter.addImportDeclarations([
{
namedImports: ['AnyTRPCRouter as AnyRouter'],
isTypeOnly: true,
moduleSpecifier: '@trpc/server',
},
]);
}

appRouter.addImportDeclarations([
{
namedImports: [
'unsetMarker',
'AnyRouter',
'AnyRootConfig',
'CreateRouterInner',
'Procedure',
'ProcedureBuilder',
'ProcedureParams',
'ProcedureRouterRecord',
'ProcedureType',
],
isTypeOnly: true,
moduleSpecifier: '@trpc/server',
},
{
namedImports: ['PrismaClient'],
isTypeOnly: true,
moduleSpecifier: prismaImport,
},
]);

appRouter.addStatements(`

if (version === 'v10') {
appRouter.addStatements(`
export type BaseConfig = AnyRootConfig;

export type RouterFactory<Config extends BaseConfig> = <
Expand All @@ -133,30 +168,40 @@ function createAppRouter(

export type ProcBuilder<Config extends BaseConfig> = ProcedureBuilder<
ProcedureParams<Config, any, any, any, UnsetMarker, UnsetMarker, any>
>;
>;
`);
} else {
appRouter.addImportDeclaration({
namedImports: ['createTRPCRouter'],
moduleSpecifier: options.importCreateRouter as string,
});
}

appRouter.addStatements(`
export function db(ctx: any) {
if (!ctx.prisma) {
throw new Error('Missing "prisma" field in trpc context');
}
return ctx.prisma as PrismaClient;
}

`);

const filteredModelOperations = modelOperations.filter((mo) => !hiddenModels.includes(mo.model));

appRouter
.addFunction({
name: 'createRouter<Config extends BaseConfig>',
parameters: [
{ name: 'router', type: 'RouterFactory<Config>' },
{ name: 'procedure', type: 'ProcBuilder<Config>' },
],
name: version === 'v10' ? 'createRouter<Config extends BaseConfig>' : 'createRouter',
parameters:
version === 'v10'
? [
{ name: 'router', type: 'RouterFactory<Config>' },
{ name: 'procedure', type: 'ProcBuilder<Config>' },
]
: [],
isExported: true,
})
.setBodyText((writer) => {
writer.write('return router(');
writer.write(`return ${version === 'v10' ? 'router' : 'createTRPCRouter'}(`);
writer.block(() => {
for (const modelOperation of filteredModelOperations) {
const { model, ...operations } = modelOperation;
Expand All @@ -173,15 +218,20 @@ function createAppRouter(
generateClientHelpers,
zodSchemasImport,
options,
zmodel
zmodel,
version
);

appRouter.addImportDeclaration({
defaultImport: `create${model}Router`,
moduleSpecifier: `./${model}.router`,
});

writer.writeLine(`${lowerCaseFirst(model)}: create${model}Router(router, procedure),`);
if (version === 'v10') {
writer.writeLine(`${lowerCaseFirst(model)}: create${model}Router(router, procedure),`);
} else {
writer.writeLine(`${lowerCaseFirst(model)}: create${model}Router(),`);
}
}
});
writer.write(');');
Expand All @@ -204,30 +254,30 @@ function createAppRouter(
}),
});

createClientHelpers(outDir, generateClientHelpers);
createClientHelpers(outDir, generateClientHelpers, version);
}

appRouter.formatText();
}

function createClientHelpers(outputDir: string, generateClientHelpers: string[]) {
function createClientHelpers(outputDir: string, generateClientHelpers: string[], version: string) {
const utils = project.createSourceFile(path.resolve(outputDir, 'client', `utils.ts`), undefined, {
overwrite: true,
});
utils.replaceWithText(fs.readFileSync(path.join(__dirname, './res/client/utils.ts'), 'utf-8'));
utils.replaceWithText(fs.readFileSync(path.join(__dirname, `./res/client/${version}/utils.ts`), 'utf-8'));

for (const client of generateClientHelpers) {
switch (client) {
case 'react': {
const content = fs.readFileSync(path.join(__dirname, './res/client/react.ts'), 'utf-8');
const content = fs.readFileSync(path.join(__dirname, `./res/client/${version}/react.ts`), 'utf-8');
project.createSourceFile(path.resolve(outputDir, 'client', 'react.ts'), content, {
overwrite: true,
});
break;
}

case 'next': {
const content = fs.readFileSync(path.join(__dirname, './res/client/next.ts'), 'utf-8');
const content = fs.readFileSync(path.join(__dirname, `./res/client/${version}/next.ts`), 'utf-8');
project.createSourceFile(path.resolve(outputDir, 'client', 'next.ts'), content, { overwrite: true });
break;
}
Expand All @@ -244,36 +294,72 @@ function generateModelCreateRouter(
generateClientHelpers: string[] | undefined,
zodSchemasImport: string,
options: PluginOptions,
zmodel: Model
zmodel: Model,
version: string
) {
const modelRouter = project.createSourceFile(path.resolve(outputDir, 'routers', `${model}.router.ts`), undefined, {
overwrite: true,
});

modelRouter.addStatements('/* eslint-disable */');

modelRouter.addImportDeclarations([
{
namedImports: ['type RouterFactory', 'type ProcBuilder', 'type BaseConfig', 'db'],
moduleSpecifier: '.',
},
]);
if (version === 'v10') {
modelRouter.addImportDeclarations([
{
namedImports: ['type RouterFactory', 'type ProcBuilder', 'type BaseConfig', 'db'],
moduleSpecifier: '.',
},
]);
} else {
modelRouter.addImportDeclarations([
{
namedImports: ['db'],
moduleSpecifier: '.',
},
]);

modelRouter.addImportDeclarations([
{
namedImports: ['createTRPCRouter'],
moduleSpecifier: options.importCreateRouter as string,
},
]);

modelRouter.addImportDeclarations([
{
namedImports: ['procedure'],
moduleSpecifier: options.importProcedure as string,
},
]);
}

// zod schema import
generateRouterSchemaImport(modelRouter, zodSchemasImport);

// runtime helpers
generateHelperImport(modelRouter);

// client helper imports
if (generateClientHelpers) {
generateRouterTypingImports(modelRouter, options);
generateRouterTypingImports(modelRouter, options, version);
}

const createRouterFunc = modelRouter.addFunction({
name: 'createRouter<Config extends BaseConfig>',
parameters: [
{ name: 'router', type: 'RouterFactory<Config>' },
{ name: 'procedure', type: 'ProcBuilder<Config>' },
],
isExported: true,
isDefaultExport: true,
});
const createRouterFunc =
version === 'v10'
? modelRouter.addFunction({
name: 'createRouter<Config extends BaseConfig>',
parameters: [
{ name: 'router', type: 'RouterFactory<Config>' },
{ name: 'procedure', type: 'ProcBuilder<Config>' },
],
isExported: true,
isDefaultExport: true,
})
: modelRouter.addFunction({
name: 'createRouter',
isExported: true,
isDefaultExport: true,
});

let routerTypingStructure: InterfaceDeclarationStructure | undefined = undefined;
if (generateClientHelpers) {
Expand All @@ -294,7 +380,7 @@ function generateModelCreateRouter(
}

createRouterFunc.setBodyText((funcWriter) => {
funcWriter.write('return router(');
funcWriter.write(`return ${version === 'v10' ? 'router' : 'createTRPCRouter'}(`);
funcWriter.block(() => {
for (const [opType, opNameWithModel] of Object.entries(operations)) {
if (isDelegateModel(dataModel) && (opType.startsWith('create') || opType.startsWith('upsert'))) {
Expand Down Expand Up @@ -322,7 +408,7 @@ function generateModelCreateRouter(
kind: StructureKind.PropertySignature,
name: generateOpName,
type: (writer) => {
generateRouterTyping(writer, generateOpName, model, baseOpType);
generateRouterTyping(writer, generateOpName, model, baseOpType, version);
},
});
}
Expand Down
Loading
Loading