Last active
July 19, 2025 06:41
-
-
Save devpanda0/5747d7b4703ffc51240f63c8e73d18c1 to your computer and use it in GitHub Desktop.
Script to auto-generate entity classes, DTOs, and associated files from Prisma models
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { InterfaceDeclaration, Project, SyntaxKind } from 'ts-morph'; | |
| import * as path from 'path'; | |
| import * as fs from 'fs'; | |
| interface Property { | |
| propName: string; | |
| propType: string; | |
| isOptional: boolean; | |
| validator: string; | |
| example: string; | |
| isRelation: boolean; | |
| } | |
| /** | |
| * Script to auto-generate entity classes, DTOs, and associated files from Prisma models. | |
| * | |
| * This script analyzes the Prisma client type definitions and generates: | |
| * - Entity classes with optional Swagger decorators | |
| * - Create DTOs with validation decorators | |
| * - Update DTOs with validation decorators | |
| * - A barrel file (index.ts) exporting all generated classes | |
| */ | |
| // Centralized configuration | |
| const config = { | |
| // Paths | |
| outputDir: path.resolve(__dirname, '../../src/v2/entities'), | |
| prismaClientPath: 'prisma/client/index.d.ts', | |
| // Options | |
| generateEntities: true, // Controls whether entity classes are generated | |
| generateDTOs: true, // Controls whether DTO classes are generated | |
| includeSwagger: true, // Set to false to exclude Swagger decorators | |
| includeClassValidator: true, // Set to false to exclude class-validator decorators | |
| combineDtos: true, // Set to true to combine Create and Update DTOs in the same file | |
| }; | |
| // Utility functions | |
| /** | |
| * Removes import expressions from Prisma type references | |
| */ | |
| function cleanType(type: string): string { | |
| return type.replace(/import\([^)]+\)\.Prisma\./g, 'Prisma.'); | |
| } | |
| /** | |
| * Converts first letter of a string to lowercase | |
| */ | |
| function lowerFirstLetter(str: string): string { | |
| return str.charAt(0).toLowerCase() + str.substring(1); | |
| } | |
| /** | |
| * Determines the appropriate class-validator decorator based on property type | |
| */ | |
| function getValidatorForType(type: string): string { | |
| if (type.includes('number') || type.includes('Decimal') || type.includes('Int')) { | |
| return 'IsNumber()'; | |
| } else if (type.includes('string') || type.includes('String')) { | |
| return 'IsString()'; | |
| } else if (type.includes('boolean') || type.includes('Boolean')) { | |
| return 'IsBoolean()'; | |
| } else if (type.includes('Date')) { | |
| return 'IsDate()'; | |
| } else { | |
| return 'IsObject()'; | |
| } | |
| } | |
| /** | |
| * Generates contextual example values for Swagger documentation | |
| */ | |
| function getExampleForType(type: string, propName: string): string { | |
| if (type.includes('number') || type.includes('Decimal')) { | |
| return '123.45'; | |
| } else if (type.includes('Int')) { | |
| return '42'; | |
| } else if (type.includes('string') || type.includes('String')) { | |
| if (propName.toLowerCase().includes('email')) { | |
| return '"user@example.com"'; | |
| } else if (propName.toLowerCase().includes('name')) { | |
| return '"John Doe"'; | |
| } else if (propName.toLowerCase().includes('password')) { | |
| return '"securePassword123"'; | |
| } else { | |
| return '"example"'; | |
| } | |
| } else if (type.includes('boolean') || type.includes('Boolean')) { | |
| return 'true'; | |
| } else if (type.includes('Date')) { | |
| return 'new Date()'; | |
| } else { | |
| return '{}'; | |
| } | |
| } | |
| // Import templates | |
| function getImports() { | |
| const imports: string[] = []; | |
| if (config.includeClassValidator) { | |
| imports.push( | |
| "import { IsOptional, IsString, IsNumber, IsBoolean, IsDate, IsObject } from 'class-validator';" | |
| ); | |
| } | |
| if (config.includeSwagger) { | |
| imports.unshift("import { ApiProperty } from '@nestjs/swagger';"); | |
| } | |
| return imports; | |
| } | |
| /** | |
| * Creates a Swagger decorator string | |
| */ | |
| function getSwaggerDecorator( | |
| prop: Property, | |
| modelName: string, | |
| isUpdate: boolean = false | |
| ): string | null { | |
| if (!config.includeSwagger) return null; | |
| return `@ApiProperty({ | |
| example: ${prop.example}, | |
| required: ${isUpdate ? false : !prop.isOptional}, | |
| description: '${prop.propName} ${isUpdate ? 'for updating' : 'of'} a ${modelName}' | |
| })`; | |
| } | |
| /** | |
| * Generates the content for entity class files | |
| */ | |
| function getEntityContent(modelName: string, properties: Property[]): string { | |
| const imports = config.includeSwagger | |
| ? ["import { ApiProperty } from '@nestjs/swagger';"] | |
| : []; | |
| return `${imports.join('\n')} | |
| ${imports.length > 0 ? '\n' : ''}export class ${modelName} { | |
| ${properties | |
| .map((prop) => { | |
| const decorators: string[] = []; | |
| if (config.includeSwagger) { | |
| const res = getSwaggerDecorator(prop, modelName); | |
| if (res) decorators.push(res); | |
| } | |
| const decoratorString = | |
| decorators.length > 0 | |
| ? decorators.filter(Boolean).join('\n ') + '\n ' | |
| : ''; | |
| return ` ${decoratorString}${prop.propName}${ | |
| prop.isOptional ? '?' : '' | |
| }: ${prop.propType};`; | |
| }) | |
| .join('\n\n')} | |
| }`; | |
| } | |
| function getCreateDtoContent(modelName: string, properties: Property[]): string { | |
| const imports: string[] = getImports(); | |
| return `${imports.join('\n')} | |
| export class Create${modelName}Dto { | |
| ${properties | |
| .map((prop) => { | |
| const decorators: string[] = []; | |
| if (config.includeSwagger) { | |
| const res = getSwaggerDecorator(prop, modelName); | |
| if (res) decorators.push(res); | |
| } | |
| if (config.includeClassValidator) { | |
| if (prop.isOptional) { | |
| decorators.push('@IsOptional()'); | |
| } | |
| decorators.push(`@${prop.validator}`); | |
| } | |
| return ` ${decorators.filter(Boolean).join('\n ')} | |
| ${prop.propName}${prop.isOptional ? '?' : ''}: ${prop.propType};`; | |
| }) | |
| .join('\n\n')} | |
| }`; | |
| } | |
| function getUpdateDtoContent(modelName: string, properties: Property[]): string { | |
| const imports = getImports(); | |
| return `${imports.join('\n')} | |
| export class Update${modelName}Dto { | |
| ${properties | |
| .map((prop) => { | |
| const decorators: string[] = []; | |
| if (config.includeSwagger) { | |
| const res = getSwaggerDecorator(prop, modelName, true); | |
| if (res) decorators.push(res); | |
| } | |
| if (config.includeClassValidator) { | |
| decorators.push('@IsOptional()'); | |
| decorators.push(`@${prop.validator}`); | |
| } | |
| return ` ${decorators.filter(Boolean).join('\n ')} | |
| ${prop.propName}?: ${prop.propType};`; | |
| }) | |
| .join('\n\n')} | |
| }`; | |
| } | |
| function getCombinedDtoContent(modelName: string, properties: Property[]): string { | |
| const imports = getImports(); | |
| return `${imports.join('\n')} | |
| export class Create${modelName}Dto { | |
| ${properties | |
| .map((prop) => { | |
| const decorators: string[] = []; | |
| if (config.includeSwagger) { | |
| const res = getSwaggerDecorator(prop, modelName); | |
| if (res) decorators.push(res); | |
| } | |
| if (config.includeClassValidator) { | |
| if (prop.isOptional) { | |
| decorators.push('@IsOptional()'); | |
| } | |
| decorators.push(`@${prop.validator}`); | |
| } | |
| return ` ${decorators.filter(Boolean).join('\n ')} | |
| ${prop.propName}${prop.isOptional ? '?' : ''}: ${prop.propType};`; | |
| }) | |
| .join('\n\n')} | |
| } | |
| export class Update${modelName}Dto { | |
| ${properties | |
| .map((prop) => { | |
| const decorators: string[] = []; | |
| if (config.includeSwagger) { | |
| const res = getSwaggerDecorator(prop, modelName, true); | |
| if (res) decorators.push(res); | |
| } | |
| if (config.includeClassValidator) { | |
| decorators.push('@IsOptional()'); | |
| decorators.push(`@${prop.validator}`); | |
| } | |
| return ` ${decorators.filter(Boolean).join('\n ')} | |
| ${prop.propName}?: ${prop.propType};`; | |
| }) | |
| .join('\n\n')} | |
| }`; | |
| } | |
| /** | |
| * Converts Prisma-specific types to native TypeScript types | |
| */ | |
| function convertPrismaTypeToTsType(type: string): string { | |
| const fieldRefMatch = type.match(/Prisma\.FieldRef<[^,]+,\s*"([^"]+)"/); | |
| if (fieldRefMatch) { | |
| const prismaType = fieldRefMatch[1]; | |
| switch (prismaType) { | |
| case 'String': | |
| return 'string'; | |
| case 'Int': | |
| return 'number'; | |
| case 'Float': | |
| return 'number'; | |
| case 'Decimal': | |
| return 'number'; | |
| case 'Boolean': | |
| return 'boolean'; | |
| case 'DateTime': | |
| return 'Date'; | |
| case 'Json': | |
| return 'any'; | |
| default: | |
| return 'any'; | |
| } | |
| } | |
| return type; | |
| } | |
| /** | |
| * Processes model interfaces and generates corresponding entity/DTO files | |
| * @param models Array of model interfaces to process | |
| * @returns Array of model names that were processed | |
| */ | |
| function processModels(models: InterfaceDeclaration[]) { | |
| const modelNames: string[] = []; | |
| // Process each model | |
| models.forEach((modelType) => { | |
| const modelName = modelType.getName().replace('FieldRefs', ''); | |
| if (!modelName) return; | |
| modelNames.push(modelName); | |
| console.log(`Generating classes for ${modelName}...`); | |
| // Extract properties | |
| const properties: Property[] = modelType.getProperties().map((prop) => { | |
| const propName = prop.getName(); | |
| let propType: string; | |
| try { | |
| propType = prop.getType().getText(); | |
| } catch (e) { | |
| const propText = prop.getText(); | |
| const typeMatch = propText.match(/:\s*([^;]+)/); | |
| propType = typeMatch ? typeMatch[1].trim() : 'any'; | |
| } | |
| propType = cleanType(propType); | |
| propType = convertPrismaTypeToTsType(propType); | |
| const isOptional = prop.hasQuestionToken(); | |
| const validator = getValidatorForType(propType); | |
| const example = getExampleForType(propType, propName); | |
| const isRelation = | |
| !propType.includes('string') && | |
| !propType.includes('number') && | |
| !propType.includes('boolean') && | |
| !propType.includes('Date') && | |
| !propName.endsWith('Id'); | |
| return { propName, propType, isOptional, validator, example, isRelation }; | |
| }); | |
| // Generate entity file if enabled | |
| if (config.generateEntities) { | |
| const entityContent = getEntityContent(modelName, properties); | |
| fs.writeFileSync( | |
| path.resolve(config.outputDir, `${lowerFirstLetter(modelName)}.entity.ts`), | |
| entityContent | |
| ); | |
| } | |
| // Generate DTOs if enabled | |
| if (config.generateDTOs) { | |
| if (config.combineDtos) { | |
| // Write combined DTO file | |
| const combinedDtoContent = getCombinedDtoContent(modelName, properties); | |
| fs.writeFileSync( | |
| path.resolve(config.outputDir, `${lowerFirstLetter(modelName)}.dto.ts`), | |
| combinedDtoContent | |
| ); | |
| } else { | |
| // Write separate DTO files | |
| const createDtoContent = getCreateDtoContent(modelName, properties); | |
| const updateDtoContent = getUpdateDtoContent(modelName, properties); | |
| fs.writeFileSync( | |
| path.resolve(config.outputDir, `create${modelName}.dto.ts`), | |
| createDtoContent | |
| ); | |
| fs.writeFileSync( | |
| path.resolve(config.outputDir, `update${modelName}.dto.ts`), | |
| updateDtoContent | |
| ); | |
| } | |
| } | |
| }); | |
| return modelNames; | |
| } | |
| /** | |
| * Generates a barrel file (index.ts) that exports all entities and DTOs | |
| * @param modelNames Array of model names to include in the barrel file | |
| */ | |
| function generateBarrelFile(modelNames: string[]) { | |
| // Entity imports - only if entities are being generated | |
| const entityImports = config.generateEntities | |
| ? modelNames | |
| .map( | |
| (name) => `import { ${name} } from './${lowerFirstLetter(name)}.entity';` | |
| ) | |
| .join('\n') | |
| : ''; | |
| let normalExports = []; | |
| // Add entity exports if entities are being generated | |
| if (config.generateEntities) { | |
| normalExports.push( | |
| ...modelNames.map( | |
| (name) => `export { ${name} } from './${lowerFirstLetter(name)}.entity';` | |
| ) | |
| ); | |
| } | |
| // Add DTO exports if DTOs are being generated | |
| if (config.generateDTOs) { | |
| if (config.combineDtos) { | |
| normalExports.push( | |
| ...modelNames.map( | |
| (name) => | |
| `export { Create${name}Dto, Update${name}Dto } from './${lowerFirstLetter( | |
| name | |
| )}.dto';` | |
| ) | |
| ); | |
| } else { | |
| normalExports.push( | |
| ...modelNames.flatMap((name) => [ | |
| `export { Create${name}Dto } from './create${name}.dto';`, | |
| `export { Update${name}Dto } from './update${name}.dto';`, | |
| ]) | |
| ); | |
| } | |
| } | |
| let entitiesArray = ''; | |
| if (config.includeSwagger && config.generateEntities) { | |
| entitiesArray = ` | |
| // Array of all entities for Swagger extraModels | |
| export const ENTITIES = [${modelNames.join(', ')}]; | |
| `; | |
| } | |
| const barrelContent = `${entityImports} | |
| ${normalExports.join('\n')} | |
| ${entitiesArray}`; | |
| fs.writeFileSync(path.resolve(config.outputDir, 'index.ts'), barrelContent); | |
| } | |
| // Main execution flow | |
| if (!fs.existsSync(config.outputDir)) { | |
| fs.mkdirSync(config.outputDir, { recursive: true }); | |
| } | |
| const project = new Project(); | |
| project.addSourceFilesAtPaths([config.prismaClientPath]); | |
| const prismaFile = project.getSourceFileOrThrow(config.prismaClientPath); | |
| // Find the Prisma namespace | |
| const namespaceDeclarations = prismaFile | |
| .getDescendantsOfKind(SyntaxKind.ModuleDeclaration) | |
| .filter((decl) => decl.getName() === 'Prisma'); | |
| if (namespaceDeclarations.length === 0) { | |
| throw new Error('Prisma namespace not found'); | |
| } | |
| const prismaNamespace = namespaceDeclarations[0]; | |
| const namespaceBody = prismaNamespace.getBody(); | |
| if (!namespaceBody) { | |
| throw new Error('Prisma namespace body not found'); | |
| } | |
| // Find model interfaces | |
| const modelTypes = namespaceBody | |
| .getDescendantsOfKind(SyntaxKind.InterfaceDeclaration) | |
| .filter((i) => { | |
| const name = i.getName() || ''; | |
| return ( | |
| !name.includes('Delegate') && | |
| !name.includes('Client') && | |
| !name.includes('Args') && | |
| !name.includes('Payload') && | |
| name !== 'TypeMapCb' | |
| ); | |
| }); | |
| // Fall back to alternative approach if no models found in namespace | |
| if (modelTypes.length === 0) { | |
| console.log('No models found in Prisma namespace. Trying alternative approach...'); | |
| const allInterfaces = prismaFile.getDescendantsOfKind( | |
| SyntaxKind.InterfaceDeclaration | |
| ); | |
| const possibleModels = allInterfaces.filter((i) => { | |
| const name = i.getName() || ''; | |
| return ( | |
| !name.includes('Delegate') && | |
| !name.includes('Client') && | |
| !name.includes('Args') && | |
| !name.includes('Payload') && | |
| name !== 'TypeMapCb' | |
| ); | |
| }); | |
| console.log(`Found ${possibleModels.length} possible model interfaces.`); | |
| // Process models using same logic | |
| const modelNames = processModels(possibleModels); | |
| // Generate barrel file | |
| generateBarrelFile(modelNames); | |
| console.log(`Generation complete! Files written to ${config.outputDir}`); | |
| } else { | |
| // Process models | |
| const modelNames = processModels(modelTypes); | |
| // Generate barrel file | |
| generateBarrelFile(modelNames); | |
| console.log(`Generation complete! Files written to ${config.outputDir}`); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment