From f84392561e88a20f3d80ccb5863e6b631288c9e0 Mon Sep 17 00:00:00 2001 From: Ting Lei Date: Fri, 3 Jan 2025 10:55:31 +0800 Subject: [PATCH 1/6] fix: generator: flatten zod schemas in chained calls --- .../procedure.generator.spec.ts.snap | 21 +++++++++ .../__tests__/procedure.generator.spec.ts | 45 +++++++++++++++++-- .../lib/generators/procedure.generator.ts | 11 ++++- 3 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 packages/nestjs-trpc/lib/generators/__tests__/__snapshots__/procedure.generator.spec.ts.snap diff --git a/packages/nestjs-trpc/lib/generators/__tests__/__snapshots__/procedure.generator.spec.ts.snap b/packages/nestjs-trpc/lib/generators/__tests__/__snapshots__/procedure.generator.spec.ts.snap new file mode 100644 index 0000000..1e60607 --- /dev/null +++ b/packages/nestjs-trpc/lib/generators/__tests__/__snapshots__/procedure.generator.spec.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProcedureGenerator flattenZodSchema should flatten all chained call expressions 1`] = ` +"z.object({ + options: z + .object({ + userId: z.string().describe('ID of the current user'), + type1: z + .enum(['Normal', 'Unknown']) + .describe('Type of the item').optional().describe('Type 1 of the item') + }) + .merge({ + z.object({ + type2: z + .enum(['Normal', 'Unknown']) + .describe('Type of the item').optional().describe('Type 2 of the item') + }) + }) + .describe('Options to find many items'), + })" +`; diff --git a/packages/nestjs-trpc/lib/generators/__tests__/procedure.generator.spec.ts b/packages/nestjs-trpc/lib/generators/__tests__/procedure.generator.spec.ts index ae949fd..35d5e35 100644 --- a/packages/nestjs-trpc/lib/generators/__tests__/procedure.generator.spec.ts +++ b/packages/nestjs-trpc/lib/generators/__tests__/procedure.generator.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { Project } from 'ts-morph'; +import { Identifier, Project, SourceFile, SyntaxKind } from 'ts-morph'; import { ProcedureGeneratorMetadata, } from '../../interfaces/generator.interface'; @@ -10,7 +10,6 @@ import { TYPESCRIPT_APP_ROUTER_SOURCE_FILE } from '../generator.constants'; describe('ProcedureGenerator', () => { let procedureGenerator: ProcedureGenerator; - let project: Project; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -18,7 +17,7 @@ describe('ProcedureGenerator', () => { ProcedureGenerator, { provide: ImportsScanner, - useValue: jest.fn(), + useValue: new ImportsScanner(), }, { provide: StaticGenerator, @@ -69,4 +68,44 @@ describe('ProcedureGenerator', () => { }); }) }); + + describe('flattenZodSchema', () => { + let project: Project; + + beforeEach(async () => { + project = new Project(); + }); + + it('should flatten all chained call expressions', () => { + const sourceFile: SourceFile = project.createSourceFile( + "test.ts", + ` + import { z } from 'zod'; + + const TypeEnum = z + .enum(['Normal', 'Unknown']) + .describe('Type of the item'); + + const FindManyInput = z.object({ + options: z + .object({ + userId: z.string().describe('ID of the current user'), + type1: TypeEnum.optional().describe('Type 1 of the item') + }) + .merge({ + z.object({ + type2: TypeEnum.optional().describe('Type 2 of the item') + }) + }) + .describe('Options to find many items'), + }); + `, + { overwrite: true } + ); + + const node = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier).find((identifier) => identifier.getText() === "FindManyInput") as Identifier; + const result = procedureGenerator.flattenZodSchema(node, sourceFile, project, node.getText()); + expect(result).toMatchSnapshot(); + }); + }); }); \ No newline at end of file diff --git a/packages/nestjs-trpc/lib/generators/procedure.generator.ts b/packages/nestjs-trpc/lib/generators/procedure.generator.ts index d99d0e1..1c32456 100644 --- a/packages/nestjs-trpc/lib/generators/procedure.generator.ts +++ b/packages/nestjs-trpc/lib/generators/procedure.generator.ts @@ -132,7 +132,6 @@ export class ProcedureGenerator { importsMap, ); } - for (const arg of node.getArguments()) { const argText = arg.getText(); schema = schema.replace( @@ -140,6 +139,16 @@ export class ProcedureGenerator { this.flattenZodSchema(arg, sourceFile, project, argText), ); } + + for (const child of expression.getChildren()) { + if (Node.isCallExpression(child)) { + const childText = child.getText(); + schema = schema.replace( + childText, + this.flattenZodSchema(child, sourceFile, project, childText), + ); + } + } } else if (Node.isPropertyAccessExpression(node)) { schema = this.flattenZodSchema( node.getExpression(), From f1e4a4d06f1d6887d6562fb781070b95a5ca4ebf Mon Sep 17 00:00:00 2001 From: Ting Lei Date: Fri, 3 Jan 2025 11:00:02 +0800 Subject: [PATCH 2/6] fix: restore blank lines --- packages/nestjs-trpc/lib/generators/procedure.generator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nestjs-trpc/lib/generators/procedure.generator.ts b/packages/nestjs-trpc/lib/generators/procedure.generator.ts index 1c32456..d888e40 100644 --- a/packages/nestjs-trpc/lib/generators/procedure.generator.ts +++ b/packages/nestjs-trpc/lib/generators/procedure.generator.ts @@ -132,6 +132,7 @@ export class ProcedureGenerator { importsMap, ); } + for (const arg of node.getArguments()) { const argText = arg.getText(); schema = schema.replace( From 47dbd3b206a17c4da1c386a15f33f2cfb49808c5 Mon Sep 17 00:00:00 2001 From: Ting Lei Date: Fri, 3 Jan 2025 14:52:55 +0800 Subject: [PATCH 3/6] feat: generator: allow tsconfig path as module option --- .../__tests__/generator.module.spec.ts | 23 +++++++++++++++++++ .../lib/generators/generator.interface.ts | 1 + .../lib/generators/generator.module.ts | 5 +++- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 packages/nestjs-trpc/lib/generators/__tests__/generator.module.spec.ts diff --git a/packages/nestjs-trpc/lib/generators/__tests__/generator.module.spec.ts b/packages/nestjs-trpc/lib/generators/__tests__/generator.module.spec.ts new file mode 100644 index 0000000..f2f5d2d --- /dev/null +++ b/packages/nestjs-trpc/lib/generators/__tests__/generator.module.spec.ts @@ -0,0 +1,23 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { GeneratorModule } from '../generator.module'; + +describe('GeneratorModule', () => { + let generatorModule: GeneratorModule; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + GeneratorModule.forRoot({ + rootModuleFilePath: '', + tsConfigFilePath: './tsconfig.json', + }), + ], + }).compile(); + + generatorModule = module.get(GeneratorModule); + }); + + it('should be defined', () => { + expect(generatorModule).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/packages/nestjs-trpc/lib/generators/generator.interface.ts b/packages/nestjs-trpc/lib/generators/generator.interface.ts index 80f89f1..d90fdc0 100644 --- a/packages/nestjs-trpc/lib/generators/generator.interface.ts +++ b/packages/nestjs-trpc/lib/generators/generator.interface.ts @@ -6,4 +6,5 @@ export interface GeneratorModuleOptions { context?: Class; outputDirPath?: string; schemaFileImports?: Array; + tsConfigFilePath?: string; } diff --git a/packages/nestjs-trpc/lib/generators/generator.module.ts b/packages/nestjs-trpc/lib/generators/generator.module.ts index db79bcd..2aca678 100644 --- a/packages/nestjs-trpc/lib/generators/generator.module.ts +++ b/packages/nestjs-trpc/lib/generators/generator.module.ts @@ -58,7 +58,10 @@ export class GeneratorModule implements OnModuleInit { checkJs: true, esModuleInterop: true, }; - const project = new Project({ compilerOptions: defaultCompilerOptions }); + const project = new Project({ + compilerOptions: defaultCompilerOptions, + tsConfigFilePath: options.tsConfigFilePath, + }); const appRouterSourceFile = project.createSourceFile( path.resolve(options.outputDirPath ?? './', 'server.ts'), From d08b8f6b2d2484886fa93ad9c33e468f213a4f60 Mon Sep 17 00:00:00 2001 From: Ting Lei Date: Fri, 3 Jan 2025 15:05:13 +0800 Subject: [PATCH 4/6] feat: generator: allow tsconfig path as module option --- .../nestjs-trpc/lib/interfaces/module-options.interface.ts | 5 +++++ packages/nestjs-trpc/lib/trpc.module.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/packages/nestjs-trpc/lib/interfaces/module-options.interface.ts b/packages/nestjs-trpc/lib/interfaces/module-options.interface.ts index 5b773d2..c1c5243 100644 --- a/packages/nestjs-trpc/lib/interfaces/module-options.interface.ts +++ b/packages/nestjs-trpc/lib/interfaces/module-options.interface.ts @@ -19,6 +19,11 @@ export interface TRPCModuleOptions { */ autoSchemaFile?: string; + /** + * Path to project tsconfig. + */ + tsConfigFilePath?: string; + /** * Specifies additional imports for the schema file. This array can include functions, objects, or Zod schemas. * While `nestjs-trpc` typically handles imports automatically, this option allows manual inclusion of imports for exceptional cases. diff --git a/packages/nestjs-trpc/lib/trpc.module.ts b/packages/nestjs-trpc/lib/trpc.module.ts index 2c393be..b65cd84 100644 --- a/packages/nestjs-trpc/lib/trpc.module.ts +++ b/packages/nestjs-trpc/lib/trpc.module.ts @@ -54,6 +54,7 @@ export class TRPCModule implements OnModuleInit { imports.push( GeneratorModule.forRoot({ outputDirPath: options.autoSchemaFile, + tsConfigFilePath: options.tsConfigFilePath, rootModuleFilePath: callerFilePath, schemaFileImports: options.schemaFileImports, context: options.context, From 4901cc22c92e54162bbf62ad9737bac0475c83df Mon Sep 17 00:00:00 2001 From: Ting Lei Date: Fri, 3 Jan 2025 17:11:08 +0800 Subject: [PATCH 5/6] feat: generator: flatten enum to literval value --- .../procedure.generator.spec.ts.snap | 11 ++++++ .../__tests__/procedure.generator.spec.ts | 31 +++++++++++++++ .../lib/generators/procedure.generator.ts | 38 ++++++++++++++----- 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/packages/nestjs-trpc/lib/generators/__tests__/__snapshots__/procedure.generator.spec.ts.snap b/packages/nestjs-trpc/lib/generators/__tests__/__snapshots__/procedure.generator.spec.ts.snap index 1e60607..179e713 100644 --- a/packages/nestjs-trpc/lib/generators/__tests__/__snapshots__/procedure.generator.spec.ts.snap +++ b/packages/nestjs-trpc/lib/generators/__tests__/__snapshots__/procedure.generator.spec.ts.snap @@ -19,3 +19,14 @@ exports[`ProcedureGenerator flattenZodSchema should flatten all chained call exp .describe('Options to find many items'), })" `; + +exports[`ProcedureGenerator flattenZodSchema should flatten enum to literal value 1`] = ` +"z.object({ + options: z + .object({ + userId: z.string().describe('ID of the current user'), + type: z.literal('Normal').describe('Type of the item') + }) + .describe('Options to find many items'), + })" +`; diff --git a/packages/nestjs-trpc/lib/generators/__tests__/procedure.generator.spec.ts b/packages/nestjs-trpc/lib/generators/__tests__/procedure.generator.spec.ts index 35d5e35..4ec6996 100644 --- a/packages/nestjs-trpc/lib/generators/__tests__/procedure.generator.spec.ts +++ b/packages/nestjs-trpc/lib/generators/__tests__/procedure.generator.spec.ts @@ -107,5 +107,36 @@ describe('ProcedureGenerator', () => { const result = procedureGenerator.flattenZodSchema(node, sourceFile, project, node.getText()); expect(result).toMatchSnapshot(); }); + + it('should flatten enum to literal value', () => { + project.createSourceFile( + "types.ts", + ` + export enum TypeEnum { Normal = 'Normal', Unknown = 'Unknown' }; + `, + { overwrite: true } + ); + const sourceFile: SourceFile = project.createSourceFile( + "test.ts", + ` + import { z } from 'zod'; + import { TypeEnum } from './types'; + + const FindManyInput = z.object({ + options: z + .object({ + userId: z.string().describe('ID of the current user'), + type: z.literal(TypeEnum.Normal).describe('Type of the item') + }) + .describe('Options to find many items'), + }); + `, + { overwrite: true } + ); + + const node = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier).find((identifier) => identifier.getText() === "FindManyInput") as Identifier; + const result = procedureGenerator.flattenZodSchema(node, sourceFile, project, node.getText()); + expect(result).toMatchSnapshot(); + }); }); }); \ No newline at end of file diff --git a/packages/nestjs-trpc/lib/generators/procedure.generator.ts b/packages/nestjs-trpc/lib/generators/procedure.generator.ts index d888e40..181d2d9 100644 --- a/packages/nestjs-trpc/lib/generators/procedure.generator.ts +++ b/packages/nestjs-trpc/lib/generators/procedure.generator.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { ProcedureGeneratorMetadata } from '../interfaces/generator.interface'; import { ProcedureType } from '../trpc.enum'; -import { Project, SourceFile, Node } from 'ts-morph'; +import { Project, SourceFile, Node, SyntaxKind } from 'ts-morph'; import { ImportsScanner } from '../scanners/imports.scanner'; import { StaticGenerator } from './static.generator'; import { TYPESCRIPT_APP_ROUTER_SOURCE_FILE } from './generator.constants'; @@ -48,7 +48,34 @@ export class ProcedureGenerator { sourceFile, project, ); - if (Node.isIdentifier(node)) { + if (Node.isPropertyAccessExpression(node)) { + const propertyAccess = node.asKindOrThrow( + SyntaxKind.PropertyAccessExpression, + ); + const enumName = propertyAccess.getExpression().getText(); + const enumDeclaration = + sourceFile.getEnum(enumName) ?? + importsMap + .get(enumName) + ?.initializer?.asKind(SyntaxKind.EnumDeclaration); + + let enumValue: string | undefined; + if (enumDeclaration) { + enumValue = enumDeclaration + .getMember(propertyAccess.getName()) + ?.getInitializer() + ?.getText(); + } + + schema = + enumValue ?? + this.flattenZodSchema( + node.getExpression(), + sourceFile, + project, + node.getExpression().getText(), + ); + } else if (Node.isIdentifier(node)) { const identifierName = node.getText(); const identifierDeclaration = sourceFile.getVariableDeclaration(identifierName); @@ -150,13 +177,6 @@ export class ProcedureGenerator { ); } } - } else if (Node.isPropertyAccessExpression(node)) { - schema = this.flattenZodSchema( - node.getExpression(), - sourceFile, - project, - node.getExpression().getText(), - ); } return schema; From 07db26980d22fee80b412405cc5e18a185193051 Mon Sep 17 00:00:00 2001 From: Ting Lei Date: Mon, 6 Jan 2025 20:02:33 +0800 Subject: [PATCH 6/6] feat: trpc-module: allow configuring `rootModuleFilePath` in options --- .../nestjs-trpc/lib/interfaces/module-options.interface.ts | 5 +++++ packages/nestjs-trpc/lib/trpc.module.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/nestjs-trpc/lib/interfaces/module-options.interface.ts b/packages/nestjs-trpc/lib/interfaces/module-options.interface.ts index c1c5243..26b20df 100644 --- a/packages/nestjs-trpc/lib/interfaces/module-options.interface.ts +++ b/packages/nestjs-trpc/lib/interfaces/module-options.interface.ts @@ -24,6 +24,11 @@ export interface TRPCModuleOptions { */ tsConfigFilePath?: string; + /** + * The file path of the module that calls the TRPCModule. + */ + rootModuleFilePath?: string; + /** * Specifies additional imports for the schema file. This array can include functions, objects, or Zod schemas. * While `nestjs-trpc` typically handles imports automatically, this option allows manual inclusion of imports for exceptional cases. diff --git a/packages/nestjs-trpc/lib/trpc.module.ts b/packages/nestjs-trpc/lib/trpc.module.ts index b65cd84..6760e85 100644 --- a/packages/nestjs-trpc/lib/trpc.module.ts +++ b/packages/nestjs-trpc/lib/trpc.module.ts @@ -55,7 +55,7 @@ export class TRPCModule implements OnModuleInit { GeneratorModule.forRoot({ outputDirPath: options.autoSchemaFile, tsConfigFilePath: options.tsConfigFilePath, - rootModuleFilePath: callerFilePath, + rootModuleFilePath: options.rootModuleFilePath ?? callerFilePath, schemaFileImports: options.schemaFileImports, context: options.context, }),