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,389 lines (1,383 loc) 51.6 kB
// src/lib/Schema.ts import fs5 from "fs"; import { Engine, TableProcessor, Config as ConfigClass } from "@dbcube/core"; import path4 from "path"; // src/lib/FileUtils.ts import * as fs from "fs"; import * as path from "path"; var FileUtils = class { /** * Verifica si un archivo existe (asincrónico). * @param filePath - Ruta del archivo. * @returns True si el archivo existe, false si no. */ static async fileExists(filePath) { return new Promise((resolve2) => { fs.access(path.resolve(filePath), fs.constants.F_OK, (err) => { resolve2(!err); }); }); } /** * Verifica si un archivo existe (sincrónico). * @param filePath - Ruta del archivo. * @returns True si el archivo existe, false si no. */ static fileExistsSync(filePath) { try { fs.accessSync(path.resolve(filePath), fs.constants.F_OK); return true; } catch { return false; } } static extractDatabaseName(input) { const match = input.match(/@database\(["']?([\w-]+)["']?\)/); return match ? match[1] : null; } /** * Lee recursivamente archivos que terminan en un sufijo dado y los ordena numéricamente. * @param dir - Directorio base (relativo o absoluto). * @param suffix - Sufijo de archivo (como 'table.cube'). * @returns Rutas absolutas de los archivos encontrados y ordenados. */ static getCubeFilesRecursively(dir, suffix) { const baseDir = path.resolve(dir); const cubeFiles = []; function recurse(currentDir) { const entries = fs.readdirSync(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { recurse(fullPath); } else if (entry.isFile() && entry.name.endsWith(suffix)) { cubeFiles.push(fullPath); } } } recurse(baseDir); cubeFiles.sort((a, b) => { const aNum = parseInt(path.basename(a)); const bNum = parseInt(path.basename(b)); return (isNaN(aNum) ? 0 : aNum) - (isNaN(bNum) ? 0 : bNum); }); return cubeFiles; } /** * Extracts database name from cube files * @param filePath - Path to the .cube file * @returns Object containing status and database name */ static extractDatabaseNameFromCube(filePath) { try { const content = fs.readFileSync(filePath, "utf8"); const databaseMatch = content.match(/@database\s*\(\s*["']([^"']+)["']\s*\)\s*;?/); if (databaseMatch) { return { status: 200, message: databaseMatch[1] }; } throw new Error(`No @database directive found in file ${filePath}`); } catch (error) { if (error instanceof Error) { return { status: 500, message: error.message }; } return { status: 500, message: String(error) }; } } /** * Extrae nombres de tablas reales de archivos .cube * @param {string} filePath - String ruta del archivo .cube * @returns {object} - Objeto que contiene el estado y el mensaje con el nombre de la tabla */ static extracTableNameFromCube(filePath) { try { const content = fs.readFileSync(filePath, "utf8"); const metaMatch = content.match(/@meta\s*\(\s*\{\s*name\s*:\s*["']([^"']+)["']\s*;\s*[^}]*\}\s*\)/s); if (metaMatch) { return { status: 200, message: metaMatch[1] }; } throw new Error(`Error to execute this file ${filePath} because no exist a name of table.`); } catch (error) { if (error instanceof Error) { return { status: 500, message: error.message }; } return { status: 500, message: String(error) }; } } }; var FileUtils_default = FileUtils; // src/lib/Schema.ts import chalk2 from "chalk"; // src/lib/UIUtils.ts import chalk from "chalk"; import fs2 from "fs"; var UIUtils = class _UIUtils { /** * Shows animated progress for processing items */ static async showItemProgress(itemName, current, total) { const consoleWidth = process.stdout.columns || 80; const prefix = `\u251C\u2500 ${itemName} `; const suffix = ` \u2713 OK`; const availableSpace = consoleWidth - prefix.length - suffix.length; const maxDots = Math.max(10, availableSpace); return new Promise((resolve2) => { process.stdout.write(`${chalk.blue("\u251C\u2500")} ${chalk.cyan(itemName)} `); let dotCount = 0; const interval = setInterval(() => { if (dotCount < maxDots) { process.stdout.write(chalk.gray(".")); dotCount++; } else { clearInterval(interval); resolve2(); } }, 10); }); } /** * Shows success for a processed item */ static showItemSuccess(itemName) { process.stdout.write(` ${chalk.green("\u2713")} ${chalk.gray("OK")} `); } /** * Shows error for an item (simplified - only shows X) */ static showItemError(itemName, error) { process.stdout.write(` ${chalk.red("\u2717")} `); } /** * Shows operation header */ static showOperationHeader(operationName, databaseName, icon = "\u{1F5D1}\uFE0F") { console.log(` ${chalk.cyan(icon)} ${chalk.bold.green(operationName.toUpperCase())}`); console.log(chalk.gray("\u2500".repeat(60))); console.log(`${chalk.blue("\u250C\u2500")} ${chalk.bold(`Database: ${databaseName}`)}`); } /** * Shows comprehensive operation summary */ static showOperationSummary(summary) { const { startTime, totalProcessed, successCount, errorCount, processedItems, operationName, databaseName } = summary; const totalTime = ((Date.now() - startTime) / 1e3).toFixed(1); console.log(` ${chalk.cyan("\u{1F4CA}")} ${chalk.bold.green(`SUMMARY OF ${operationName.toUpperCase()}`)}`); console.log(chalk.gray("\u2500".repeat(60))); if (successCount > 0) { console.log(`${chalk.green("\u250C\u2500")} ${chalk.bold("Successful processing:")}`); console.log(`${chalk.green("\u251C\u2500")} ${chalk.cyan(`Items processed: ${successCount}`)}`); console.log(`${chalk.green("\u251C\u2500")} ${chalk.gray(`Database: ${databaseName}`)}`); if (processedItems.length > 0) { console.log(`${chalk.green("\u251C\u2500")} ${chalk.yellow("Items updated:")}`); processedItems.forEach((item, index) => { const isLast = index === processedItems.length - 1; const connector = isLast ? "\u2514\u2500" : "\u251C\u2500"; console.log(`${chalk.green("\u2502 ")} ${chalk.gray(connector)} ${chalk.cyan(item)}`); }); } } if (errorCount > 0) { console.log(`${chalk.red("\u251C\u2500")} ${chalk.bold.red(`Errors: ${errorCount}`)}`); } console.log(`${chalk.blue("\u251C\u2500")} ${chalk.gray(`Total time: ${totalTime}s`)}`); console.log(`${chalk.blue("\u2514\u2500")} ${chalk.bold(totalProcessed > 0 ? chalk.green("\u2705 Completed") : chalk.yellow("\u26A0\uFE0F No changes"))}`); if (summary.errors && summary.errors.length > 0) { console.log(` ${chalk.red("\u{1F6AB}")} ${chalk.bold.red("ERRORS FOUND")}`); console.log(chalk.red("\u2500".repeat(60))); summary.errors.forEach((error, index) => { console.log(`${chalk.red("[error]")} ${chalk.red(error.error)}`); console.log(""); if (error.filePath) { const location = error.lineNumber ? `${error.filePath}:${error.lineNumber}:7` : error.filePath; console.log(`${chalk.cyan("[code]")} ${chalk.yellow(location)}`); _UIUtils.showCodeContext(error.filePath, error.lineNumber || 1); } if (index < summary.errors.length - 1) { console.log(""); } }); } } /** * Shows code context around an error location */ static showCodeContext(filePath, lineNumber, contextLines = 2) { try { const content = fs2.readFileSync(filePath, "utf8"); const lines = content.split("\n"); const startLine = Math.max(0, lineNumber - contextLines - 1); const endLine = Math.min(lines.length, lineNumber + contextLines); for (let i = startLine; i < endLine; i++) { const currentLineNum = i + 1; const line = lines[i]; const lineNumStr = currentLineNum.toString().padStart(4, " "); if (currentLineNum === lineNumber) { console.log(`${chalk.gray(lineNumStr)} ${chalk.red("<-")} ${chalk.white(line)}`); } else { console.log(`${chalk.gray(lineNumStr)} ${chalk.white(line)}`); } } } catch (error) { console.log(chalk.gray(" (unable to show code context)")); } } }; // src/lib/CubeValidator.ts import fs3 from "fs"; import path2 from "path"; var CubeValidator = class { validTypes = ["varchar", "int", "string", "text", "boolean", "date", "datetime", "timestamp", "decimal", "float", "double", "enum", "json"]; validOptions = ["not null", "primary", "autoincrement", "unique", "zerofill", "index", "required", "unsigned"]; validProperties = ["type", "length", "options", "value", "defaultValue", "foreign", "enumValues", "description"]; knownAnnotations = ["database", "table", "meta", "columns", "fields", "dataset", "beforeAdd", "afterAdd", "beforeUpdate", "afterUpdate", "beforeDelete", "afterDelete", "compute", "column"]; /** * Validates a cube file comprehensively */ validateCubeFile(filePath) { const errors = []; try { const content = fs3.readFileSync(filePath, "utf8"); const lines = content.split("\n"); const fileName = path2.basename(filePath, path2.extname(filePath)); for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { const line = lines[lineIndex]; if (line.trim() === "" || line.trim().startsWith("//")) { continue; } this.validateAnnotations(line, lineIndex + 1, filePath, fileName, errors); this.validateDataTypes(line, lineIndex + 1, filePath, fileName, errors, content); this.validateColumnOptions(line, lineIndex + 1, filePath, fileName, errors, lines); this.validateColumnProperties(line, lineIndex + 1, filePath, fileName, errors, content); this.validateRequiredColumnProperties(lines, lineIndex + 1, filePath, fileName, errors); this.validateGeneralSyntax(line, lineIndex + 1, filePath, fileName, errors); } this.validateOverallStructure(content, filePath, fileName, errors); } catch (error) { errors.push({ itemName: path2.basename(filePath, path2.extname(filePath)), error: `Failed to read cube file: ${error.message}`, filePath, lineNumber: 1 }); } return { isValid: errors.length === 0, errors }; } validateAnnotations(line, lineNumber, filePath, fileName, errors) { const lineWithoutStrings = line.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''"); const annotationRegex = /@(\w+)/g; let match; while ((match = annotationRegex.exec(lineWithoutStrings)) !== null) { const annotation = match[1]; if (!this.knownAnnotations.includes(annotation)) { errors.push({ itemName: fileName, error: `Unknown annotation '@${annotation}'. Valid annotations: ${this.knownAnnotations.join(", ")}`, filePath, lineNumber }); } } } validateDataTypes(line, lineNumber, filePath, fileName, errors, content) { const typeRegex = /type:\s*["'](\w+)["']/g; let match; while ((match = typeRegex.exec(line)) !== null) { const type = match[1]; if (!this.validTypes.includes(type)) { errors.push({ itemName: fileName, error: `Invalid data type '${type}'. Valid types: ${this.validTypes.join(", ")}`, filePath, lineNumber }); } } if (line.includes('type: "varchar"')) { const lines = content.split("\n"); const hasLengthNearby = lines.slice(Math.max(0, lineNumber - 1), Math.min(lineNumber + 4, lines.length)).some((nextLine) => nextLine.includes("length:")); if (!hasLengthNearby) { errors.push({ itemName: fileName, error: "VARCHAR type requires a length specification", filePath, lineNumber }); } } } validateColumnOptions(line, lineNumber, filePath, fileName, errors, lines) { const optionsMatch = line.match(/^\s*options\s*:\s*\[(.*)\]\s*;?\s*$/); if (!optionsMatch) return; const optionsContent = optionsMatch[1].trim(); const invalidSyntaxMatch = optionsContent.match(/[^",\s]+(?![^"]*")/); if (invalidSyntaxMatch) { errors.push({ itemName: fileName, error: `Invalid syntax '${invalidSyntaxMatch[0]}' in options array. All values must be quoted strings`, filePath, lineNumber }); return; } const optionMatches = optionsContent.match(/"([^"]*)"/g); if (optionMatches) { const columnType = this.getColumnTypeForOptions(lines, lineNumber - 1); optionMatches.forEach((optionMatch) => { const option = optionMatch.replace(/"/g, ""); if (option.trim() === "") { errors.push({ itemName: fileName, error: "Empty option found in options array. All options must have a value", filePath, lineNumber }); } else if (!this.validOptions.includes(option)) { errors.push({ itemName: fileName, error: `Invalid option '${option}'. Valid options: ${this.validOptions.join(", ")}`, filePath, lineNumber }); } else if (columnType !== "unknown" && !this.isOptionCompatibleWithType(option, columnType)) { errors.push({ itemName: fileName, error: `Option '${option}' is not compatible with type '${columnType}'`, filePath, lineNumber }); } }); } } validateColumnProperties(line, lineNumber, filePath, fileName, errors, content) { const propertyKeyRegex = /^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/; const propMatch = propertyKeyRegex.exec(line); if (!propMatch) return; const propertyName = propMatch[1]; if (/^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*:\s*\{/.test(line)) { return; } if (this.isInsideForeignKeyObject(content, lineNumber - 1)) { const validForeignKeyProperties = ["table", "column"]; if (!validForeignKeyProperties.includes(propertyName)) { errors.push({ itemName: fileName, error: `Invalid foreign key property '${propertyName}'. Valid foreign key properties: ${validForeignKeyProperties.join(", ")}`, filePath, lineNumber }); } return; } if (this.isInsideColumnsBlock(content, lineNumber - 1)) { if (!this.validProperties.includes(propertyName)) { errors.push({ itemName: fileName, error: `Invalid property '${propertyName}'. Valid properties: ${this.validProperties.join(", ")}`, filePath, lineNumber }); } } if (/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*$/.test(line)) { errors.push({ itemName: fileName, error: `Property '${propertyName}' is missing a value`, filePath, lineNumber }); } } validateRequiredColumnProperties(lines, lineNumber, filePath, fileName, errors) { const line = lines[lineNumber - 1]; if (!/^\s*\}\s*;?\s*$/.test(line)) { return; } let columnStartLine = -1; let columnName = ""; for (let i = lineNumber - 2; i >= 0; i--) { const currentLine = lines[i]; const columnDefMatch = currentLine.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*\{/); if (columnDefMatch) { let openBraces = 0; let closeBraces = 0; for (let j = i; j < lineNumber; j++) { openBraces += (lines[j].match(/\{/g) || []).length; closeBraces += (lines[j].match(/\}/g) || []).length; } if (openBraces === closeBraces) { columnStartLine = i; columnName = columnDefMatch[1]; break; } } } if (columnStartLine === -1 || !columnName) return; let hasType = false; for (let i = columnStartLine + 1; i < lineNumber - 1; i++) { if (lines[i].match(/^\s*type\s*:/)) { hasType = true; break; } } if (!hasType && columnName !== "foreign" && columnName !== "defaultValue") { errors.push({ itemName: fileName, error: `Column '${columnName}' is missing required 'type' property`, filePath, lineNumber: columnStartLine + 1 }); } } validateGeneralSyntax(line, lineNumber, filePath, fileName, errors) { const quotes = line.match(/["']/g); if (quotes && quotes.length % 2 !== 0) { errors.push({ itemName: fileName, error: "Mismatched quotes detected", filePath, lineNumber }); } if (line.includes("@database") || line.includes("@table")) { const stringAnnotationRegex = /@(database|table)\s*\(\s*"([^"]*)"\s*\)/; if (!stringAnnotationRegex.test(line)) { errors.push({ itemName: fileName, error: 'Invalid annotation syntax. Expected format: @annotation("value")', filePath, lineNumber }); } } if (line.includes("@meta")) { const metaObjectRegex = /@meta\s*\(\s*\{/; if (!metaObjectRegex.test(line)) { errors.push({ itemName: fileName, error: "Invalid @meta syntax. Expected format: @meta({ ... })", filePath, lineNumber }); } } } validateOverallStructure(content, filePath, fileName, errors) { const lines = content.split("\n"); const hasDatabase = lines.some((line) => line.includes("@database")); if (!hasDatabase) { errors.push({ itemName: fileName, error: "Missing required @database annotation", filePath, lineNumber: 1 }); } if (filePath.includes(".table.cube")) { const hasColumns = lines.some((line) => line.includes("@columns")); if (!hasColumns) { errors.push({ itemName: fileName, error: "Table cube files require @columns annotation", filePath, lineNumber: 1 }); } } } getColumnTypeForOptions(lines, optionsLineIndex) { for (let i = optionsLineIndex - 1; i >= 0; i--) { const line = lines[i]; const typeMatch = line.match(/^\s*type\s*:\s*"([^"]+)"/); if (typeMatch) { return typeMatch[1]; } if (/^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*:\s*\{/.test(line)) { break; } } return "unknown"; } isOptionCompatibleWithType(option, type) { const compatibilityRules = { "zerofill": ["int", "decimal", "float", "double"], "unsigned": ["int", "decimal", "float", "double"], "autoincrement": ["int"], "primary": ["int", "varchar", "string"], "not null": ["int", "varchar", "string", "text", "boolean", "date", "datetime", "timestamp", "decimal", "float", "double"], "unique": ["int", "varchar", "string", "text"], "index": ["int", "varchar", "string", "text", "date", "datetime", "timestamp"], "required": ["int", "varchar", "string", "text", "boolean", "date", "datetime", "timestamp", "decimal", "float", "double"] }; const compatibleTypes = compatibilityRules[option]; if (!compatibleTypes) { return true; } return compatibleTypes.includes(type); } isInsideColumnsBlock(content, lineIndex) { const lines = content.split("\n"); let columnsStartLine = -1; let columnsEndLine = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].includes("@columns")) { columnsStartLine = i; let braceCount = 0; for (let j = i; j < lines.length; j++) { const currentLine = lines[j]; braceCount += (currentLine.match(/\{/g) || []).length; braceCount -= (currentLine.match(/\}/g) || []).length; if (braceCount === 0 && j > i) { columnsEndLine = j; break; } } break; } } return columnsStartLine !== -1 && columnsEndLine !== -1 && lineIndex > columnsStartLine && lineIndex < columnsEndLine; } isInsideForeignKeyObject(content, lineIndex) { const lines = content.split("\n"); for (let i = lineIndex; i >= 0; i--) { const line = lines[i]; if (/foreign\s*:\s*\{/.test(line)) { let braceCount = 0; for (let j = i; j <= lineIndex; j++) { const currentLine = lines[j]; const openBraces = (currentLine.match(/\{/g) || []).length; const closeBraces = (currentLine.match(/\}/g) || []).length; braceCount += openBraces - closeBraces; if (braceCount === 0 && j > i) { return false; } } return braceCount > 0; } if (line.trim() === "}" || line.includes("};")) { break; } } return false; } }; // src/lib/DependencyResolver.ts import fs4 from "fs"; import path3 from "path"; var DependencyResolver = class { /** * Resolves table dependencies and creates execution order */ static resolveDependencies(cubeFiles, cubeType = "table") { const tableDependencies = this.extractDependencies(cubeFiles, cubeType); const orderedTables = this.topologicalSort(tableDependencies); const executionOrder = { tables: cubeType === "table" ? orderedTables : [], seeders: cubeType === "seeder" ? orderedTables : [], timestamp: (/* @__PURE__ */ new Date()).toISOString() }; this.saveExecutionOrder(executionOrder); return executionOrder; } /** * Extracts dependencies from cube files */ static extractDependencies(cubeFiles, cubeType) { const dependencies = []; for (const file of cubeFiles) { let filePath; if (path3.isAbsolute(file)) { filePath = file; } else if (fs4.existsSync(file)) { filePath = path3.resolve(file); } else { filePath = path3.join(process.cwd(), "dbcube", file); } try { const tableNameResult = FileUtils_default.extracTableNameFromCube(filePath); const tableName = tableNameResult.status === 200 ? tableNameResult.message : path3.basename(file, `.${cubeType}.cube`); const deps = this.extractForeignKeyReferences(filePath); dependencies.push({ tableName, filePath, dependencies: deps }); } catch (error) { console.error(`Error processing ${filePath}:`, error); } } return dependencies; } /** * Extracts foreign key references from a cube file */ static extractForeignKeyReferences(filePath) { const dependencies = []; try { const content = fs4.readFileSync(filePath, "utf8"); const lines = content.split("\n"); let insideForeignKey = false; let braceCount = 0; for (const line of lines) { if (/foreign\s*:\s*\{/.test(line)) { insideForeignKey = true; braceCount = 1; const sameLineMatch = line.match(/table\s*:\s*["']([^"']+)["']/); if (sameLineMatch) { dependencies.push(sameLineMatch[1]); insideForeignKey = false; braceCount = 0; } continue; } if (insideForeignKey) { braceCount += (line.match(/\{/g) || []).length; braceCount -= (line.match(/\}/g) || []).length; const tableMatch = line.match(/table\s*:\s*["']([^"']+)["']/); if (tableMatch) { dependencies.push(tableMatch[1]); } if (braceCount === 0) { insideForeignKey = false; } } } } catch (error) { console.error(`Error reading file ${filePath}:`, error); } return dependencies; } /** * Performs topological sort to determine execution order */ static topologicalSort(dependencies) { const graph = /* @__PURE__ */ new Map(); const inDegree = /* @__PURE__ */ new Map(); const tableMap = /* @__PURE__ */ new Map(); for (const dep of dependencies) { graph.set(dep.tableName, dep.dependencies); inDegree.set(dep.tableName, 0); tableMap.set(dep.tableName, dep); } for (const dep of dependencies) { for (const dependency of dep.dependencies) { if (inDegree.has(dependency)) { inDegree.set(dep.tableName, (inDegree.get(dep.tableName) || 0) + 1); } } } const queue = []; const result = []; for (const [table, degree] of inDegree) { if (degree === 0) { queue.push(table); } } while (queue.length > 0) { const current = queue.shift(); result.push(current); const currentDeps = graph.get(current) || []; for (const neighbor of currentDeps) { if (inDegree.has(neighbor)) { const newDegree = (inDegree.get(neighbor) || 0) - 1; inDegree.set(neighbor, newDegree); if (newDegree === 0) { queue.push(neighbor); } } } } if (result.length !== dependencies.length) { for (const dep of dependencies) { if (!result.includes(dep.tableName)) { result.push(dep.tableName); } } } return result; } /** * Saves the execution order to .dbcube/orderexecute.json */ static saveExecutionOrder(order) { try { const projectRoot = process.cwd(); const dbcubeDir = path3.join(projectRoot, ".dbcube"); const orderFile = path3.join(dbcubeDir, "orderexecute.json"); if (!fs4.existsSync(dbcubeDir)) { fs4.mkdirSync(dbcubeDir, { recursive: true }); } fs4.writeFileSync(orderFile, JSON.stringify(order, null, 2), "utf8"); } catch (error) { console.error("\u274C Failed to save execution order:", error); } } /** * Loads the execution order from .dbcube/orderexecute.json */ static loadExecutionOrder() { try { const projectRoot = process.cwd(); const orderFile = path3.join(projectRoot, ".dbcube", "orderexecute.json"); if (!fs4.existsSync(orderFile)) { return null; } const content = fs4.readFileSync(orderFile, "utf8"); return JSON.parse(content); } catch (error) { console.error("\u274C Failed to load execution order:", error); return null; } } /** * Orders cube files based on saved execution order */ static orderCubeFiles(cubeFiles, cubeType) { const executionOrder = this.loadExecutionOrder(); if (!executionOrder) { return cubeFiles; } const orderList = executionOrder.tables; const orderedFiles = []; const fileMap = /* @__PURE__ */ new Map(); for (const file of cubeFiles) { const filePath = path3.isAbsolute(file) ? file : path3.join(process.cwd(), "dbcube", file); const tableNameResult = FileUtils_default.extracTableNameFromCube(filePath); const tableName = tableNameResult.status === 200 ? tableNameResult.message : path3.basename(file, `.${cubeType}.cube`); fileMap.set(tableName, file); } for (const tableName of orderList) { if (fileMap.has(tableName)) { orderedFiles.push(fileMap.get(tableName)); fileMap.delete(tableName); } } for (const [, file] of fileMap) { orderedFiles.push(file); } return orderedFiles; } }; // src/lib/Schema.ts import { createRequire } from "module"; var Schema = class { name; engine; constructor(name) { this.name = name; this.engine = new Engine(name); } /** * Validates cube file comprehensively including syntax, database configuration, and structure * @param filePath - Path to the cube file * @returns validation result with any errors found */ validateDatabaseConfiguration(filePath) { try { const cubeValidator = new CubeValidator(); const cubeValidation = cubeValidator.validateCubeFile(filePath); if (!cubeValidation.isValid && cubeValidation.errors.length > 0) { return { isValid: false, error: cubeValidation.errors[0] // Return the first error found }; } const dbResult = FileUtils_default.extractDatabaseNameFromCube(filePath); if (dbResult.status !== 200) { return { isValid: false, error: { itemName: path4.basename(filePath, path4.extname(filePath)), error: `Error reading database directive: ${dbResult.message}`, filePath, lineNumber: this.findDatabaseLineNumber(filePath) } }; } const cubeDbName = dbResult.message; const configInstance = new ConfigClass(); const configFilePath = path4.resolve(process.cwd(), "dbcube.config.js"); const requireUrl = typeof __filename !== "undefined" ? __filename : process.cwd(); const require2 = createRequire(requireUrl); delete require2.cache[require2.resolve(configFilePath)]; const configModule = require2(configFilePath); const configFn = configModule.default || configModule; if (typeof configFn === "function") { configFn(configInstance); } else { throw new Error("\u274C The dbcube.config.js file does not export a function."); } const dbConfig = configInstance.getDatabase(cubeDbName); if (!dbConfig) { let availableDbs = []; try { const testNames = ["test", "development", "production", "local", "main"]; for (const testName of testNames) { try { const testConfig = configInstance.getDatabase(testName); if (testConfig) { availableDbs.push(testName); } } catch (e) { } } } catch (e) { } const availableText = availableDbs.length > 0 ? availableDbs.join(", ") : "none found"; return { isValid: false, error: { itemName: path4.basename(filePath, path4.extname(filePath)), error: `Database configuration '${cubeDbName}' not found in dbcube.config.js. Available: ${availableText}`, filePath, lineNumber: this.findDatabaseLineNumber(filePath) } }; } return { isValid: true }; } catch (error) { return { isValid: false, error: { itemName: path4.basename(filePath, path4.extname(filePath)), error: `Database configuration validation failed: ${error.message}`, filePath, lineNumber: this.findDatabaseLineNumber(filePath) } }; } } /** * Finds the line number where @database directive is located */ findDatabaseLineNumber(filePath) { try { const content = fs5.readFileSync(filePath, "utf8"); const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { if (lines[i].includes("@database")) { return i + 1; } } return 1; } catch { return 1; } } /** * Extracts foreign key dependencies from a cube file */ extractForeignKeyDependencies(filePath) { const dependencies = []; try { const content = fs5.readFileSync(filePath, "utf8"); const lines = content.split("\n"); let insideForeignKey = false; let braceCount = 0; for (const line of lines) { if (/foreign\s*:\s*\{/.test(line)) { insideForeignKey = true; braceCount = 1; const sameLineMatch = line.match(/table\s*:\s*["']([^"']+)["']/); if (sameLineMatch) { dependencies.push(sameLineMatch[1]); insideForeignKey = false; braceCount = 0; } continue; } if (insideForeignKey) { braceCount += (line.match(/\{/g) || []).length; braceCount -= (line.match(/\}/g) || []).length; const tableMatch = line.match(/table\s*:\s*["']([^"']+)["']/); if (tableMatch) { dependencies.push(tableMatch[1]); } if (braceCount === 0) { insideForeignKey = false; } } } } catch (error) { console.error(`Error reading dependencies from ${filePath}:`, error); } return dependencies; } /** * Finds the line number where a foreign key table reference is located */ findForeignKeyLineNumber(filePath, tableName) { try { const content = fs5.readFileSync(filePath, "utf8"); const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { if (lines[i].includes(`table: "${tableName}"`) || lines[i].includes(`table: '${tableName}'`)) { return i + 1; } } return 1; } catch { return 1; } } async createDatabase() { const startTime = Date.now(); const rootPath = path4.resolve(process.cwd()); UIUtils.showOperationHeader(" CREATING DATABASE", this.name, "\u{1F5C4}\uFE0F"); await UIUtils.showItemProgress("Preparando e instalando base de datos", 1, 1); try { const response = await this.engine.run("schema_engine", [ "--action", "create_database", "--path", rootPath ]); if (response.status != 200) { returnFormattedError(response.status, response.message); } UIUtils.showItemSuccess("Database"); const summary = { startTime, totalProcessed: 1, successCount: 1, errorCount: 0, processedItems: [this.name], operationName: "create database", databaseName: this.name, errors: [] }; UIUtils.showOperationSummary(summary); return response.data; } catch (error) { UIUtils.showItemError("Database", error.message); const summary = { startTime, totalProcessed: 0, successCount: 0, errorCount: 1, processedItems: [], operationName: "create database", databaseName: this.name, errors: [] }; UIUtils.showOperationSummary(summary); throw error; } } async refreshTables() { const startTime = Date.now(); const cubesDir = path4.join(process.cwd(), "dbcube"); if (!fs5.existsSync(cubesDir)) { throw new Error("\u274C The cubes folder does not exist"); } const cubeFiles = FileUtils_default.getCubeFilesRecursively("dbcube", ".table.cube"); if (cubeFiles.length === 0) { throw new Error("\u274C There are no cubes to execute"); } DependencyResolver.resolveDependencies(cubeFiles, "table"); const orderedCubeFiles = DependencyResolver.orderCubeFiles(cubeFiles, "table"); UIUtils.showOperationHeader("EXECUTING REFRESH TABLES", this.name, "\u{1F504}"); let totalTablesProcessed = 0; let successCount = 0; let errorCount = 0; const processedTables = []; const errors = []; const failedTables = /* @__PURE__ */ new Set(); for (let index = 0; index < orderedCubeFiles.length; index++) { const file = orderedCubeFiles[index]; const filePath = path4.isAbsolute(file) ? file : path4.join(cubesDir, file); const stats = fs5.statSync(filePath); if (stats.isFile()) { const getTableName = FileUtils_default.extracTableNameFromCube(filePath); const tableName = getTableName.status === 200 ? getTableName.message : path4.basename(file, ".table.cube"); await UIUtils.showItemProgress(tableName, index + 1, orderedCubeFiles.length); try { const validation = this.validateDatabaseConfiguration(filePath); if (!validation.isValid && validation.error) { UIUtils.showItemError(tableName, validation.error.error); errors.push(validation.error); failedTables.add(tableName); errorCount++; continue; } const dependencies = this.extractForeignKeyDependencies(filePath); const missingDependencies = dependencies.filter((dep) => failedTables.has(dep)); if (missingDependencies.length > 0) { const dependencyError = { itemName: tableName, error: `Cannot refresh table '${tableName}' because it depends on failed table(s): ${missingDependencies.join(", ")}`, filePath, lineNumber: this.findForeignKeyLineNumber(filePath, missingDependencies[0]) }; UIUtils.showItemError(tableName, dependencyError.error); errors.push(dependencyError); failedTables.add(tableName); errorCount++; continue; } const dml = await this.engine.run("schema_engine", [ "--action", "parse_table", "--mode", "refresh", "--schema-path", filePath ]); if (dml.status != 200) { returnFormattedError(dml.status, dml.message); break; } const parseJson = JSON.stringify(dml.data.actions).replace(/[\r\n\t]/g, "").replace(/\\[rnt]/g, "").replace(/\s{2,}/g, " "); const queries = await this.engine.run("schema_engine", [ "--action", "generate", "--mode", "refresh", "--dml", parseJson ]); if (queries.status != 200) { returnFormattedError(queries.status, queries.message); break; } delete queries.data.database_type; const parseJsonQueries = JSON.stringify(queries.data); const response = await this.engine.run("schema_engine", [ "--action", "execute", "--mode", "refresh", "--dml", parseJsonQueries ]); if (response.status != 200) { returnFormattedError(response.status, response.message); break; } const createQuery = queries.data.regular_queries.filter((q) => q.includes("CREATE"))[0]; await TableProcessor.saveQuery(dml.data.table, dml.data.database, createQuery); UIUtils.showItemSuccess(tableName); successCount++; processedTables.push(tableName); totalTablesProcessed++; } catch (error) { const processError = { itemName: tableName, error: error.message, filePath }; UIUtils.showItemError(tableName, error.message); errors.push(processError); failedTables.add(tableName); errorCount++; } } } const summary = { startTime, totalProcessed: totalTablesProcessed, successCount, errorCount, processedItems: processedTables, operationName: "refresh tables", databaseName: this.name, errors }; UIUtils.showOperationSummary(summary); return totalTablesProcessed > 0 ? { processed: totalTablesProcessed, success: successCount, errors: errorCount } : null; } async freshTables() { const startTime = Date.now(); const cubesDir = path4.join(process.cwd(), "dbcube"); if (!fs5.existsSync(cubesDir)) { throw new Error("\u274C The cubes folder does not exist"); } const cubeFiles = FileUtils_default.getCubeFilesRecursively("dbcube", ".table.cube"); if (cubeFiles.length === 0) { throw new Error("\u274C There are no cubes to execute"); } DependencyResolver.resolveDependencies(cubeFiles, "table"); const orderedCubeFiles = DependencyResolver.orderCubeFiles(cubeFiles, "table"); UIUtils.showOperationHeader("EXECUTING FRESH TABLES", this.name); let totalTablesProcessed = 0; let successCount = 0; let errorCount = 0; const processedTables = []; const errors = []; const failedTables = /* @__PURE__ */ new Set(); for (let index = 0; index < orderedCubeFiles.length; index++) { const file = orderedCubeFiles[index]; const filePath = path4.isAbsolute(file) ? file : path4.join(cubesDir, file); const stats = fs5.statSync(filePath); if (stats.isFile()) { const getTableName = FileUtils_default.extracTableNameFromCube(filePath); const tableName = getTableName.status === 200 ? getTableName.message : path4.basename(file, ".table.cube"); await UIUtils.showItemProgress(tableName, index + 1, orderedCubeFiles.length); try { const validation = this.validateDatabaseConfiguration(filePath); if (!validation.isValid && validation.error) { UIUtils.showItemError(tableName, validation.error.error); errors.push(validation.error); failedTables.add(tableName); errorCount++; continue; } const dependencies = this.extractForeignKeyDependencies(filePath); const missingDependencies = dependencies.filter((dep) => failedTables.has(dep)); if (missingDependencies.length > 0) { const dependencyError = { itemName: tableName, error: `Cannot create table '${tableName}' because it depends on failed table(s): ${missingDependencies.join(", ")}`, filePath, lineNumber: this.findForeignKeyLineNumber(filePath, missingDependencies[0]) }; UIUtils.showItemError(tableName, dependencyError.error); errors.push(dependencyError); failedTables.add(tableName); errorCount++; continue; } const dml = await this.engine.run("schema_engine", [ "--action", "parse_table", "--schema-path", filePath, "--mode", "fresh" ]); if (dml.status != 200) { returnFormattedError(dml.status, dml.message); break; } const parseJson = JSON.stringify(dml.data.actions).replace(/[\r\n\t]/g, "").replace(/\\[rnt]/g, "").replace(/\s{2,}/g, " "); const queries = await this.engine.run("schema_engine", [ "--action", "generate", "--dml", parseJson ]); if (queries.status != 200) { returnFormattedError(queries.status, queries.message); break; } delete queries.data._type; const createQuery = queries.data.regular_queries.filter((q) => q.includes("CREATE"))[0]; const parseJsonQueries = JSON.stringify(queries.data); const response = await this.engine.run("schema_engine", [ "--action", "execute", "--mode", "fresh", "--dml", parseJsonQueries ]); if (response.status != 200) { returnFormattedError(response.status, response.message); break; } await TableProcessor.saveQuery(dml.data.table, dml.data.database, createQuery); UIUtils.showItemSuccess(tableName); successCount++; processedTables.push(tableName); totalTablesProcessed++; } catch (error) { const processError = { itemName: tableName, error: error.message, filePath }; UIUtils.showItemError(tableName, error.message); errors.push(processError); failedTables.add(tableName); errorCount++; } } } const summary = { startTime, totalProcessed: totalTablesProcessed, successCount, errorCount, processedItems: processedTables, operationName: "fresh tables", databaseName: this.name, errors }; UIUtils.showOperationSummary(summary); return totalTablesProcessed > 0 ? { processed: totalTablesProcessed, success: successCount, errors: errorCount } : null; } async executeSeeders() { const startTime = Date.now(); const cubesDir = path4.join(process.cwd(), "dbcube"); if (!fs5.existsSync(cubesDir)) { throw new Error("\u274C The cubes folder does not exist"); } const cubeFiles = FileUtils_default.getCubeFilesRecursively("dbcube", ".seeder.cube"); if (cubeFiles.length === 0) { throw new Error("\u274C There are no cubes to execute"); } const orderedCubeFiles = DependencyResolver.orderCubeFiles(cubeFiles, "seeder"); UIUtils.showOperationHeader("EXECUTING SEEDERS", this.name, "\u{1F331}"); let totalSeedersProcessed = 0; let successCount = 0; let errorCount = 0; const processedSeeders = []; const errors = []; for (let index = 0; index < orderedCubeFiles.length; index++) { const file = orderedCubeFiles[index]; const filePath = path4.isAbsolute(file) ? file : path4.join(cubesDir, file); const stats = fs5.statSync(filePath); if (stats.isFile()) { const getSeederName = FileUtils_default.extracTableNameFromCube(filePath); const seederName = getSeederName.status === 200 ? getSeederName.message : path4.basename(file, ".seeder.cube"); await UIUtils.showItemProgress(seederName, index + 1, orderedCubeFiles.length); try { const validation = this.validateDatabaseConfiguration(filePath); if (!validation.isValid && validation.error) { UIUtils.showItemError(seederName, validation.error.error); errors.push(validation.error); errorCount++; continue; } const response = await this.engine.run("schema_engine", [ "--action", "seeder", "--schema-path", filePath ]); if (response.status != 200) { returnFormattedError(response.status, response.message); break; } UIUtils.showItemSuccess(seederName); successCount++; processedSeeders.push(seederName); totalSeedersProcessed++; } catch (error) { const processError = { itemName: seederName, error: error.message, filePath }; UIUtils.showItemError(seederName, error.message); errors.push(processError); errorCount++; } } } const summary = { startTime, totalProcessed: totalSeedersProcessed, successCount, errorCount, processedItems: processedSeeders, operationName: "seeders", databaseName: this.name, errors }; UIUtils.showOperationSummary(summary); return totalSeedersProcessed > 0 ? { processed: totalSeedersProcessed, success: successCount, errors: errorCount } : null; } async executeTriggers() { const startTime = Date.now(); const cubesDir = path4.join(process.cwd(), "dbcube"); const triggersDirExit = path4.join(process.cwd(), "dbcube", "triggers"); if (!fs5.existsSync(cubesDir)) { throw new Error("\u274C The cubes folder does not exist"); } const cubeFiles = FileUtils_default.getCubeFilesRecursively("dbcube", ".trigger.cube"); if (cubeFiles.length === 0) { throw new Error("\u274C There are no cubes to execute"); } UIUtils.showOperationHeader("EXECUTING TRIGGERS", this.name, "\u26A1"); let totalTriggersProcessed = 0; let successCount = 0; let errorCount = 0; const processedTriggers = []; const errors = []; for (let index = 0; index < cubeFiles.length; index++) { const file = cubeFiles[index]; const filePath = path4.isAbsolute(file) ? file : path4.join(cubesDir, file); const stats = fs5.statSync(filePath); if (stats.isFile()) { const getTriggerName = FileUtils_default.extracTableNameFromCube(filePath); const triggerName = getTriggerName.status === 200 ? getTriggerName.message : path4.basename(file, ".trigger.cube"); await UIUtils.showItemProgress(triggerName, index + 1, cubeFiles.length); try { const validation = this.validateDatabaseConfiguration(filePath); if (!validation.isValid && validation.error) { UIUtils.showItemError(triggerName, validation.error.error); errors.push(validation.error); errorCount++; continue; } const response = await this.engine.run("schema_engine", [ "--action", "trigger", "--path-exit", triggersDirExit, "--schema-path", filePath ]); if (response.status != 200) { returnFormattedError(response.status, response.message); break; } UIUtils.showItemSuccess(triggerName); successCount++; processedTriggers.push(triggerName); totalTriggersProcessed++; } catch (error) { const processError = { itemName: triggerName, error: error.message, filePath }; UIUtils.showItemError(triggerName, error.message); errors.push(processError); errorCount++; } } } const summary = { startTime, totalProcessed: totalTriggersProcessed, successCount, errorCount, processedItems: processedTriggers, operationName: "triggers", databaseName: this.name, errors }; UIUtils.showOperationSummary(summary); return totalTriggersProcessed > 0 ? { processed: totalTriggersProcessed, success: successCount, errors: errorCount } : null; } }; function returnFormattedError(status, message) { console.l