UNPKG

@dbcube/schema-builder

Version:

The Dbcube Query Builder is a lightweight, flexible, and fluent library for building queries across multiple database engines, including MySQL, PostgreSQL, SQLite, and MongoDB, using JavaScript/Node.js. Its agnostic design allows you to generate data man

1 lines 110 kB
{"version":3,"sources":["../src/index.ts","../src/lib/Schema.ts","../src/lib/FileUtils.ts","../src/lib/UIUtils.ts","../src/lib/CubeValidator.ts","../src/lib/DependencyResolver.ts"],"sourcesContent":["import { Schema } from './lib/Schema';\r\nimport { CubeValidator } from './lib/CubeValidator';\r\nimport { UIUtils } from './lib/UIUtils';\r\nimport { DependencyResolver } from './lib/DependencyResolver';\r\n\r\nexport default Schema;\r\nexport { Schema, CubeValidator, UIUtils, DependencyResolver };","import fs from 'fs';\r\nimport { Engine, TableProcessor, Config as ConfigClass } from \"@dbcube/core\";\r\nimport path from 'path';\r\nimport FileUtils from './FileUtils';\r\nimport chalk from 'chalk';\r\nimport { UIUtils, ProcessSummary, ProcessError } from './UIUtils';\r\nimport { CubeValidator } from './CubeValidator';\r\nimport { DependencyResolver } from './DependencyResolver';\r\nimport { createRequire } from 'module';\r\n\r\n/**\r\n * Main class to handle MySQL database connections and queries.\r\n * Implements the Singleton pattern to ensure a single instance of the connection pool.\r\n */\r\nclass Schema {\r\n private name: string;\r\n private engine: any;\r\n\r\n constructor(name: string) {\r\n this.name = name;\r\n this.engine = new Engine(name);\r\n }\r\n\r\n /**\r\n * Validates cube file comprehensively including syntax, database configuration, and structure\r\n * @param filePath - Path to the cube file\r\n * @returns validation result with any errors found\r\n */\r\n private validateDatabaseConfiguration(filePath: string): { isValid: boolean; error?: ProcessError } {\r\n try {\r\n // First, perform comprehensive cube file validation\r\n const cubeValidator = new CubeValidator();\r\n const cubeValidation = cubeValidator.validateCubeFile(filePath);\r\n\r\n // If cube file has syntax errors, return the first one\r\n if (!cubeValidation.isValid && cubeValidation.errors.length > 0) {\r\n return {\r\n isValid: false,\r\n error: cubeValidation.errors[0] // Return the first error found\r\n };\r\n }\r\n\r\n // Extract database name from cube file\r\n const dbResult = FileUtils.extractDatabaseNameFromCube(filePath);\r\n if (dbResult.status !== 200) {\r\n return {\r\n isValid: false,\r\n error: {\r\n itemName: path.basename(filePath, path.extname(filePath)),\r\n error: `Error reading database directive: ${dbResult.message}`,\r\n filePath,\r\n lineNumber: this.findDatabaseLineNumber(filePath)\r\n }\r\n };\r\n }\r\n\r\n const cubeDbName = dbResult.message;\r\n\r\n // Get available configurations\r\n const configInstance = new ConfigClass();\r\n const configFilePath = path.resolve(process.cwd(), 'dbcube.config.js');\r\n\r\n // Use __filename for CJS, process.cwd() for ESM\r\n const requireUrl = typeof __filename !== 'undefined' ? __filename : process.cwd();\r\n const require = createRequire(requireUrl);\r\n // Clear require cache to ensure fresh load\r\n delete require.cache[require.resolve(configFilePath)];\r\n const configModule = require(configFilePath);\r\n const configFn = configModule.default || configModule;\r\n\r\n if (typeof configFn === 'function') {\r\n configFn(configInstance);\r\n } else {\r\n throw new Error('❌ The dbcube.config.js file does not export a function.');\r\n }\r\n\r\n // Check if the database configuration exists\r\n const dbConfig = configInstance.getDatabase(cubeDbName);\r\n if (!dbConfig) {\r\n // Try to get available databases by attempting to access common ones\r\n let availableDbs: string[] = [];\r\n try {\r\n // Try some common database names to see what exists\r\n const testNames = ['test', 'development', 'production', 'local', 'main'];\r\n for (const testName of testNames) {\r\n try {\r\n const testConfig = configInstance.getDatabase(testName);\r\n if (testConfig) {\r\n availableDbs.push(testName);\r\n }\r\n } catch (e) {\r\n // Skip non-existent configs\r\n }\r\n }\r\n } catch (e) {\r\n // Fallback if we can't determine available databases\r\n }\r\n\r\n const availableText = availableDbs.length > 0 ? availableDbs.join(', ') : 'none found';\r\n return {\r\n isValid: false,\r\n error: {\r\n itemName: path.basename(filePath, path.extname(filePath)),\r\n error: `Database configuration '${cubeDbName}' not found in dbcube.config.js. Available: ${availableText}`,\r\n filePath,\r\n lineNumber: this.findDatabaseLineNumber(filePath)\r\n }\r\n };\r\n }\r\n\r\n return { isValid: true };\r\n\r\n } catch (error: any) {\r\n return {\r\n isValid: false,\r\n error: {\r\n itemName: path.basename(filePath, path.extname(filePath)),\r\n error: `Database configuration validation failed: ${error.message}`,\r\n filePath,\r\n lineNumber: this.findDatabaseLineNumber(filePath)\r\n }\r\n };\r\n }\r\n }\r\n\r\n /**\r\n * Finds the line number where @database directive is located\r\n */\r\n private findDatabaseLineNumber(filePath: string): number {\r\n try {\r\n const content = fs.readFileSync(filePath, 'utf8');\r\n const lines = content.split('\\n');\r\n for (let i = 0; i < lines.length; i++) {\r\n if (lines[i].includes('@database')) {\r\n return i + 1; // Line numbers start at 1\r\n }\r\n }\r\n return 1;\r\n } catch {\r\n return 1;\r\n }\r\n }\r\n\r\n /**\r\n * Extracts foreign key dependencies from a cube file\r\n */\r\n private extractForeignKeyDependencies(filePath: string): string[] {\r\n const dependencies: string[] = [];\r\n\r\n try {\r\n const content = fs.readFileSync(filePath, 'utf8');\r\n const lines = content.split('\\n');\r\n\r\n let insideForeignKey = false;\r\n let braceCount = 0;\r\n\r\n for (const line of lines) {\r\n // Check for foreign key start\r\n if (/foreign\\s*:\\s*\\{/.test(line)) {\r\n insideForeignKey = true;\r\n braceCount = 1;\r\n\r\n // Check if table is on the same line\r\n const sameLineMatch = line.match(/table\\s*:\\s*[\"']([^\"']+)[\"']/);\r\n if (sameLineMatch) {\r\n dependencies.push(sameLineMatch[1]);\r\n insideForeignKey = false;\r\n braceCount = 0;\r\n }\r\n continue;\r\n }\r\n\r\n if (insideForeignKey) {\r\n // Count braces to track if we're still inside the foreign object\r\n braceCount += (line.match(/\\{/g) || []).length;\r\n braceCount -= (line.match(/\\}/g) || []).length;\r\n\r\n // Look for table reference\r\n const tableMatch = line.match(/table\\s*:\\s*[\"']([^\"']+)[\"']/);\r\n if (tableMatch) {\r\n dependencies.push(tableMatch[1]);\r\n }\r\n\r\n // If braces are balanced, we're out of the foreign object\r\n if (braceCount === 0) {\r\n insideForeignKey = false;\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n console.error(`Error reading dependencies from ${filePath}:`, error);\r\n }\r\n\r\n return dependencies;\r\n }\r\n\r\n /**\r\n * Finds the line number where a foreign key table reference is located\r\n */\r\n private findForeignKeyLineNumber(filePath: string, tableName: string): number {\r\n try {\r\n const content = fs.readFileSync(filePath, 'utf8');\r\n const lines = content.split('\\n');\r\n for (let i = 0; i < lines.length; i++) {\r\n if (lines[i].includes(`table: \"${tableName}\"`) || lines[i].includes(`table: '${tableName}'`)) {\r\n return i + 1; // Line numbers start at 1\r\n }\r\n }\r\n return 1;\r\n } catch {\r\n return 1;\r\n }\r\n }\r\n\r\n async createDatabase(): Promise<any> {\r\n const startTime = Date.now();\r\n const rootPath = path.resolve(process.cwd());\r\n\r\n // Show header\r\n UIUtils.showOperationHeader(' CREATING DATABASE', this.name, '🗄️');\r\n\r\n // Show progress for database creation\r\n await UIUtils.showItemProgress('Preparando e instalando base de datos', 1, 1);\r\n\r\n try {\r\n const response = await this.engine.run('schema_engine', [\r\n '--action', 'create_database',\r\n '--path', rootPath,\r\n ]);\r\n\r\n if (response.status != 200) {\r\n returnFormattedError(response.status, response.message);\r\n }\r\n\r\n UIUtils.showItemSuccess('Database');\r\n\r\n // Show final summary\r\n const summary: ProcessSummary = {\r\n startTime,\r\n totalProcessed: 1,\r\n successCount: 1,\r\n errorCount: 0,\r\n processedItems: [this.name],\r\n operationName: 'create database',\r\n databaseName: this.name,\r\n errors: []\r\n };\r\n UIUtils.showOperationSummary(summary);\r\n\r\n return response.data;\r\n\r\n } catch (error: any) {\r\n UIUtils.showItemError('Database', error.message);\r\n const summary: ProcessSummary = {\r\n startTime,\r\n totalProcessed: 0,\r\n successCount: 0,\r\n errorCount: 1,\r\n processedItems: [],\r\n operationName: 'create database',\r\n databaseName: this.name,\r\n errors: []\r\n };\r\n UIUtils.showOperationSummary(summary);\r\n throw error;\r\n }\r\n }\r\n\r\n async refreshTables(): Promise<any> {\r\n const startTime = Date.now();\r\n const cubesDir = path.join(process.cwd(), 'dbcube');\r\n\r\n // Verificar si la carpeta existe\r\n if (!fs.existsSync(cubesDir)) {\r\n throw new Error('❌ The cubes folder does not exist');\r\n }\r\n\r\n const cubeFiles = FileUtils.getCubeFilesRecursively('dbcube', '.table.cube');\r\n if (cubeFiles.length === 0) {\r\n throw new Error('❌ There are no cubes to execute');\r\n }\r\n\r\n // Resolve dependencies and create execution order\r\n DependencyResolver.resolveDependencies(cubeFiles, 'table');\r\n\r\n // Order files based on dependencies\r\n const orderedCubeFiles = DependencyResolver.orderCubeFiles(cubeFiles, 'table');\r\n\r\n // Show header\r\n UIUtils.showOperationHeader('EXECUTING REFRESH TABLES', this.name, '🔄');\r\n\r\n let totalTablesProcessed = 0;\r\n let successCount = 0;\r\n let errorCount = 0;\r\n const processedTables: string[] = [];\r\n const errors: ProcessError[] = [];\r\n const failedTables = new Set<string>(); // Track failed table names\r\n\r\n for (let index = 0; index < orderedCubeFiles.length; index++) {\r\n const file = orderedCubeFiles[index];\r\n const filePath = path.isAbsolute(file) ? file : path.join(cubesDir, file);\r\n const stats = fs.statSync(filePath);\r\n\r\n if (stats.isFile()) {\r\n const getTableName = FileUtils.extracTableNameFromCube(filePath);\r\n const tableName = getTableName.status === 200 ? getTableName.message : path.basename(file, '.table.cube');\r\n\r\n // Show visual progress for each table\r\n await UIUtils.showItemProgress(tableName, index + 1, orderedCubeFiles.length);\r\n\r\n try {\r\n // Validate database configuration before processing\r\n const validation = this.validateDatabaseConfiguration(filePath);\r\n if (!validation.isValid && validation.error) {\r\n UIUtils.showItemError(tableName, validation.error.error);\r\n errors.push(validation.error);\r\n failedTables.add(tableName);\r\n errorCount++;\r\n continue;\r\n }\r\n\r\n // Check if any dependent tables failed\r\n const dependencies = this.extractForeignKeyDependencies(filePath);\r\n const missingDependencies = dependencies.filter(dep => failedTables.has(dep));\r\n\r\n if (missingDependencies.length > 0) {\r\n const dependencyError: ProcessError = {\r\n itemName: tableName,\r\n error: `Cannot refresh table '${tableName}' because it depends on failed table(s): ${missingDependencies.join(', ')}`,\r\n filePath,\r\n lineNumber: this.findForeignKeyLineNumber(filePath, missingDependencies[0])\r\n };\r\n UIUtils.showItemError(tableName, dependencyError.error);\r\n errors.push(dependencyError);\r\n failedTables.add(tableName);\r\n errorCount++;\r\n continue;\r\n }\r\n\r\n const dml = await this.engine.run('schema_engine', [\r\n '--action', 'parse_table',\r\n '--mode', 'refresh',\r\n '--schema-path', filePath,\r\n ]);\r\n if (dml.status != 200) {\r\n returnFormattedError(dml.status, dml.message);\r\n break;\r\n }\r\n const parseJson = JSON.stringify(dml.data.actions).replace(/[\\r\\n\\t]/g, '').replace(/\\\\[rnt]/g, '').replace(/\\s{2,}/g, ' ');\r\n\r\n const queries = await this.engine.run('schema_engine', [\r\n '--action', 'generate',\r\n '--mode', 'refresh',\r\n '--dml', parseJson,\r\n ]);\r\n if (queries.status != 200) {\r\n returnFormattedError(queries.status, queries.message);\r\n break;\r\n }\r\n delete queries.data.database_type;\r\n\r\n const parseJsonQueries = JSON.stringify(queries.data);\r\n\r\n const response = await this.engine.run('schema_engine', [\r\n '--action', 'execute',\r\n '--mode', 'refresh',\r\n '--dml', parseJsonQueries,\r\n ]);\r\n\r\n if (response.status != 200) {\r\n returnFormattedError(response.status, response.message);\r\n break;\r\n }\r\n const createQuery = queries.data.regular_queries.filter((q: string) => q.includes(\"CREATE\"))[0];\r\n\r\n await TableProcessor.saveQuery(dml.data.table, dml.data.database, createQuery);\r\n\r\n UIUtils.showItemSuccess(tableName);\r\n successCount++;\r\n processedTables.push(tableName);\r\n totalTablesProcessed++;\r\n\r\n } catch (error: any) {\r\n const processError: ProcessError = {\r\n itemName: tableName,\r\n error: error.message,\r\n filePath\r\n };\r\n UIUtils.showItemError(tableName, error.message);\r\n errors.push(processError);\r\n failedTables.add(tableName);\r\n errorCount++;\r\n }\r\n }\r\n }\r\n\r\n // Show final summary\r\n const summary: ProcessSummary = {\r\n startTime,\r\n totalProcessed: totalTablesProcessed,\r\n successCount,\r\n errorCount,\r\n processedItems: processedTables,\r\n operationName: 'refresh tables',\r\n databaseName: this.name,\r\n errors\r\n };\r\n UIUtils.showOperationSummary(summary);\r\n\r\n return totalTablesProcessed > 0 ? { processed: totalTablesProcessed, success: successCount, errors: errorCount } : null;\r\n }\r\n\r\n async freshTables(): Promise<any> {\r\n const startTime = Date.now();\r\n const cubesDir = path.join(process.cwd(), 'dbcube');\r\n\r\n // Verificar si la carpeta existe\r\n if (!fs.existsSync(cubesDir)) {\r\n throw new Error('❌ The cubes folder does not exist');\r\n }\r\n\r\n const cubeFiles = FileUtils.getCubeFilesRecursively('dbcube', '.table.cube');\r\n if (cubeFiles.length === 0) {\r\n throw new Error('❌ There are no cubes to execute');\r\n }\r\n\r\n // Resolve dependencies and create execution order\r\n DependencyResolver.resolveDependencies(cubeFiles, 'table');\r\n\r\n // Order files based on dependencies\r\n const orderedCubeFiles = DependencyResolver.orderCubeFiles(cubeFiles, 'table');\r\n\r\n // Show header\r\n UIUtils.showOperationHeader('EXECUTING FRESH TABLES', this.name);\r\n\r\n let totalTablesProcessed = 0;\r\n let successCount = 0;\r\n let errorCount = 0;\r\n const processedTables: string[] = [];\r\n const errors: ProcessError[] = [];\r\n const failedTables = new Set<string>(); // Track failed table names\r\n\r\n for (let index = 0; index < orderedCubeFiles.length; index++) {\r\n const file = orderedCubeFiles[index];\r\n const filePath = path.isAbsolute(file) ? file : path.join(cubesDir, file);\r\n const stats = fs.statSync(filePath);\r\n\r\n if (stats.isFile()) {\r\n const getTableName = FileUtils.extracTableNameFromCube(filePath);\r\n const tableName = getTableName.status === 200 ? getTableName.message : path.basename(file, '.table.cube');\r\n\r\n // Show visual progress for each table\r\n await UIUtils.showItemProgress(tableName, index + 1, orderedCubeFiles.length);\r\n\r\n try {\r\n // Validate database configuration before processing\r\n const validation = this.validateDatabaseConfiguration(filePath);\r\n if (!validation.isValid && validation.error) {\r\n UIUtils.showItemError(tableName, validation.error.error);\r\n errors.push(validation.error);\r\n failedTables.add(tableName);\r\n errorCount++;\r\n continue;\r\n }\r\n\r\n // Check if any dependent tables failed\r\n const dependencies = this.extractForeignKeyDependencies(filePath);\r\n const missingDependencies = dependencies.filter(dep => failedTables.has(dep));\r\n\r\n if (missingDependencies.length > 0) {\r\n const dependencyError: ProcessError = {\r\n itemName: tableName,\r\n error: `Cannot create table '${tableName}' because it depends on failed table(s): ${missingDependencies.join(', ')}`,\r\n filePath,\r\n lineNumber: this.findForeignKeyLineNumber(filePath, missingDependencies[0])\r\n };\r\n UIUtils.showItemError(tableName, dependencyError.error);\r\n errors.push(dependencyError);\r\n failedTables.add(tableName);\r\n errorCount++;\r\n continue;\r\n }\r\n\r\n const dml = await this.engine.run('schema_engine', [\r\n '--action', 'parse_table',\r\n '--schema-path', filePath,\r\n '--mode', 'fresh',\r\n ]);\r\n\r\n if (dml.status != 200) {\r\n returnFormattedError(dml.status, dml.message);\r\n break;\r\n }\r\n\r\n const parseJson = JSON.stringify(dml.data.actions).replace(/[\\r\\n\\t]/g, '').replace(/\\\\[rnt]/g, '').replace(/\\s{2,}/g, ' ');\r\n\r\n const queries = await this.engine.run('schema_engine', [\r\n '--action', 'generate',\r\n '--dml', parseJson,\r\n ]);\r\n\r\n if (queries.status != 200) {\r\n returnFormattedError(queries.status, queries.message);\r\n break;\r\n }\r\n\r\n delete queries.data._type;\r\n const createQuery = queries.data.regular_queries.filter((q: string) => q.includes(\"CREATE\"))[0];\r\n\r\n // For fresh mode, use the generated queries directly without alterations\r\n // generateAlterQueries is used for refresh mode, not fresh mode\r\n\r\n const parseJsonQueries = JSON.stringify(queries.data);\r\n\r\n const response = await this.engine.run('schema_engine', [\r\n '--action', 'execute',\r\n '--mode', 'fresh',\r\n '--dml', parseJsonQueries,\r\n ]);\r\n\r\n if (response.status != 200) {\r\n returnFormattedError(response.status, response.message);\r\n break;\r\n }\r\n\r\n await TableProcessor.saveQuery(dml.data.table, dml.data.database, createQuery);\r\n\r\n UIUtils.showItemSuccess(tableName);\r\n successCount++;\r\n processedTables.push(tableName);\r\n totalTablesProcessed++;\r\n\r\n } catch (error: any) {\r\n const processError: ProcessError = {\r\n itemName: tableName,\r\n error: error.message,\r\n filePath\r\n };\r\n UIUtils.showItemError(tableName, error.message);\r\n errors.push(processError);\r\n failedTables.add(tableName);\r\n errorCount++;\r\n }\r\n }\r\n }\r\n\r\n // Show final summary\r\n const summary: ProcessSummary = {\r\n startTime,\r\n totalProcessed: totalTablesProcessed,\r\n successCount,\r\n errorCount,\r\n processedItems: processedTables,\r\n operationName: 'fresh tables',\r\n databaseName: this.name,\r\n errors\r\n };\r\n UIUtils.showOperationSummary(summary);\r\n\r\n return totalTablesProcessed > 0 ? { processed: totalTablesProcessed, success: successCount, errors: errorCount } : null;\r\n }\r\n\r\n\r\n async executeSeeders(): Promise<any> {\r\n const startTime = Date.now();\r\n const cubesDir = path.join(process.cwd(), 'dbcube');\r\n\r\n // Verificar si la carpeta existe\r\n if (!fs.existsSync(cubesDir)) {\r\n throw new Error('❌ The cubes folder does not exist');\r\n }\r\n\r\n const cubeFiles = FileUtils.getCubeFilesRecursively('dbcube', '.seeder.cube');\r\n\r\n if (cubeFiles.length === 0) {\r\n throw new Error('❌ There are no cubes to execute');\r\n }\r\n\r\n // Use existing table dependency order for seeders\r\n const orderedCubeFiles = DependencyResolver.orderCubeFiles(cubeFiles, 'seeder');\r\n\r\n // Show header\r\n UIUtils.showOperationHeader('EXECUTING SEEDERS', this.name, '🌱');\r\n\r\n let totalSeedersProcessed = 0;\r\n let successCount = 0;\r\n let errorCount = 0;\r\n const processedSeeders: string[] = [];\r\n const errors: ProcessError[] = [];\r\n\r\n for (let index = 0; index < orderedCubeFiles.length; index++) {\r\n const file = orderedCubeFiles[index];\r\n const filePath = path.isAbsolute(file) ? file : path.join(cubesDir, file);\r\n const stats = fs.statSync(filePath);\r\n\r\n if (stats.isFile()) {\r\n const getSeederName = FileUtils.extracTableNameFromCube(filePath);\r\n const seederName = getSeederName.status === 200 ? getSeederName.message : path.basename(file, '.seeder.cube');\r\n\r\n // Show visual progress for each seeder\r\n await UIUtils.showItemProgress(seederName, index + 1, orderedCubeFiles.length);\r\n\r\n try {\r\n // Validate database configuration before processing\r\n const validation = this.validateDatabaseConfiguration(filePath);\r\n if (!validation.isValid && validation.error) {\r\n UIUtils.showItemError(seederName, validation.error.error);\r\n errors.push(validation.error);\r\n errorCount++;\r\n continue;\r\n }\r\n\r\n const response = await this.engine.run('schema_engine', [\r\n '--action', 'seeder',\r\n '--schema-path', filePath,\r\n ]);\r\n\r\n if (response.status != 200) {\r\n returnFormattedError(response.status, response.message);\r\n break;\r\n }\r\n\r\n UIUtils.showItemSuccess(seederName);\r\n successCount++;\r\n processedSeeders.push(seederName);\r\n totalSeedersProcessed++;\r\n\r\n } catch (error: any) {\r\n const processError: ProcessError = {\r\n itemName: seederName,\r\n error: error.message,\r\n filePath\r\n };\r\n UIUtils.showItemError(seederName, error.message);\r\n errors.push(processError);\r\n errorCount++;\r\n }\r\n }\r\n }\r\n\r\n // Show final summary\r\n const summary: ProcessSummary = {\r\n startTime,\r\n totalProcessed: totalSeedersProcessed,\r\n successCount,\r\n errorCount,\r\n processedItems: processedSeeders,\r\n operationName: 'seeders',\r\n databaseName: this.name,\r\n errors\r\n };\r\n UIUtils.showOperationSummary(summary);\r\n\r\n return totalSeedersProcessed > 0 ? { processed: totalSeedersProcessed, success: successCount, errors: errorCount } : null;\r\n }\r\n\r\n async executeTriggers(): Promise<any> {\r\n const startTime = Date.now();\r\n const cubesDir = path.join(process.cwd(), 'dbcube');\r\n const triggersDirExit = path.join(process.cwd(), 'dbcube', 'triggers');\r\n\r\n // Verificar si la carpeta existe\r\n if (!fs.existsSync(cubesDir)) {\r\n throw new Error('❌ The cubes folder does not exist');\r\n }\r\n\r\n const cubeFiles = FileUtils.getCubeFilesRecursively('dbcube', '.trigger.cube');\r\n\r\n if (cubeFiles.length === 0) {\r\n throw new Error('❌ There are no cubes to execute');\r\n }\r\n\r\n // Show header\r\n UIUtils.showOperationHeader('EXECUTING TRIGGERS', this.name, '⚡');\r\n\r\n let totalTriggersProcessed = 0;\r\n let successCount = 0;\r\n let errorCount = 0;\r\n const processedTriggers: string[] = [];\r\n const errors: ProcessError[] = [];\r\n\r\n for (let index = 0; index < cubeFiles.length; index++) {\r\n const file = cubeFiles[index];\r\n const filePath = path.isAbsolute(file) ? file : path.join(cubesDir, file);\r\n const stats = fs.statSync(filePath);\r\n\r\n if (stats.isFile()) {\r\n const getTriggerName = FileUtils.extracTableNameFromCube(filePath);\r\n const triggerName = getTriggerName.status === 200 ? getTriggerName.message : path.basename(file, '.trigger.cube');\r\n\r\n // Show visual progress for each trigger\r\n await UIUtils.showItemProgress(triggerName, index + 1, cubeFiles.length);\r\n\r\n try {\r\n // Validate database configuration before processing\r\n const validation = this.validateDatabaseConfiguration(filePath);\r\n if (!validation.isValid && validation.error) {\r\n UIUtils.showItemError(triggerName, validation.error.error);\r\n errors.push(validation.error);\r\n errorCount++;\r\n continue;\r\n }\r\n\r\n const response = await this.engine.run('schema_engine', [\r\n '--action', 'trigger',\r\n '--path-exit', triggersDirExit,\r\n '--schema-path', filePath,\r\n ]);\r\n\r\n if (response.status != 200) {\r\n returnFormattedError(response.status, response.message);\r\n break;\r\n }\r\n\r\n UIUtils.showItemSuccess(triggerName);\r\n successCount++;\r\n processedTriggers.push(triggerName);\r\n totalTriggersProcessed++;\r\n\r\n } catch (error: any) {\r\n const processError: ProcessError = {\r\n itemName: triggerName,\r\n error: error.message,\r\n filePath\r\n };\r\n UIUtils.showItemError(triggerName, error.message);\r\n errors.push(processError);\r\n errorCount++;\r\n }\r\n }\r\n }\r\n\r\n // Show final summary\r\n const summary: ProcessSummary = {\r\n startTime,\r\n totalProcessed: totalTriggersProcessed,\r\n successCount,\r\n errorCount,\r\n processedItems: processedTriggers,\r\n operationName: 'triggers',\r\n databaseName: this.name,\r\n errors\r\n };\r\n UIUtils.showOperationSummary(summary);\r\n\r\n return totalTriggersProcessed > 0 ? { processed: totalTriggersProcessed, success: successCount, errors: errorCount } : null;\r\n }\r\n}\r\n\r\n\r\nfunction returnFormattedError(status: number, message: string) {\r\n console.log(`\\n${chalk.red('🚫')} ${chalk.bold.red('ERRORS FOUND')}`);\r\n console.log(chalk.red('─'.repeat(60)));\r\n\r\n // Show error with [error] tag format\r\n console.log(`${chalk.red('[error]')} ${chalk.red(message)}`);\r\n console.log('');\r\n\r\n const err = new Error();\r\n const stackLines = err.stack?.split('\\n') || [];\r\n\r\n // Find the first stack line outside of node_modules\r\n const relevantStackLine = stackLines.find(line =>\r\n line.includes('.js:') && !line.includes('node_modules')\r\n );\r\n\r\n if (relevantStackLine) {\r\n const match = relevantStackLine.match(/\\((.*):(\\d+):(\\d+)\\)/) ||\r\n relevantStackLine.match(/at (.*):(\\d+):(\\d+)/);\r\n\r\n if (match) {\r\n const [, filePath, lineStr, columnStr] = match;\r\n const lineNum = parseInt(lineStr, 10);\r\n const errorLocation = `${filePath}:${lineStr}:${columnStr}`;\r\n\r\n // Show code location with [code] tag format\r\n console.log(`${chalk.cyan('[code]')} ${chalk.yellow(errorLocation)}`);\r\n\r\n // Show code context\r\n try {\r\n const codeLines = fs.readFileSync(filePath, 'utf-8').split('\\n');\r\n const start = Math.max(0, lineNum - 3);\r\n const end = Math.min(codeLines.length, lineNum + 2);\r\n\r\n for (let i = start; i < end; i++) {\r\n const line = codeLines[i];\r\n const lineLabel = `${i + 1}`.padStart(4, ' ');\r\n const pointer = i + 1 === lineNum ? `${chalk.red('<-')}` : ' ';\r\n console.log(`${chalk.gray(lineLabel)} ${pointer} ${chalk.white(line)}`);\r\n }\r\n } catch (err) {\r\n console.log(chalk.gray(' (unable to show code context)'));\r\n }\r\n }\r\n }\r\n\r\n console.log('');\r\n}\r\n\r\nexport default Schema;\r\nexport { Schema };","import * as fs from 'fs';\r\nimport * as path from 'path';\r\n\r\nclass FileUtils {\r\n /**\r\n * Verifica si un archivo existe (asincrónico).\r\n * @param filePath - Ruta del archivo.\r\n * @returns True si el archivo existe, false si no.\r\n */\r\n static async fileExists(filePath: string): Promise<boolean> {\r\n return new Promise((resolve) => {\r\n fs.access(path.resolve(filePath), fs.constants.F_OK, (err) => {\r\n resolve(!err);\r\n });\r\n });\r\n }\r\n\r\n /**\r\n * Verifica si un archivo existe (sincrónico).\r\n * @param filePath - Ruta del archivo.\r\n * @returns True si el archivo existe, false si no.\r\n */\r\n static fileExistsSync(filePath: string): boolean {\r\n try {\r\n fs.accessSync(path.resolve(filePath), fs.constants.F_OK);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n static extractDatabaseName(input: string): string | null {\r\n const match = input.match(/@database\\([\"']?([\\w-]+)[\"']?\\)/);\r\n return match ? match[1] : null;\r\n }\r\n\r\n /**\r\n * Lee recursivamente archivos que terminan en un sufijo dado y los ordena numéricamente.\r\n * @param dir - Directorio base (relativo o absoluto).\r\n * @param suffix - Sufijo de archivo (como 'table.cube').\r\n * @returns Rutas absolutas de los archivos encontrados y ordenados.\r\n */\r\n static getCubeFilesRecursively(dir: string, suffix: string): string[] {\r\n const baseDir = path.resolve(dir); // ✅ Asegura que sea absoluto\r\n const cubeFiles: string[] = [];\r\n\r\n function recurse(currentDir: string): void {\r\n const entries = fs.readdirSync(currentDir, { withFileTypes: true });\r\n\r\n for (const entry of entries) {\r\n const fullPath = path.join(currentDir, entry.name);\r\n\r\n if (entry.isDirectory()) {\r\n recurse(fullPath);\r\n } else if (entry.isFile() && entry.name.endsWith(suffix)) {\r\n cubeFiles.push(fullPath); // Ya es absoluta\r\n }\r\n }\r\n }\r\n\r\n recurse(baseDir);\r\n\r\n // Ordenar por número si los archivos comienzan con un número\r\n cubeFiles.sort((a, b) => {\r\n const aNum = parseInt(path.basename(a));\r\n const bNum = parseInt(path.basename(b));\r\n return (isNaN(aNum) ? 0 : aNum) - (isNaN(bNum) ? 0 : bNum);\r\n });\r\n\r\n return cubeFiles;\r\n }\r\n\r\n /**\r\n * Extracts database name from cube files\r\n * @param filePath - Path to the .cube file\r\n * @returns Object containing status and database name\r\n */\r\n static extractDatabaseNameFromCube(filePath: string) {\r\n try {\r\n const content = fs.readFileSync(filePath, 'utf8');\r\n \r\n // Pattern: @database(\"database_name\") or @database('database_name')\r\n const databaseMatch = content.match(/@database\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)\\s*;?/);\r\n if (databaseMatch) {\r\n return {\r\n status: 200,\r\n message: databaseMatch[1]\r\n };\r\n }\r\n \r\n throw new Error(`No @database directive found in file ${filePath}`);\r\n \r\n } catch (error: unknown) {\r\n if (error instanceof Error) {\r\n return {\r\n status: 500,\r\n message: error.message\r\n };\r\n }\r\n return {\r\n status: 500,\r\n message: String(error)\r\n };\r\n }\r\n }\r\n\r\n /**\r\n * Extrae nombres de tablas reales de archivos .cube\r\n * @param {string} filePath - String ruta del archivo .cube\r\n * @returns {object} - Objeto que contiene el estado y el mensaje con el nombre de la tabla\r\n */\r\n static extracTableNameFromCube(filePath: string) {\r\n try {\r\n const content = fs.readFileSync(filePath, 'utf8');\r\n\r\n // Patrón principal: @meta({ name: \"nombre_tabla\"; }) o @meta({ name: 'nombre_tabla'; })\r\n const metaMatch = content.match(/@meta\\s*\\(\\s*\\{\\s*name\\s*:\\s*[\"']([^\"']+)[\"']\\s*;\\s*[^}]*\\}\\s*\\)/s);\r\n if (metaMatch) {\r\n return {\r\n status: 200,\r\n message: metaMatch[1]\r\n };\r\n }\r\n\r\n throw new Error(`Error to execute this file ${filePath} because no exist a name of table.`);\r\n\r\n } catch (error: unknown) {\r\n if (error instanceof Error) {\r\n return {\r\n status: 500,\r\n message: error.message\r\n };\r\n }\r\n return {\r\n status: 500,\r\n message: String(error)\r\n };\r\n }\r\n }\r\n}\r\n\r\nexport default FileUtils;","import chalk from 'chalk';\r\nimport fs from 'fs';\r\n\r\nexport interface ProcessError {\r\n itemName: string;\r\n error: string;\r\n filePath?: string;\r\n lineNumber?: number;\r\n}\r\n\r\nexport interface ProcessSummary {\r\n startTime: number;\r\n totalProcessed: number;\r\n successCount: number;\r\n errorCount: number;\r\n processedItems: string[];\r\n operationName: string;\r\n databaseName: string;\r\n errors: ProcessError[];\r\n}\r\n\r\nexport class UIUtils {\r\n /**\r\n * Shows animated progress for processing items\r\n */\r\n static async showItemProgress(\r\n itemName: string,\r\n current: number,\r\n total: number\r\n ): Promise<void> {\r\n // Get console width, default to 80 if not available\r\n const consoleWidth = process.stdout.columns || 80;\r\n\r\n // Calculate available space for dots\r\n // Format: \"├─ itemName \" + dots + \" ✓ OK\"\r\n const prefix = `├─ ${itemName} `;\r\n const suffix = ` ✓ OK`;\r\n const availableSpace = consoleWidth - prefix.length - suffix.length;\r\n const maxDots = Math.max(10, availableSpace); // Minimum 10 dots\r\n\r\n return new Promise((resolve) => {\r\n process.stdout.write(`${chalk.blue('├─')} ${chalk.cyan(itemName)} `);\r\n\r\n let dotCount = 0;\r\n const interval = setInterval(() => {\r\n if (dotCount < maxDots) {\r\n process.stdout.write(chalk.gray('.'));\r\n dotCount++;\r\n } else {\r\n clearInterval(interval);\r\n resolve();\r\n }\r\n }, 10); // Faster animation\r\n });\r\n }\r\n\r\n /**\r\n * Shows success for a processed item\r\n */\r\n static showItemSuccess(itemName: string): void {\r\n process.stdout.write(` ${chalk.green('✓')} ${chalk.gray('OK')}\\n`);\r\n }\r\n\r\n /**\r\n * Shows error for an item (simplified - only shows X)\r\n */\r\n static showItemError(itemName: string, error: string): void {\r\n process.stdout.write(` ${chalk.red('✗')}\\n`);\r\n }\r\n\r\n /**\r\n * Shows operation header\r\n */\r\n static showOperationHeader(operationName: string, databaseName: string, icon: string = '🗑️'): void {\r\n console.log(`\\n${chalk.cyan(icon)} ${chalk.bold.green(operationName.toUpperCase())}`);\r\n console.log(chalk.gray('─'.repeat(60)));\r\n console.log(`${chalk.blue('┌─')} ${chalk.bold(`Database: ${databaseName}`)}`);\r\n }\r\n\r\n /**\r\n * Shows comprehensive operation summary\r\n */\r\n static showOperationSummary(summary: ProcessSummary): void {\r\n const { startTime, totalProcessed, successCount, errorCount, processedItems, operationName, databaseName } = summary;\r\n const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);\r\n\r\n console.log(`\\n${chalk.cyan('📊')} ${chalk.bold.green(`SUMMARY OF ${operationName.toUpperCase()}`)}`);\r\n console.log(chalk.gray('─'.repeat(60)));\r\n\r\n if (successCount > 0) {\r\n console.log(`${chalk.green('┌─')} ${chalk.bold('Successful processing:')}`);\r\n console.log(`${chalk.green('├─')} ${chalk.cyan(`Items processed: ${successCount}`)}`);\r\n console.log(`${chalk.green('├─')} ${chalk.gray(`Database: ${databaseName}`)}`);\r\n\r\n if (processedItems.length > 0) {\r\n console.log(`${chalk.green('├─')} ${chalk.yellow('Items updated:')}`);\r\n processedItems.forEach((item, index) => {\r\n const isLast = index === processedItems.length - 1;\r\n const connector = isLast ? '└─' : '├─';\r\n console.log(`${chalk.green('│ ')} ${chalk.gray(connector)} ${chalk.cyan(item)}`);\r\n });\r\n }\r\n }\r\n\r\n if (errorCount > 0) {\r\n console.log(`${chalk.red('├─')} ${chalk.bold.red(`Errors: ${errorCount}`)}`);\r\n }\r\n\r\n console.log(`${chalk.blue('├─')} ${chalk.gray(`Total time: ${totalTime}s`)}`);\r\n console.log(`${chalk.blue('└─')} ${chalk.bold(totalProcessed > 0 ? chalk.green('✅ Completed') : chalk.yellow('⚠️ No changes'))}`);\r\n\r\n // Show detailed errors section if there are errors\r\n if (summary.errors && summary.errors.length > 0) {\r\n console.log(`\\n${chalk.red('🚫')} ${chalk.bold.red('ERRORS FOUND')}`);\r\n console.log(chalk.red('─'.repeat(60)));\r\n \r\n summary.errors.forEach((error, index) => {\r\n // Show error with [error] tag format\r\n console.log(`${chalk.red('[error]')} ${chalk.red(error.error)}`);\r\n console.log('');\r\n \r\n if (error.filePath) {\r\n // Show code location with [code] tag format\r\n const location = error.lineNumber ? `${error.filePath}:${error.lineNumber}:7` : error.filePath;\r\n console.log(`${chalk.cyan('[code]')} ${chalk.yellow(location)}`);\r\n \r\n // Try to show code context if file exists\r\n UIUtils.showCodeContext(error.filePath, error.lineNumber || 1);\r\n }\r\n \r\n if (index < summary.errors.length - 1) {\r\n console.log('');\r\n }\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Shows code context around an error location\r\n */\r\n static showCodeContext(filePath: string, lineNumber: number, contextLines: number = 2): void {\r\n try {\r\n const content = fs.readFileSync(filePath, 'utf8');\r\n const lines = content.split('\\n');\r\n \r\n const startLine = Math.max(0, lineNumber - contextLines - 1);\r\n const endLine = Math.min(lines.length, lineNumber + contextLines);\r\n \r\n for (let i = startLine; i < endLine; i++) {\r\n const currentLineNum = i + 1;\r\n const line = lines[i];\r\n const lineNumStr = currentLineNum.toString().padStart(4, ' ');\r\n \r\n if (currentLineNum === lineNumber) {\r\n // Highlight the error line with arrow\r\n console.log(`${chalk.gray(lineNumStr)} ${chalk.red('<-')} ${chalk.white(line)}`);\r\n } else {\r\n // Normal context lines\r\n console.log(`${chalk.gray(lineNumStr)} ${chalk.white(line)}`);\r\n }\r\n }\r\n } catch (error) {\r\n // If we can't read the file, just skip showing context\r\n console.log(chalk.gray(' (unable to show code context)'));\r\n }\r\n }\r\n}","import fs from 'fs';\r\nimport path from 'path';\r\nimport { ProcessError } from './UIUtils';\r\n\r\nexport interface ValidationResult {\r\n isValid: boolean;\r\n errors: ProcessError[];\r\n}\r\n\r\nexport class CubeValidator {\r\n private validTypes = ['varchar', 'int', 'string', 'text', 'boolean', 'date', 'datetime', 'timestamp', 'decimal', 'float', 'double', 'enum', 'json'];\r\n private validOptions = ['not null', 'primary', 'autoincrement', 'unique', 'zerofill', 'index', 'required', 'unsigned'];\r\n private validProperties = ['type', 'length', 'options', 'value', 'defaultValue', 'foreign', 'enumValues', 'description'];\r\n private knownAnnotations = ['database', 'table', 'meta', 'columns', 'fields', 'dataset', 'beforeAdd', 'afterAdd', 'beforeUpdate', 'afterUpdate', 'beforeDelete', 'afterDelete', 'compute', 'column'];\r\n\r\n /**\r\n * Validates a cube file comprehensively\r\n */\r\n validateCubeFile(filePath: string): ValidationResult {\r\n const errors: ProcessError[] = [];\r\n \r\n try {\r\n const content = fs.readFileSync(filePath, 'utf8');\r\n const lines = content.split('\\n');\r\n const fileName = path.basename(filePath, path.extname(filePath));\r\n\r\n // Validate each line\r\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\r\n const line = lines[lineIndex];\r\n \r\n // Skip empty lines and comments\r\n if (line.trim() === '' || line.trim().startsWith('//')) {\r\n continue;\r\n }\r\n\r\n // Validate annotations\r\n this.validateAnnotations(line, lineIndex + 1, filePath, fileName, errors);\r\n \r\n // Validate data types\r\n this.validateDataTypes(line, lineIndex + 1, filePath, fileName, errors, content);\r\n \r\n // Validate column options\r\n this.validateColumnOptions(line, lineIndex + 1, filePath, fileName, errors, lines);\r\n \r\n // Validate column properties\r\n this.validateColumnProperties(line, lineIndex + 1, filePath, fileName, errors, content);\r\n \r\n // Validate required column properties\r\n this.validateRequiredColumnProperties(lines, lineIndex + 1, filePath, fileName, errors);\r\n \r\n // Validate general syntax\r\n this.validateGeneralSyntax(line, lineIndex + 1, filePath, fileName, errors);\r\n }\r\n\r\n // Validate overall structure\r\n this.validateOverallStructure(content, filePath, fileName, errors);\r\n\r\n } catch (error: any) {\r\n errors.push({\r\n itemName: path.basename(filePath, path.extname(filePath)),\r\n error: `Failed to read cube file: ${error.message}`,\r\n filePath,\r\n lineNumber: 1\r\n });\r\n }\r\n\r\n return {\r\n isValid: errors.length === 0,\r\n errors\r\n };\r\n }\r\n\r\n private validateAnnotations(line: string, lineNumber: number, filePath: string, fileName: string, errors: ProcessError[]): void {\r\n // Remove content inside strings to avoid false positives\r\n const lineWithoutStrings = line.replace(/\"[^\"]*\"/g, '\"\"').replace(/'[^']*'/g, \"''\");\r\n \r\n const anno