nestjs-reverse-engineering
Version:
A powerful TypeScript/NestJS library for database reverse engineering, entity generation, and CRUD operations
434 lines (433 loc) • 17.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SqlGenerator = void 0;
/* eslint-disable prettier/prettier */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const database_types_1 = require("../types/database.types");
const type_mapper_1 = require("../utils/type-mapper");
const naming_utils_1 = require("../utils/naming-utils");
class SqlGenerator {
constructor(options = {}) {
this.options = {
dialect: database_types_1.DatabaseDialect.POSTGRES,
includeDropIfExists: false,
includeCreateIfNotExists: true,
includeComments: true,
engineType: 'InnoDB',
charset: 'utf8mb4',
collation: 'utf8mb4_unicode_ci',
outputPath: './generated/sql',
...options
};
this.typeMapper = new type_mapper_1.TypeMapper();
this.namingUtils = new naming_utils_1.NamingUtils();
}
/**
* Generate CREATE TABLE scripts from table schemas
*/
generateCreateTableScripts(tables) {
const sqlLines = [];
// Add header comment
sqlLines.push(`-- Generated CREATE TABLE scripts`);
sqlLines.push(`-- Dialect: ${this.options.dialect.toUpperCase()}`);
sqlLines.push(`-- Generated at: ${new Date().toISOString()}`);
sqlLines.push(`-- Total tables: ${tables.length}`);
sqlLines.push('');
// Add dialect-specific setup
this.addDialectSpecificSetup(sqlLines);
// Generate table creation scripts
tables.forEach((table, index) => {
this.generateSingleTableScript(table, sqlLines);
// Add separator between tables
if (index < tables.length - 1) {
sqlLines.push('');
sqlLines.push('-- ===================================');
sqlLines.push('');
}
});
// Add foreign key constraints at the end
sqlLines.push('');
sqlLines.push('-- ===================================');
sqlLines.push('-- FOREIGN KEY CONSTRAINTS');
sqlLines.push('-- ===================================');
sqlLines.push('');
tables.forEach(table => {
this.generateForeignKeyConstraints(table, sqlLines);
});
const sql = sqlLines.join('\n');
return {
sql,
tableCount: tables.length,
dialect: this.options.dialect,
options: this.options
};
}
/**
* Generate CREATE TABLE script for a single table
*/
generateSingleTableScript(table, sqlLines) {
const tableName = this.getFullTableName(table.tableName);
// Add table comment
if (this.options.includeComments) {
sqlLines.push(`-- Table: ${tableName}`);
if (table.tableComment) {
sqlLines.push(`-- ${table.tableComment}`);
}
sqlLines.push('');
}
// Add DROP IF EXISTS
if (this.options.includeDropIfExists) {
sqlLines.push(`DROP TABLE IF EXISTS ${tableName};`);
sqlLines.push('');
}
// Start CREATE TABLE
const createClause = this.options.includeCreateIfNotExists ? 'CREATE TABLE IF NOT EXISTS' : 'CREATE TABLE';
sqlLines.push(`${createClause} ${tableName} (`);
// Add columns
const columnDefinitions = [];
table.columns.forEach(column => {
columnDefinitions.push(this.generateColumnDefinition(column));
});
// Add primary key constraint
const primaryKeyColumns = table.columns
.filter(col => col.isPrimaryKey)
.map(col => this.escapeIdentifier(col.columnName));
if (primaryKeyColumns.length > 0) {
columnDefinitions.push(` CONSTRAINT ${this.escapeIdentifier(`pk_${table.tableName}`)} PRIMARY KEY (${primaryKeyColumns.join(', ')})`);
}
// Add unique constraints
table.columns
.filter(col => col.isUnique && !col.isPrimaryKey)
.forEach(col => {
columnDefinitions.push(` CONSTRAINT ${this.escapeIdentifier(`uk_${table.tableName}_${col.columnName}`)} UNIQUE (${this.escapeIdentifier(col.columnName)})`);
});
// Join column definitions
sqlLines.push(columnDefinitions.join(',\n'));
// Close CREATE TABLE with dialect-specific options
sqlLines.push(')');
// Add table-specific options
this.addTableOptions(table, sqlLines);
sqlLines.push(';');
// Add column comments (for dialects that support it)
this.addColumnComments(table, sqlLines);
// Add indexes
this.addIndexes(table, sqlLines);
}
/**
* Generate column definition
*/
generateColumnDefinition(column) {
const parts = [];
// Column name
parts.push(` ${this.escapeIdentifier(column.columnName)}`);
// Data type
parts.push(this.getColumnDataType(column));
// NOT NULL constraint
if (!column.isNullable && !column.isPrimaryKey) {
parts.push('NOT NULL');
}
// Default value
if (column.defaultValue !== null && column.defaultValue !== undefined) {
parts.push(`DEFAULT ${this.formatDefaultValue(column.defaultValue, column.dataType)}`);
}
// Auto increment / Serial
if (column.isAutoIncrement) {
if (this.options.dialect === 'postgres') {
// For PostgreSQL, we use SERIAL or BIGSERIAL
// This is handled in getColumnDataType
}
else if (this.options.dialect === 'mysql') {
parts.push('AUTO_INCREMENT');
}
}
return parts.join(' ');
}
/**
* Get column data type with proper dialect mapping
*/
getColumnDataType(column) {
// Handle auto increment columns
if (column.isAutoIncrement && this.options.dialect === 'postgres') {
if (column.dataType.includes('bigint') || column.dataType.includes('int8')) {
return 'BIGSERIAL';
}
return 'SERIAL';
}
// Map TypeORM types to SQL types
const sqlType = type_mapper_1.TypeMapper.mapToSqlType(column.dataType, this.options.dialect);
// Handle length/precision
if (column.characterMaximumLength) {
if (sqlType.includes('VARCHAR') || sqlType.includes('CHAR')) {
return `${sqlType}(${column.characterMaximumLength})`;
}
}
if (column.numericPrecision && column.numericScale !== null) {
if (sqlType.includes('DECIMAL') || sqlType.includes('NUMERIC')) {
return `${sqlType}(${column.numericPrecision}, ${column.numericScale})`;
}
}
return sqlType;
}
/**
* Format default value based on data type
*/
formatDefaultValue(defaultValue, dataType) {
if (defaultValue === null)
return 'NULL';
// Handle functions/expressions
if (typeof defaultValue === 'string' && defaultValue.includes('()')) {
if (this.options.dialect === 'postgres') {
if (defaultValue.includes('CURRENT_TIMESTAMP') || defaultValue.includes('NOW()')) {
return 'CURRENT_TIMESTAMP';
}
if (defaultValue.includes('CURRENT_DATE')) {
return 'CURRENT_DATE';
}
}
else if (this.options.dialect === 'mysql') {
if (defaultValue.includes('CURRENT_TIMESTAMP')) {
return 'CURRENT_TIMESTAMP';
}
}
return defaultValue;
}
// Handle string values
if (dataType.includes('varchar') || dataType.includes('char') || dataType.includes('text')) {
return `'${defaultValue.toString().replace(/'/g, "''")}'`;
}
// Handle boolean values
if (dataType.includes('boolean') || dataType.includes('bool')) {
if (this.options.dialect === 'postgres') {
return defaultValue ? 'true' : 'false';
}
else {
return defaultValue ? '1' : '0';
}
}
// Handle numeric values
if (dataType.includes('int') || dataType.includes('decimal') || dataType.includes('float') || dataType.includes('double')) {
return defaultValue.toString();
}
// Default case
return `'${defaultValue.toString()}'`;
}
/**
* Generate foreign key constraints
*/
generateForeignKeyConstraints(table, sqlLines) {
const foreignKeyColumns = table.columns.filter(col => col.foreignKeyTable);
if (foreignKeyColumns.length === 0)
return;
foreignKeyColumns.forEach(column => {
const tableName = this.getFullTableName(table.tableName);
const referencedTable = this.getFullTableName(column.foreignKeyTable);
const constraintName = `fk_${table.tableName}_${column.columnName}`;
sqlLines.push(`-- Foreign key: ${table.tableName}.${column.columnName} -> ${column.foreignKeyTable}.${column.foreignKeyColumn || 'id'}`);
const alterStatement = [
`ALTER TABLE ${tableName}`,
`ADD CONSTRAINT ${this.escapeIdentifier(constraintName)}`,
`FOREIGN KEY (${this.escapeIdentifier(column.columnName)})`,
`REFERENCES ${referencedTable} (${this.escapeIdentifier(column.foreignKeyColumn || 'id')})`
];
// Add referential actions
if (column.onDelete) {
alterStatement.push(`ON DELETE ${column.onDelete.toUpperCase()}`);
}
if (column.onUpdate) {
alterStatement.push(`ON UPDATE ${column.onUpdate.toUpperCase()}`);
}
sqlLines.push(alterStatement.join(' ') + ';');
sqlLines.push('');
});
}
/**
* Add dialect-specific setup commands
*/
addDialectSpecificSetup(sqlLines) {
if (this.options.dialect === 'mysql') {
sqlLines.push('-- MySQL specific settings');
sqlLines.push('SET FOREIGN_KEY_CHECKS = 0;');
sqlLines.push('SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";');
sqlLines.push('SET AUTOCOMMIT = 0;');
sqlLines.push('START TRANSACTION;');
sqlLines.push('SET time_zone = "+00:00";');
sqlLines.push('');
}
else if (this.options.dialect === 'postgres') {
sqlLines.push('-- PostgreSQL specific settings');
if (this.options.schemaName && this.options.schemaName !== 'public') {
sqlLines.push(`CREATE SCHEMA IF NOT EXISTS ${this.escapeIdentifier(this.options.schemaName)};`);
sqlLines.push(`SET search_path TO ${this.escapeIdentifier(this.options.schemaName)};`);
}
sqlLines.push('');
}
}
/**
* Add table-specific options
*/
addTableOptions(table, sqlLines) {
if (this.options.dialect === 'mysql') {
const options = [];
if (this.options.engineType) {
options.push(`ENGINE=${this.options.engineType}`);
}
if (this.options.charset) {
options.push(`DEFAULT CHARSET=${this.options.charset}`);
}
if (this.options.collation) {
options.push(`COLLATE=${this.options.collation}`);
}
if (options.length > 0) {
sqlLines[sqlLines.length - 1] = `)${options.join(' ')}`;
}
}
else if (this.options.dialect === 'postgres') {
const options = [];
if (this.options.tablespace) {
options.push(`TABLESPACE ${this.options.tablespace}`);
}
if (this.options.fillfactor) {
options.push(`WITH (fillfactor = ${this.options.fillfactor})`);
}
if (this.options.withOids) {
options.push('WITH OIDS');
}
if (options.length > 0) {
sqlLines[sqlLines.length - 1] = `) ${options.join(' ')}`;
}
}
}
/**
* Add column comments
*/
addColumnComments(table, sqlLines) {
if (!this.options.includeComments)
return;
const tableName = this.getFullTableName(table.tableName);
const columnsWithComments = table.columns.filter(col => col.comment);
if (columnsWithComments.length === 0)
return;
sqlLines.push('');
columnsWithComments.forEach(column => {
if (this.options.dialect === 'postgres') {
sqlLines.push(`COMMENT ON COLUMN ${tableName}.${this.escapeIdentifier(column.columnName)} IS '${column.comment?.replace(/'/g, "''")}';`);
}
else if (this.options.dialect === 'mysql') {
sqlLines.push(`ALTER TABLE ${tableName} MODIFY COLUMN ${this.escapeIdentifier(column.columnName)} ${this.getColumnDataType(column)} COMMENT '${column.comment?.replace(/'/g, "\\'")}';`);
}
});
}
/**
* Add indexes
*/
addIndexes(table, sqlLines) {
const tableName = this.getFullTableName(table.tableName);
// Add indexes for foreign key columns
table.columns
.filter(col => col.foreignKeyTable && !col.isPrimaryKey)
.forEach(column => {
const indexName = `idx_${table.tableName}_${column.columnName}`;
sqlLines.push('');
sqlLines.push(`CREATE INDEX ${this.escapeIdentifier(indexName)} ON ${tableName} (${this.escapeIdentifier(column.columnName)});`);
});
// Add indexes for unique columns (if not already primary key)
table.columns
.filter(col => col.isUnique && !col.isPrimaryKey && !col.foreignKeyTable)
.forEach(column => {
const indexName = `idx_${table.tableName}_${column.columnName}_unique`;
sqlLines.push('');
sqlLines.push(`CREATE UNIQUE INDEX ${this.escapeIdentifier(indexName)} ON ${tableName} (${this.escapeIdentifier(column.columnName)});`);
});
}
/**
* Get full table name with schema prefix
*/
getFullTableName(tableName) {
const escapedTableName = this.escapeIdentifier(tableName);
if (this.options.schemaName && this.options.dialect === 'postgres') {
return `${this.escapeIdentifier(this.options.schemaName)}.${escapedTableName}`;
}
return escapedTableName;
}
/**
* Escape SQL identifiers
*/
escapeIdentifier(identifier) {
if (this.options.dialect === 'postgres') {
return `"${identifier}"`;
}
else if (this.options.dialect === 'mysql') {
return `\`${identifier}\``;
}
return identifier;
}
/**
* Save SQL script to file
*/
async saveSqlScript(result, filename) {
if (!this.options.outputPath) {
throw new Error('Output path not configured');
}
// Ensure output directory exists
if (!fs.existsSync(this.options.outputPath)) {
fs.mkdirSync(this.options.outputPath, { recursive: true });
}
const fileName = filename || `create_tables_${this.options.dialect}_${Date.now()}.sql`;
const filePath = path.join(this.options.outputPath, fileName);
fs.writeFileSync(filePath, result.sql, 'utf8');
return filePath;
}
/**
* Generate SQL from entity files in directory
*/
async generateFromEntityDirectory(entitiesPath) {
const { EntityParser } = await Promise.resolve().then(() => __importStar(require('./entity-parser')));
const tables = await EntityParser.parseEntityFiles({
entitiesPath,
filePattern: '**/*.entity.ts'
});
if (tables.length === 0) {
throw new Error(`No entity files found in ${entitiesPath}`);
}
console.log(`🔍 Found ${tables.length} entity files to process`);
tables.forEach(table => {
console.log(` - ${table.tableName} (${table.columns.length} columns)`);
});
return this.generateCreateTableScripts(tables);
}
}
exports.SqlGenerator = SqlGenerator;