Skip to content

Instantly share code, notes, and snippets.

@devpanda0
Last active July 19, 2025 06:41
Show Gist options
  • Select an option

  • Save devpanda0/5747d7b4703ffc51240f63c8e73d18c1 to your computer and use it in GitHub Desktop.

Select an option

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
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