UNPKG

nestjs-reverse-engineering

Version:

A powerful TypeScript/NestJS library for database reverse engineering, entity generation, and CRUD operations

244 lines (243 loc) 9.85 kB
"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.EntityParser = void 0; /* eslint-disable prettier/prettier */ const fs = __importStar(require("fs")); const path = __importStar(require("path")); const glob_1 = require("glob"); class EntityParser { /** * Parse TypeORM entity files and extract table schemas */ static async parseEntityFiles(options) { const tables = []; const searchPattern = path.join(options.entitiesPath, options.filePattern); const files = await (0, glob_1.glob)(searchPattern, { ignore: ['**/index.ts', '**/*.spec.ts', '**/*.test.ts'], absolute: true }); for (const filePath of files) { const tableSchema = this.parseEntityFile(filePath); if (tableSchema) { tables.push(tableSchema); } } return tables.sort((a, b) => a.tableName.localeCompare(b.tableName)); } /** * Parse a single entity file and extract table schema */ static parseEntityFile(filePath) { try { const fileContent = fs.readFileSync(filePath, 'utf8'); // Extract table name from @Entity decorator const entityMatch = fileContent.match(/@Entity\(['"`]([^'"`]+)['"`]\)/); const tableName = entityMatch ? entityMatch[1] : this.getTableNameFromClass(fileContent); if (!tableName) { console.warn(`Could not extract table name from ${filePath}`); return null; } // Extract class name const classMatch = fileContent.match(/export\s+class\s+(\w+)/); const className = classMatch ? classMatch[1] : ''; // Parse columns from the file const columns = this.parseColumns(fileContent); // Parse primary keys const primaryKeys = columns .filter(col => col.isPrimaryKey) .map(col => col.columnName); return { tableName, tableSchema: 'public', // Default schema tableComment: this.extractTableComment(fileContent), columns, primaryKeys, foreignKeys: [], // Will be populated from column foreign key info indexes: [] }; } catch (error) { console.error(`Error parsing entity file ${filePath}:`, error); return null; } } /** * Get table name from class name if not specified in @Entity */ static getTableNameFromClass(fileContent) { const classMatch = fileContent.match(/export\s+class\s+(\w+)/); if (!classMatch) return null; // Convert PascalCase to snake_case const className = classMatch[1]; return className.replace(/([A-Z])/g, (match, char, index) => { return index === 0 ? char.toLowerCase() : `_${char.toLowerCase()}`; }); } /** * Parse columns from entity file content */ static parseColumns(fileContent) { const columns = []; // Find all property declarations with decorators - improved regex const propertyRegex = /@(Column|PrimaryGeneratedColumn|PrimaryColumn|CreateDateColumn|UpdateDateColumn|DeleteDateColumn|VersionColumn)\s*\([^)]*\)\s*\n\s*(\w+)(\?)?:\s*([^;|\n]+)/g; let match; while ((match = propertyRegex.exec(fileContent)) !== null) { const [fullMatch, decoratorType, propertyName, isOptional, tsType] = match; // Skip if propertyName is undefined if (!propertyName) { console.warn('Skipping property with undefined name:', fullMatch); continue; } // Extract column options from decorator const columnOptions = this.parseColumnOptions(fullMatch); const column = { columnName: columnOptions.name || this.camelToSnakeCase(propertyName), dataType: this.mapTsTypeToColumnType(tsType, columnOptions), isNullable: isOptional === '?' || columnOptions.nullable !== false, defaultValue: columnOptions.default, characterMaximumLength: columnOptions.length, numericPrecision: columnOptions.precision, numericScale: columnOptions.scale, columnComment: columnOptions.comment, comment: columnOptions.comment, // Alias isAutoIncrement: decoratorType === 'PrimaryGeneratedColumn', ordinalPosition: columns.length + 1, isPrimaryKey: decoratorType === 'PrimaryGeneratedColumn' || decoratorType === 'PrimaryColumn', isUnique: columnOptions.unique || false, // Handle foreign keys (would need more sophisticated parsing for relationships) foreignKeyTable: undefined, foreignKeyColumn: undefined, onDelete: undefined, onUpdate: undefined }; columns.push(column); } return columns; } /** * Parse column options from decorator string */ static parseColumnOptions(decoratorString) { const options = {}; // Extract options from decorator parameters const optionsMatch = decoratorString.match(/\(([^)]*)\)/); if (!optionsMatch) return options; const optionsStr = optionsMatch[1]; // Parse individual options (simplified parsing) const patterns = { name: /name:\s*['"`]([^'"`]+)['"`]/, type: /type:\s*['"`]([^'"`]+)['"`]/, length: /length:\s*(\d+)/, precision: /precision:\s*(\d+)/, scale: /scale:\s*(\d+)/, default: /default:\s*(?:\(\)\s*=>\s*['"`]([^'"`]+)['"`]|['"`]([^'"`]+)['"`]|(\w+))/, nullable: /nullable:\s*(true|false)/, unique: /unique:\s*(true|false)/, comment: /comment:\s*['"`]([^'"`]+)['"`]/ }; for (const [key, pattern] of Object.entries(patterns)) { const match = optionsStr.match(pattern); if (match) { if (key === 'nullable' || key === 'unique') { options[key] = match[1] === 'true'; } else if (key === 'length' || key === 'precision' || key === 'scale') { options[key] = parseInt(match[1], 10); } else if (key === 'default') { options[key] = match[1] || match[2] || match[3]; } else { options[key] = match[1]; } } } return options; } /** * Map TypeScript type to column type */ static mapTsTypeToColumnType(tsType, options) { // Remove null/undefined union types const cleanType = tsType.replace(/\s*\|\s*(null|undefined)/g, '').trim(); // Handle specific type mappings if (options.type) { return options.type; } switch (cleanType) { case 'string': if (options.length) return 'varchar'; return 'text'; case 'number': if (options.precision && options.scale) return 'decimal'; return 'int'; case 'boolean': return 'boolean'; case 'Date': return 'timestamp'; case 'Buffer': return 'bytea'; default: if (cleanType.endsWith('[]')) return 'json'; if (cleanType === 'any') return 'json'; return 'varchar'; } } /** * Convert camelCase to snake_case */ static camelToSnakeCase(str) { if (!str) return ''; return str.replace(/([A-Z])/g, (match, char, index) => { return index === 0 ? char.toLowerCase() : `_${char.toLowerCase()}`; }); } /** * Extract table comment from class comment */ static extractTableComment(fileContent) { const commentMatch = fileContent.match(/\/\*\*\s*\n\s*\*\s*([^\n]*)\s*\n\s*\*\//); return commentMatch ? commentMatch[1].trim() : undefined; } } exports.EntityParser = EntityParser;