nestjs-reverse-engineering
Version:
A powerful TypeScript/NestJS library for database reverse engineering, entity generation, and CRUD operations
268 lines • 12.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EntityBuilder = void 0;
/* eslint-disable prettier/prettier */
const file_builder_1 = require("./file-builder");
const type_mapper_1 = require("../utils/type-mapper");
const naming_utils_1 = require("../utils/naming-utils");
const path = require("path");
class EntityBuilder {
constructor(options, allTables, dialect) {
this.options = options;
this.allTables = allTables;
this.dialect = dialect;
}
/**
* Generate entity class for a table
*/
async generateEntity(table) {
const fileBuilder = new file_builder_1.FileBuilder({
outputPath: this.getEntityFilePath(table.tableName),
overwrite: true,
createDirectories: true
});
const className = naming_utils_1.NamingUtils.toSafeClassName(table.tableName);
const imports = this.generateImports(table);
// Add file header comment
fileBuilder.addComment('Generated by reverse-engineering tool', 'line');
fileBuilder.addEmptyLine();
// Add imports
imports.forEach(importStatement => {
fileBuilder.addLine(importStatement);
});
fileBuilder.addEmptyLine();
// Add table comment if available
if (table.tableComment && this.options.includeComments) {
fileBuilder.addComment(table.tableComment, 'block');
}
// Add entity decorator
const tableDecorator = this.generateTableDecorator(table);
fileBuilder.addLine(tableDecorator);
// Start class
fileBuilder.startBlock(`export class ${className}`);
// Generate properties
for (const column of table.columns) {
await this.generateProperty(fileBuilder, column, table);
fileBuilder.addEmptyLine();
}
// Generate relationships if enabled
if (this.options.includeRelations) {
await this.generateRelationships(fileBuilder, table);
}
// End class
fileBuilder.endBlock();
// Write to file
await fileBuilder.writeToFile();
console.log(`Generated entity: ${className} -> ${this.getEntityFilePath(table.tableName)}`);
}
generateImports(table) {
const imports = [];
const typeOrmImports = new Set(['Entity', 'Column']);
const entityImports = new Set();
// Check if we need PrimaryGeneratedColumn, PrimaryColumn, or composite primary key decorators
const primaryKeys = table.primaryKeys;
if (primaryKeys.length === 1) {
const pkColumn = table.columns.find(col => col.columnName === primaryKeys[0]);
if (pkColumn?.isAutoIncrement) {
typeOrmImports.add('PrimaryGeneratedColumn');
}
else {
typeOrmImports.add('PrimaryColumn');
}
}
else if (primaryKeys.length > 1) {
typeOrmImports.add('PrimaryColumn');
}
// Check for relationships
if (this.options.includeRelations && table.foreignKeys.length > 0) {
typeOrmImports.add('ManyToOne');
typeOrmImports.add('JoinColumn');
// Add imports for referenced entities
table.foreignKeys.forEach(fk => {
const referencedClassName = naming_utils_1.NamingUtils.toSafeClassName(fk.referencedTableName);
const fileName = naming_utils_1.NamingUtils.toFileName(fk.referencedTableName);
entityImports.add(`import { ${referencedClassName} } from './${fileName}.entity';`);
});
}
// Check for reverse relationships
const reverseRelations = this.findReverseRelations(table);
if (this.options.includeRelations && reverseRelations.length > 0) {
typeOrmImports.add('OneToMany');
// Add imports for related entities
reverseRelations.forEach(relation => {
const relatedClassName = naming_utils_1.NamingUtils.toSafeClassName(relation.tableName);
const fileName = naming_utils_1.NamingUtils.toFileName(relation.tableName);
entityImports.add(`import { ${relatedClassName} } from './${fileName}.entity';`);
});
}
// Check for enum types
const hasEnums = table.columns.some(col => col.enumValues && col.enumValues.length > 0);
if (hasEnums) {
// We'll generate enums inline or import them
}
imports.push(`import { ${Array.from(typeOrmImports).sort().join(', ')} } from 'typeorm';`);
// Add entity imports
Array.from(entityImports).sort().forEach(entityImport => {
imports.push(entityImport);
});
return imports;
}
generateTableDecorator(table) {
const nameInfo = naming_utils_1.NamingUtils.handleCompositeName(table.tableName);
if (nameInfo.quoted || table.tableSchema !== 'public') {
return `@Entity({ name: '${table.tableName}', schema: '${table.tableSchema}' })`;
}
else {
return `@Entity('${table.tableName}')`;
}
}
async generateProperty(fileBuilder, column, table) {
const propertyName = naming_utils_1.NamingUtils.toSafePropertyName(column.columnName, this.options.namingConvention);
const typeMapping = type_mapper_1.TypeMapper.mapType(column.dataType, this.dialect, column.isNullable);
const isPrimaryKey = table.primaryKeys.includes(column.columnName);
// Add comment if enabled
if (column.columnComment && this.options.includeComments) {
fileBuilder.addComment(column.columnComment);
}
const decorators = [];
// Primary key decorators
if (isPrimaryKey) {
if (column.isAutoIncrement && table.primaryKeys.length === 1) {
decorators.push('@PrimaryGeneratedColumn()');
}
else {
decorators.push('@PrimaryColumn()');
}
}
else {
// Regular column decorator
const columnOptions = this.generateColumnOptions(column, typeMapping);
decorators.push(`@Column(${columnOptions})`);
}
// Generate property
fileBuilder.addProperty(propertyName, typeMapping.tsType + (typeMapping.isOptional ? ' | null' : ''), {
decorators,
optional: typeMapping.isOptional,
comment: column.columnComment && this.options.includeComments ? column.columnComment : undefined
});
}
generateColumnOptions(column, typeMapping) {
const options = {};
// Handle column name mismatch
const propertyName = naming_utils_1.NamingUtils.toSafePropertyName(column.columnName, this.options.namingConvention);
const nameInfo = naming_utils_1.NamingUtils.handleCompositeName(column.columnName);
if (nameInfo.quoted || nameInfo.safe !== column.columnName || propertyName !== column.columnName) {
options.name = `'${column.columnName}'`;
}
// Type mapping
if (typeMapping.typeormType !== 'varchar' || typeMapping.typeormType === 'text') {
options.type = `'${typeMapping.typeormType}'`;
}
// Length
if (column.characterMaximumLength && typeMapping.typeormType === 'varchar') {
options.length = column.characterMaximumLength;
}
// Precision and scale for numeric types
if (column.numericPrecision && ['decimal', 'numeric'].includes(typeMapping.typeormType)) {
options.precision = column.numericPrecision;
if (column.numericScale) {
options.scale = column.numericScale;
}
}
// Nullable
if (!column.isNullable) {
options.nullable = false;
}
// Default value
if (column.defaultValue && !column.isAutoIncrement) {
// Parse and format default values
const defaultVal = column.defaultValue;
// Handle common default value patterns
if (defaultVal === 'CURRENT_TIMESTAMP' || defaultVal === 'now()') {
options.default = '() => "CURRENT_TIMESTAMP"';
}
else if (defaultVal.includes('nextval')) {
// Skip sequence defaults as they're handled by @PrimaryGeneratedColumn
}
else if (!isNaN(Number(defaultVal))) {
options.default = Number(defaultVal);
}
else if (defaultVal.startsWith("'") && defaultVal.endsWith("'")) {
options.default = defaultVal;
}
else {
options.default = `'${defaultVal}'`;
}
}
// Enum values
if (column.enumValues && column.enumValues.length > 0) {
options.enum = `[${column.enumValues.map(val => `'${val}'`).join(', ')}]`;
}
// Comment
if (column.columnComment && this.options.includeComments) {
options.comment = `'${column.columnComment.replace(/'/g, "\\'")}'`;
}
// Format options object
if (Object.keys(options).length === 0) {
return '';
}
const optionStrings = Object.entries(options).map(([key, value]) => {
if (typeof value === 'string' && (key === 'default' && value.startsWith('()'))) {
return `${key}: ${value}`;
}
return `${key}: ${value}`;
});
return `{ ${optionStrings.join(', ')} }`;
}
async generateRelationships(fileBuilder, table) {
for (const fk of table.foreignKeys) {
await this.generateManyToOneRelation(fileBuilder, fk);
fileBuilder.addEmptyLine();
}
// Generate reverse relationships (OneToMany)
const reverseRelations = this.findReverseRelations(table);
for (const relation of reverseRelations) {
await this.generateOneToManyRelation(fileBuilder, relation, table);
fileBuilder.addEmptyLine();
}
}
async generateManyToOneRelation(fileBuilder, fk) {
const referencedTable = this.allTables.find(t => t.tableName === fk.referencedTableName && t.tableSchema === fk.referencedTableSchema);
if (!referencedTable)
return;
const referencedClassName = naming_utils_1.NamingUtils.toSafeClassName(fk.referencedTableName);
const propertyName = naming_utils_1.NamingUtils.toRelationshipName(fk.referencedTableName, false);
fileBuilder.addLine(`@ManyToOne(() => ${referencedClassName})`);
fileBuilder.addLine(`@JoinColumn({ name: '${fk.columnName}' })`);
fileBuilder.addProperty(propertyName, referencedClassName, { optional: true });
}
async generateOneToManyRelation(fileBuilder, relation, table) {
const relatedClassName = naming_utils_1.NamingUtils.toSafeClassName(relation.tableName);
const propertyName = naming_utils_1.NamingUtils.toRelationshipName(relation.tableName, true);
fileBuilder.addLine(`@OneToMany(() => ${relatedClassName}, ${relation.propertyName} => ${relation.propertyName}.${naming_utils_1.NamingUtils.toRelationshipName(table.tableName, false)})`);
fileBuilder.addProperty(propertyName, `${relatedClassName}[]`, { optional: true });
}
findReverseRelations(table) {
const reverseRelations = [];
for (const otherTable of this.allTables) {
if (otherTable.tableName === table.tableName)
continue;
for (const fk of otherTable.foreignKeys) {
if (fk.referencedTableName === table.tableName && fk.referencedTableSchema === table.tableSchema) {
reverseRelations.push({
tableName: otherTable.tableName,
foreignKey: fk,
propertyName: naming_utils_1.NamingUtils.toRelationshipName(otherTable.tableName, false)
});
}
}
}
return reverseRelations;
}
getEntityFilePath(tableName) {
const fileName = naming_utils_1.NamingUtils.toFileName(tableName);
return path.join(this.options.outputPath, `${fileName}.entity.ts`);
}
}
exports.EntityBuilder = EntityBuilder;
//# sourceMappingURL=entity-builder.js.map