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

Fix flattening zod schema - chained calls, enum value, tsConfigFilePath #59

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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'),
})"
`;

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'),
})"
`;
Original file line number Diff line number Diff line change
@@ -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>(GeneratorModule);
});

it('should be defined', () => {
expect(generatorModule).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,15 +10,14 @@ 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({
providers: [
ProcedureGenerator,
{
provide: ImportsScanner,
useValue: jest.fn(),
useValue: new ImportsScanner(),
},
{
provide: StaticGenerator,
Expand Down Expand Up @@ -69,4 +68,75 @@ 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();
});

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();
});
});
});
1 change: 1 addition & 0 deletions packages/nestjs-trpc/lib/generators/generator.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export interface GeneratorModuleOptions {
context?: Class<TRPCContext>;
outputDirPath?: string;
schemaFileImports?: Array<SchemaImports>;
tsConfigFilePath?: string;
}
5 changes: 4 additions & 1 deletion packages/nestjs-trpc/lib/generators/generator.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
48 changes: 39 additions & 9 deletions packages/nestjs-trpc/lib/generators/procedure.generator.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -140,13 +167,16 @@ export class ProcedureGenerator {
this.flattenZodSchema(arg, sourceFile, project, argText),
);
}
} else if (Node.isPropertyAccessExpression(node)) {
schema = this.flattenZodSchema(
node.getExpression(),
sourceFile,
project,
node.getExpression().getText(),
);

for (const child of expression.getChildren()) {
if (Node.isCallExpression(child)) {
const childText = child.getText();
schema = schema.replace(
childText,
this.flattenZodSchema(child, sourceFile, project, childText),
);
}
}
}

return schema;
Expand Down
10 changes: 10 additions & 0 deletions packages/nestjs-trpc/lib/interfaces/module-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ export interface TRPCModuleOptions {
*/
autoSchemaFile?: string;

/**
* Path to project tsconfig.
*/
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.
Expand Down
3 changes: 2 additions & 1 deletion packages/nestjs-trpc/lib/trpc.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export class TRPCModule implements OnModuleInit {
imports.push(
GeneratorModule.forRoot({
outputDirPath: options.autoSchemaFile,
rootModuleFilePath: callerFilePath,
tsConfigFilePath: options.tsConfigFilePath,
rootModuleFilePath: options.rootModuleFilePath ?? callerFilePath,
schemaFileImports: options.schemaFileImports,
context: options.context,
}),
Expand Down