UNPKG

@dbcube/core

Version:
1 lines 169 kB
{"version":3,"sources":["../src/index.ts","../src/lib/Engine.ts","../src/lib/Arquitecture.ts","../src/lib/Donwloader.ts","../src/lib/Binary.ts","../src/lib/Config.ts","../src/lib/QueryEngine.ts","../src/lib/SqliteExecutor.ts","../src/lib/DbConfig.ts","../src/lib/FileLogger.ts","../src/lib/Processors.ts"],"sourcesContent":["import { Engine } from './lib/Engine';\r\nimport { QueryEngine } from './lib/QueryEngine';\r\nimport { Arquitecture } from './lib/Arquitecture';\r\nimport { Binary } from './lib/Binary';\r\nimport { DbConfig } from './lib/DbConfig';\r\nimport { Config } from './lib/Config';\r\nimport { FileLogger, InterceptController } from './lib/FileLogger';\r\nimport { ComputedFieldProcessor, TableProcessor, TriggerProcessor } from './lib/Processors';\r\n\r\nexport { Config, Engine, QueryEngine, Arquitecture, Binary, DbConfig, ComputedFieldProcessor, TableProcessor, TriggerProcessor, FileLogger, InterceptController };","import path from \"path\";\r\nimport { Binary } from \"./Binary\";\r\nimport { Config as ConfigClass } from './Config';\r\nimport { spawn } from \"child_process\";\r\nimport { ProcessResponse, ResponseEngine } from \"../@types/Engine\";\r\nimport { BinaryType } from \"../@types/Binary\";\r\nimport { createRequire } from 'module';\r\n\r\nclass Engine {\r\n private name: string;\r\n private config: any;\r\n private arguments: any;\r\n private binary: BinaryType | null = null;\r\n private timeout: number;\r\n\r\n constructor(name: string, timeout = 30000) {\r\n this.name = name;\r\n this.config = this.setConfig(name);\r\n this.arguments = this.setArguments();\r\n this.timeout = timeout;\r\n }\r\n\r\n async initializeBinary(): Promise<void> {\r\n if (!this.binary) {\r\n this.binary = await Binary.get();\r\n }\r\n }\r\n\r\n setArguments() {\r\n let args = [];\r\n if (this.config.type == 'sqlite') {\r\n args = [\r\n '--id', 'dbcube-' + this.name,\r\n '--database-ref', this.name,\r\n '--database', this.config.config.DATABASE + \".db\",\r\n '--motor', this.config.type\r\n ];\r\n } else {\r\n args = [\r\n '--id', 'dbcube-' + this.name,\r\n '--database-ref', this.name,\r\n '--database', this.config.config.DATABASE,\r\n '--host', this.config.config.HOST,\r\n '--port', this.config.config.PORT,\r\n '--user', this.config.config.USER,\r\n '--password', this.config.config.PASSWORD,\r\n '--motor', this.config.type\r\n ];\r\n }\r\n return args;\r\n }\r\n\r\n setConfig(name: string) {\r\n const configInstance = new ConfigClass();\r\n\r\n try {\r\n const configFilePath = path.resolve(process.cwd(), 'dbcube.config.js');\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 console.error('❌ El archivo dbcube.config.js no exporta una función.');\r\n }\r\n } catch (error: any) {\r\n console.error('❌ Error loading config file:', error.message);\r\n if (error.code === 'MODULE_NOT_FOUND') {\r\n console.error('❌ Config file not found, please create a dbcube.config.js file');\r\n }\r\n }\r\n\r\n return configInstance.getDatabase(name);\r\n\r\n }\r\n\r\n getConfig() {\r\n return this.config;\r\n }\r\n\r\n async run(binary: string, args: []) {\r\n await this.initializeBinary();\r\n\r\n if (!this.binary) {\r\n throw new Error('Binary not initialized');\r\n }\r\n\r\n return new Promise<ResponseEngine>((resolve, reject) => {\r\n const child = spawn(this.binary![binary as keyof BinaryType], [...this.arguments, ...args]);\r\n\r\n let stdoutBuffer = '';\r\n let stderrBuffer = '';\r\n let isResolved = false;\r\n\r\n const timeoutId = setTimeout(() => {\r\n if (!isResolved) {\r\n isResolved = true;\r\n child.kill();\r\n reject(new Error('Process timeout'));\r\n }\r\n }, this.timeout);\r\n\r\n const resolveOnce = (response: ResponseEngine) => {\r\n if (!isResolved) {\r\n isResolved = true;\r\n clearTimeout(timeoutId);\r\n resolve(response);\r\n }\r\n };\r\n\r\n child.stdout.on('data', (data) => {\r\n stdoutBuffer += data.toString();\r\n // console.log(stdoutBuffer)\r\n\r\n // Buscar respuesta JSON completa\r\n const match = stdoutBuffer.match(/PROCESS_RESPONSE:(\\{.*\\})/);\r\n if (match) {\r\n try {\r\n const response: ProcessResponse = JSON.parse(match[1]);\r\n resolveOnce({\r\n status: response.status,\r\n message: response.message,\r\n data: response.data\r\n });\r\n } catch (error) {\r\n resolveOnce({\r\n status: 500,\r\n message: 'Failed to parse response JSON',\r\n data: null\r\n });\r\n }\r\n }\r\n });\r\n\r\n child.stderr.on('data', (data) => {\r\n stderrBuffer += data.toString();\r\n // console.log(stderrBuffer)\r\n\r\n // Buscar respuesta JSON completa\r\n const match = stderrBuffer.match(/PROCESS_RESPONSE:(\\{.*\\})/);\r\n if (match) {\r\n try {\r\n const response: ProcessResponse = JSON.parse(match[1]);\r\n resolveOnce({\r\n status: response.status,\r\n message: response.message,\r\n data: response.data\r\n });\r\n } catch (error) {\r\n resolveOnce({\r\n status: 500,\r\n message: 'Failed to parse response JSON',\r\n data: null\r\n });\r\n }\r\n }\r\n });\r\n\r\n\r\n child.on('close', (code) => {\r\n clearTimeout(timeoutId);\r\n if (!isResolved) {\r\n resolveOnce({\r\n status: code === 0 ? 200 : 500,\r\n message: code === 0 ? 'Process completed' : `Process exited with code ${code}`,\r\n data: null\r\n });\r\n }\r\n });\r\n\r\n child.on('error', (error) => {\r\n clearTimeout(timeoutId);\r\n if (!isResolved) {\r\n resolveOnce({\r\n status: 500,\r\n message: `Process error: ${error.message}`,\r\n data: null\r\n });\r\n }\r\n });\r\n\r\n child.unref();\r\n });\r\n }\r\n}\r\n\r\nexport { Engine }","import * as os from 'os';\r\nimport * as path from 'path';\r\nimport * as fs from 'fs';\r\n\r\ninterface SystemInfo {\r\n platform: string;\r\n arch: string;\r\n release: string;\r\n type: string;\r\n endianness: string;\r\n cpus: number;\r\n}\r\n\r\ninterface BinaryInfo {\r\n name: string;\r\n path: string;\r\n exists: boolean;\r\n executable: boolean;\r\n}\r\n\r\nclass Arquitecture {\r\n private systemInfo: SystemInfo;\r\n\r\n constructor() {\r\n this.systemInfo = this.detectSystemInfo();\r\n }\r\n\r\n /**\r\n * Detecta información completa del sistema\r\n */\r\n private detectSystemInfo(): SystemInfo {\r\n return {\r\n platform: os.platform(),\r\n arch: os.arch(),\r\n release: os.release(),\r\n type: os.type(),\r\n endianness: os.endianness(),\r\n cpus: os.cpus().length\r\n };\r\n }\r\n\r\n /**\r\n * Obtiene la plataforma normalizada\r\n */\r\n getPlatform(): string {\r\n const platform = this.systemInfo.platform;\r\n \r\n switch (platform) {\r\n case 'win32':\r\n return 'windows';\r\n case 'darwin':\r\n return 'macos';\r\n case 'linux':\r\n return 'linux';\r\n case 'freebsd':\r\n return 'freebsd';\r\n case 'openbsd':\r\n return 'openbsd';\r\n case 'sunos':\r\n return 'solaris';\r\n default:\r\n return platform;\r\n }\r\n }\r\n\r\n /**\r\n * Obtiene la arquitectura normalizada\r\n */\r\n getArchitecture(): string {\r\n const arch = this.systemInfo.arch;\r\n \r\n switch (arch) {\r\n case 'x64':\r\n return 'x86_64';\r\n case 'x32':\r\n case 'ia32':\r\n return 'i686';\r\n case 'arm64':\r\n return 'aarch64';\r\n case 'arm':\r\n return 'armv7';\r\n case 'ppc64':\r\n return 'powerpc64';\r\n case 'ppc':\r\n return 'powerpc';\r\n case 's390x':\r\n return 's390x';\r\n case 'mips':\r\n return 'mips';\r\n case 'mipsel':\r\n return 'mipsel';\r\n default:\r\n return arch;\r\n }\r\n }\r\n\r\n /**\r\n * Genera el nombre del binario basado en la arquitectura\r\n */\r\n getBinaryName(baseName: string): string {\r\n const platform = this.getPlatform();\r\n const arch = this.getArchitecture();\r\n const extension = platform === 'windows' ? '.exe' : '';\r\n \r\n return `${baseName}-${platform}-${arch}${extension}`;\r\n }\r\n\r\n /**\r\n * Obtiene información completa del sistema\r\n */\r\n getSystemInfo(): SystemInfo {\r\n return { ...this.systemInfo };\r\n }\r\n\r\n /**\r\n * Obtiene el triple de destino (target triple) para Rust\r\n */\r\n getRustTargetTriple(): string {\r\n const platform = this.getPlatform();\r\n const arch = this.getArchitecture();\r\n \r\n // Mapeo de plataformas y arquitecturas a target triples de Rust\r\n const targetMap: { [key: string]: string } = {\r\n 'linux-x86_64': 'x86_64-unknown-linux-gnu',\r\n 'linux-i686': 'i686-unknown-linux-gnu',\r\n 'linux-aarch64': 'aarch64-unknown-linux-gnu',\r\n 'linux-armv7': 'armv7-unknown-linux-gnueabihf',\r\n 'macos-x86_64': 'x86_64-apple-darwin',\r\n 'macos-aarch64': 'aarch64-apple-darwin',\r\n 'windows-x86_64': 'x86_64-pc-windows-msvc',\r\n 'windows-i686': 'i686-pc-windows-msvc',\r\n 'windows-aarch64': 'aarch64-pc-windows-msvc',\r\n 'freebsd-x86_64': 'x86_64-unknown-freebsd',\r\n };\r\n \r\n const key = `${platform}-${arch}`;\r\n return targetMap[key] || `${arch}-unknown-${platform}`;\r\n }\r\n\r\n /**\r\n * Muestra información detallada del sistema\r\n */\r\n printSystemInfo(): void {\r\n console.log('🖥️ System Information:');\r\n console.log('├─ Platform:', this.getPlatform());\r\n console.log('├─ Arquitecture:', this.getArchitecture());\r\n console.log('├─ OS Type:', this.systemInfo.type);\r\n console.log('├─ OS Release:', this.systemInfo.release);\r\n console.log('├─ Endianness:', this.systemInfo.endianness);\r\n console.log('├─ CPUs:', this.systemInfo.cpus);\r\n console.log('└─ Rust Target:', this.getRustTargetTriple());\r\n }\r\n}\r\n\r\nexport { \r\n Arquitecture, \r\n SystemInfo,\r\n BinaryInfo\r\n};","import { BinaryType } from \"../@types/Binary\";\r\nimport { Arquitecture } from \"./Arquitecture\";\r\nimport * as fs from \"fs\";\r\nimport * as path from \"path\";\r\nimport * as os from \"os\";\r\nimport followRedirects from 'follow-redirects';\r\nconst { https } = followRedirects;\r\nimport * as unzipper from \"unzipper\";\r\nimport { IncomingMessage } from 'http';\r\nimport ora from \"ora\";\r\nimport chalk from \"chalk\";\r\n\r\ninterface DownloadProgress {\r\n downloaded: number;\r\n total: number;\r\n percentage: number;\r\n}\r\n\r\nclass Downloader {\r\n private static mainSpinner: any = null;\r\n private static currentSpinner: any = null;\r\n\r\n static get(prefix: string): BinaryType & { name: string; url: string } {\r\n const arch = new Arquitecture();\r\n const platform = arch.getPlatform();\r\n const architecture = arch.getArchitecture();\r\n\r\n const platformMap: Record<string, string> = {\r\n windows: \"windows\",\r\n linux: \"linux\",\r\n darwin: \"macos\"\r\n };\r\n\r\n const archMap: Record<string, string> = {\r\n x86_64: \"x64\",\r\n aarch64: \"arm64\"\r\n };\r\n\r\n const plat = platformMap[platform];\r\n const archSuffix = archMap[architecture];\r\n\r\n if (plat && archSuffix) {\r\n const baseName = `${prefix}-engine-${plat}-${archSuffix}`;\r\n const binaryName = platform === \"windows\" ? `${baseName}.exe` : baseName;\r\n\r\n const url = `https://github.com/Dbcube/binaries/releases/download/${prefix}-engine/${prefix}-engine-latest-${plat}-${archSuffix}.zip`;\r\n return {\r\n name: binaryName,\r\n url,\r\n query_engine: binaryName,\r\n schema_engine: `${prefix}-engine-${plat}-${archSuffix}${platform === \"windows\" ? \".exe\" : \"\"}`\r\n };\r\n }\r\n\r\n return {\r\n name: \"\",\r\n url: \"\",\r\n query_engine: \"\",\r\n schema_engine: \"\"\r\n };\r\n }\r\n\r\n static async download(targetDir?: string): Promise<void> {\r\n const binaries = ['schema', 'query', 'sqlite'];\r\n const binDir = targetDir || this.getDefaultBinDir();\r\n fs.mkdirSync(binDir, { recursive: true });\r\n\r\n // Initialize main spinner\r\n this.mainSpinner = ora({\r\n text: chalk.blue('Descargando binarios necesarios...'),\r\n spinner: 'dots12'\r\n }).start();\r\n\r\n // Check which binaries need to be downloaded\r\n const binariesToDownload = [];\r\n let existingCount = 0;\r\n\r\n for (const prefix of binaries) {\r\n const binaryInfo = this.get(prefix);\r\n if (!binaryInfo.name || !binaryInfo.url) {\r\n throw new Error(`Plataforma o arquitectura no soportada para ${prefix}`);\r\n }\r\n\r\n const finalBinaryPath = path.join(binDir, binaryInfo.name);\r\n if (fs.existsSync(finalBinaryPath)) {\r\n existingCount++;\r\n continue;\r\n }\r\n\r\n binariesToDownload.push({\r\n prefix,\r\n binaryInfo,\r\n tempZipPath: path.join(os.tmpdir(), `dbcube-${prefix}-${Date.now()}.zip`),\r\n finalBinaryPath\r\n });\r\n }\r\n\r\n if (binariesToDownload.length === 0) {\r\n this.mainSpinner.succeed(chalk.green('Todos los binarios ya están descargados'));\r\n return;\r\n }\r\n\r\n // Show initial progress\r\n this.updateMainProgress('paralelo', existingCount, binaries.length, 'downloading');\r\n\r\n try {\r\n // Download all binaries in parallel\r\n await Promise.all(binariesToDownload.map(async (binary, index) => {\r\n const maxRetries = 3;\r\n let attempt = 0;\r\n\r\n while (attempt <= maxRetries) {\r\n try {\r\n // Download\r\n await this.downloadFileWithProgress(binary.binaryInfo.url, binary.tempZipPath, binary.prefix);\r\n\r\n // Extract\r\n await this.extractBinary(binary.tempZipPath, binary.finalBinaryPath, binary.prefix);\r\n\r\n // Update progress\r\n const completed = existingCount + index + 1;\r\n this.updateMainProgress(binary.prefix, completed, binaries.length, 'completed');\r\n break;\r\n\r\n } catch (error: unknown) {\r\n const errorMessage = error instanceof Error ? error.message : 'Error desconocido';\r\n\r\n if (attempt < maxRetries && (\r\n errorMessage.includes('ECONNRESET') ||\r\n errorMessage.includes('timeout') ||\r\n errorMessage.includes('ETIMEDOUT') ||\r\n errorMessage.includes('ENOTFOUND')\r\n )) {\r\n attempt++;\r\n this.updateMainProgress(binary.prefix, existingCount, binaries.length, 'retrying', attempt);\r\n await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000)); // Random delay to avoid conflicts\r\n } else {\r\n throw new Error(`Error descargando ${binary.prefix}: ${errorMessage}`);\r\n }\r\n }\r\n }\r\n }));\r\n\r\n // Complete successfully\r\n this.mainSpinner.succeed(chalk.green('Binarios descargados correctamente'));\r\n } catch (error: unknown) {\r\n const errorMessage = error instanceof Error ? error.message : 'Error desconocido';\r\n this.mainSpinner.fail(chalk.red(`Error en descarga paralela: ${errorMessage}`));\r\n throw error;\r\n }\r\n }\r\n\r\n private static updateMainProgress(binary: string, current: number, total: number, status: string, attempt?: number) {\r\n const progressBar = this.createProgressBar(current, total);\r\n const statusEmojis = {\r\n downloading: '📥',\r\n extracting: '📦',\r\n completed: '✅',\r\n exists: '✅',\r\n retrying: '🔄'\r\n };\r\n\r\n const statusMessages = {\r\n downloading: chalk.blue('descargando'),\r\n extracting: chalk.yellow('extrayendo'),\r\n completed: chalk.green('completado'),\r\n exists: chalk.gray('existe'),\r\n retrying: chalk.yellow(`reintentando (${attempt}/${3})`)\r\n };\r\n\r\n const emoji = statusEmojis[status as keyof typeof statusEmojis] || '📥';\r\n const message = statusMessages[status as keyof typeof statusMessages] || status;\r\n\r\n this.mainSpinner.text = `${progressBar} ${emoji} ${chalk.bold(binary)} - ${message}`;\r\n }\r\n\r\n private static createProgressBar(current: number, total: number, width: number = 20): string {\r\n const filled = Math.round((current / total) * width);\r\n const empty = width - filled;\r\n const filledBar = chalk.green('█'.repeat(filled));\r\n const emptyBar = chalk.gray('░'.repeat(empty));\r\n const percentage = chalk.bold(`${current}/${total}`);\r\n return `[${filledBar}${emptyBar}] ${percentage}`;\r\n }\r\n\r\n private static downloadFileWithProgress(url: string, outputPath: string, prefix: string): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n const request = https.get(url, { timeout: 0 }, (response: IncomingMessage) => {\r\n if (response.statusCode === 302 || response.statusCode === 301) {\r\n const redirectUrl = response.headers.location;\r\n if (redirectUrl) {\r\n return this.downloadFileWithProgress(redirectUrl, outputPath, prefix).then(resolve).catch(reject);\r\n }\r\n }\r\n\r\n if (response.statusCode !== 200) {\r\n reject(new Error(`HTTP ${response.statusCode} para ${prefix}`));\r\n return;\r\n }\r\n\r\n const file = fs.createWriteStream(outputPath);\r\n const totalBytes = parseInt(response.headers['content-length'] || '0', 10);\r\n let downloadedBytes = 0;\r\n\r\n response.on('data', (chunk) => {\r\n downloadedBytes += chunk.length;\r\n file.write(chunk);\r\n\r\n if (totalBytes > 0) {\r\n const progress: DownloadProgress = {\r\n downloaded: downloadedBytes,\r\n total: totalBytes,\r\n percentage: (downloadedBytes / totalBytes) * 100\r\n };\r\n this.updateDownloadProgress(prefix, progress);\r\n }\r\n });\r\n\r\n response.on('end', () => {\r\n file.end();\r\n resolve();\r\n });\r\n\r\n response.on('error', (err) => {\r\n file.close();\r\n this.cleanupFile(outputPath);\r\n reject(err);\r\n });\r\n\r\n file.on('error', (err) => {\r\n file.close();\r\n this.cleanupFile(outputPath);\r\n reject(err);\r\n });\r\n });\r\n\r\n request.on('error', reject);\r\n request.on('timeout', () => {\r\n request.destroy();\r\n reject(new Error(`Timeout descargando ${prefix}`));\r\n });\r\n });\r\n }\r\n\r\n private static updateDownloadProgress(binary: string, progress: DownloadProgress) {\r\n const percentage = progress.percentage.toFixed(1);\r\n const downloaded = (progress.downloaded / 1024 / 1024).toFixed(1);\r\n const total = (progress.total / 1024 / 1024).toFixed(1);\r\n\r\n // Create a mini progress bar for the binary\r\n const barWidth = 15;\r\n const filled = Math.round((progress.percentage / 100) * barWidth);\r\n const empty = barWidth - filled;\r\n const progressBar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));\r\n\r\n const progressText = `[${progressBar}] ${percentage}% (${downloaded}/${total}MB)`;\r\n this.mainSpinner.text = `📥 ${chalk.bold(binary)} - ${progressText}`;\r\n }\r\n\r\n private static extractBinary(zipPath: string, outputPath: string, prefix: string): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n let extracted = false;\r\n\r\n fs.createReadStream(zipPath)\r\n .pipe(unzipper.Parse())\r\n .on('entry', (entry: unzipper.Entry) => {\r\n if (entry.type === 'File' && !extracted) {\r\n extracted = true;\r\n const writeStream = fs.createWriteStream(outputPath);\r\n\r\n entry.pipe(writeStream);\r\n\r\n writeStream.on('finish', () => {\r\n if (process.platform !== 'win32') {\r\n fs.chmodSync(outputPath, 0o755);\r\n }\r\n this.cleanupFile(zipPath);\r\n resolve();\r\n });\r\n\r\n writeStream.on('error', (err: Error) => {\r\n this.cleanupFile(zipPath);\r\n reject(err);\r\n });\r\n } else {\r\n entry.autodrain();\r\n }\r\n })\r\n .on('error', (err: Error) => {\r\n this.cleanupFile(zipPath);\r\n reject(err);\r\n })\r\n .on('close', () => {\r\n if (!extracted) {\r\n this.cleanupFile(zipPath);\r\n reject(new Error(`No se encontró archivo válido en el ZIP para ${prefix}`));\r\n }\r\n });\r\n });\r\n }\r\n\r\n private static cleanupFile(filePath: string): void {\r\n try {\r\n if (fs.existsSync(filePath)) {\r\n fs.unlinkSync(filePath);\r\n }\r\n } catch {\r\n // Ignore cleanup errors\r\n }\r\n }\r\n\r\n private static getDefaultBinDir(): string {\r\n // Try to find a suitable directory for binaries in order of preference\r\n const possibleDirs = [\r\n path.resolve(process.cwd(), '.dbcube', 'bin'),\r\n path.resolve(process.cwd(), 'node_modules', '.dbcube', 'bin'),\r\n path.resolve(__dirname, '..', 'bin'),\r\n ];\r\n\r\n // Use the first one that can be created or already exists\r\n for (const dir of possibleDirs) {\r\n try {\r\n if (!fs.existsSync(dir)) {\r\n fs.mkdirSync(dir, { recursive: true });\r\n }\r\n // Test write permissions\r\n const testFile = path.join(dir, '.test');\r\n fs.writeFileSync(testFile, 'test');\r\n fs.unlinkSync(testFile);\r\n return dir;\r\n } catch {\r\n // Try next directory\r\n continue;\r\n }\r\n }\r\n\r\n // Fallback to .dbcube directory in temp\r\n const tempDir = path.join(os.tmpdir(), '.dbcube', 'bin');\r\n fs.mkdirSync(tempDir, { recursive: true });\r\n return tempDir;\r\n }\r\n}\r\n\r\nexport { Downloader };","import { BinaryType } from \"../@types/Binary\";\r\nimport { Arquitecture } from \"./Arquitecture\"\r\nimport { Downloader } from \"./Donwloader\";\r\nimport * as fs from \"fs\";\r\nimport * as path from \"path\";\r\nimport * as os from \"os\";\r\n\r\nclass Binary{\r\n private static isDownloading = false;\r\n private static downloadPromise: Promise<void> | null = null;\r\n\r\n static async ensureBinariesExist(): Promise<void> {\r\n if (this.isDownloading && this.downloadPromise) {\r\n await this.downloadPromise;\r\n return;\r\n }\r\n\r\n const binDir = this.getBinDir();\r\n const binaries = ['schema', 'query', 'sqlite'];\r\n \r\n // Check if all binaries exist\r\n const allExist = binaries.every(prefix => {\r\n const binaryInfo = Downloader.get(prefix);\r\n if (!binaryInfo.name) return false;\r\n const binaryPath = path.join(binDir, binaryInfo.name);\r\n return fs.existsSync(binaryPath);\r\n });\r\n\r\n if (allExist) {\r\n return;\r\n }\r\n\r\n // Download if not exists\r\n if (!this.isDownloading) {\r\n this.isDownloading = true;\r\n this.downloadPromise = this.downloadBinaries();\r\n \r\n try {\r\n await this.downloadPromise;\r\n } finally {\r\n this.isDownloading = false;\r\n this.downloadPromise = null;\r\n }\r\n }\r\n }\r\n\r\n private static async downloadBinaries(): Promise<void> {\r\n try {\r\n const binDir = this.getBinDir();\r\n await Downloader.download(binDir);\r\n } catch (error) {\r\n console.warn('⚠️ DBCube: Error descargando binarios:', (error as Error).message);\r\n console.log('🔧 Los binarios se intentarán descargar en la próxima ejecución');\r\n }\r\n }\r\n\r\n private static getBinDir(): string {\r\n // Try to find a suitable directory for binaries in order of preference\r\n const possibleDirs = [\r\n path.resolve(process.cwd(), '.dbcube', 'bin'),\r\n path.resolve(process.cwd(), 'node_modules', '.dbcube', 'bin'),\r\n path.resolve(__dirname, '..', 'bin'),\r\n ];\r\n\r\n // Use the first one that can be created or already exists\r\n for (const dir of possibleDirs) {\r\n try {\r\n if (!fs.existsSync(dir)) {\r\n fs.mkdirSync(dir, { recursive: true });\r\n }\r\n \r\n \r\n return dir;\r\n } catch {\r\n // Try next directory\r\n continue;\r\n }\r\n }\r\n\r\n // Fallback to .dbcube directory in temp\r\n const tempDir = path.join(os.tmpdir(), '.dbcube', 'bin');\r\n fs.mkdirSync(tempDir, { recursive: true });\r\n return tempDir;\r\n }\r\n\r\n\r\n static async get(): Promise<BinaryType> {\r\n await this.ensureBinariesExist();\r\n \r\n const arch = new Arquitecture();\r\n const platform = arch.getPlatform(); \r\n const architecture = arch.getArchitecture(); \r\n const binDir = this.getBinDir();\r\n \r\n const getFullPath = (binaryName: string) => {\r\n return path.join(binDir, binaryName);\r\n };\r\n \r\n switch(platform){\r\n case \"windows\":\r\n if(architecture==\"x86_64\"){\r\n return {\r\n query_engine: getFullPath(\"query-engine-windows-x64.exe\"),\r\n schema_engine: getFullPath(\"schema-engine-windows-x64.exe\")\r\n };\r\n }\r\n if(architecture==\"aarch64\"){\r\n return {\r\n query_engine: getFullPath(\"query-engine-windows-arm64.exe\"),\r\n schema_engine: getFullPath(\"schema-engine-windows-arm64.exe\")\r\n };\r\n }\r\n break;\r\n case \"linux\":\r\n if(architecture==\"x86_64\"){\r\n return {\r\n query_engine: getFullPath(\"query-engine-linux-x64\"),\r\n schema_engine: getFullPath(\"schema-engine-linux-x64\")\r\n };\r\n }\r\n if(architecture==\"aarch64\"){\r\n return {\r\n query_engine: getFullPath(\"query-engine-linux-arm64\"),\r\n schema_engine: getFullPath(\"schema-engine-linux-arm64\")\r\n };\r\n }\r\n break;\r\n case \"macos\":\r\n if(architecture==\"x86_64\"){\r\n return {\r\n query_engine: getFullPath(\"query-engine-macos-x64\"),\r\n schema_engine: getFullPath(\"schema-engine-macos-x64\")\r\n };\r\n }\r\n if(architecture==\"aarch64\"){\r\n return {\r\n query_engine: getFullPath(\"query-engine-macos-arm64\"),\r\n schema_engine: getFullPath(\"schema-engine-macos-arm64\")\r\n };\r\n }\r\n break;\r\n }\r\n \r\n return {\r\n query_engine: \"\",\r\n schema_engine: \"\"\r\n };\r\n }\r\n}\r\n\r\nexport { Binary }","/**\r\n * Tipo para la configuración de una base de datos\r\n */\r\ntype DatabaseConfig = Record<string, any>;\r\n\r\n/**\r\n * Tipo para la configuración general del ORM\r\n */\r\ninterface ConfigData {\r\n [key: string]: any;\r\n databases?: Record<string, DatabaseConfig>;\r\n}\r\n\r\n/**\r\n * Clase para manejar la configuración del ORM\r\n */\r\nexport class Config {\r\n private data: ConfigData = {};\r\n private databases: Record<string, DatabaseConfig> = {};\r\n\r\n /**\r\n * Establece la configuración\r\n * @param configData - Datos de configuración\r\n */\r\n set(configData: ConfigData): void {\r\n this.data = configData;\r\n\r\n if (configData.databases) {\r\n this.databases = configData.databases;\r\n }\r\n }\r\n\r\n /**\r\n * Obtiene un valor de configuración\r\n * @param key - Clave de configuración\r\n * @returns Valor de configuración\r\n */\r\n get<T = any>(key: string): T {\r\n return this.data[key];\r\n }\r\n\r\n /**\r\n * Obtiene la configuración de una base de datos específica\r\n * @param dbName - Nombre de la base de datos\r\n * @returns Configuración de la base de datos o null\r\n */\r\n getDatabase(dbName: string): DatabaseConfig | null {\r\n return this.databases[dbName] || null;\r\n }\r\n\r\n /**\r\n * Obtiene todas las bases de datos configuradas\r\n * @returns Todas las configuraciones de bases de datos\r\n */\r\n getAllDatabases(): Record<string, DatabaseConfig> {\r\n return this.databases;\r\n }\r\n}\r\n\r\nexport default Config;\r\n","import path from \"path\";\r\nimport { Binary } from \"./Binary\";\r\nimport { Config as ConfigClass } from './Config';\r\nimport { ProcessResponse, ResponseEngine } from \"../@types/Engine\";\r\nimport { BinaryType } from \"../@types/Binary\";\r\nimport { createRequire } from 'module';\r\nimport * as net from 'net';\r\nimport { spawn, ChildProcess } from \"child_process\";\r\n\r\n// Almacén global de servidores TCP y conexiones persistentes\r\nconst globalTcpServers = new Map<string, { port: number, process: ChildProcess | null }>();\r\nconst globalTcpConnections = new Map<string, net.Socket | null>();\r\n\r\n// Query Cache para evitar JSON.parse repetido\r\nconst queryCache = new Map<string, any>();\r\nlet cacheSize = 0;\r\nconst MAX_CACHE_SIZE = 500;\r\n\r\nclass QueryEngine {\r\n private name: string;\r\n private config: any;\r\n private arguments: any;\r\n private binary: BinaryType | null = null;\r\n private timeout: number;\r\n private connectionId: string;\r\n private tcpPort: number;\r\n\r\n constructor(name: string, timeout = 30000) {\r\n this.name = name;\r\n this.config = this.setConfig(name);\r\n this.arguments = this.setArguments();\r\n this.timeout = timeout;\r\n this.connectionId = `${name}_${this.config.type}_${this.config.config.DATABASE}_${this.config.config.HOST || 'localhost'}`;\r\n this.tcpPort = this.generatePort();\r\n }\r\n\r\n private generatePort(): number {\r\n // Empezar desde el puerto 9944 y buscar hacia abajo\r\n return 9944;\r\n }\r\n\r\n private async findAvailablePort(startPort: number): Promise<number> {\r\n for (let port = startPort; port >= 9900; port--) {\r\n if (await this.isPortAvailable(port)) {\r\n return port;\r\n }\r\n }\r\n throw new Error('No available ports found in range 9900-9944');\r\n }\r\n\r\n private isPortAvailable(port: number): Promise<boolean> {\r\n return new Promise((resolve) => {\r\n const tester = net.createServer();\r\n\r\n tester.once('error', () => resolve(false));\r\n tester.once('listening', () => {\r\n tester.close();\r\n resolve(true);\r\n });\r\n\r\n tester.listen(port, '127.0.0.1');\r\n });\r\n }\r\n\r\n async initializeBinary(): Promise<void> {\r\n if (!this.binary) {\r\n this.binary = await Binary.get();\r\n }\r\n }\r\n\r\n setArguments() {\r\n let args = [];\r\n if (this.config.type == 'sqlite') {\r\n args = [\r\n '--id', 'dbcube-' + this.name,\r\n '--database-ref', this.name,\r\n '--database', this.config.config.DATABASE + \".db\",\r\n '--motor', this.config.type\r\n ];\r\n } else {\r\n args = [\r\n '--id', 'dbcube-' + this.name,\r\n '--database-ref', this.name,\r\n '--database', this.config.config.DATABASE,\r\n '--host', this.config.config.HOST,\r\n '--port', this.config.config.PORT,\r\n '--user', this.config.config.USER,\r\n '--password', this.config.config.PASSWORD,\r\n '--motor', this.config.type\r\n ];\r\n }\r\n return args;\r\n }\r\n\r\n setConfig(name: string) {\r\n const configInstance = new ConfigClass();\r\n\r\n try {\r\n const configFilePath = path.resolve(process.cwd(), 'dbcube.config.js');\r\n const requireUrl = typeof __filename !== 'undefined' ? __filename : process.cwd();\r\n const require = createRequire(requireUrl);\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 console.error('❌ El archivo dbcube.config.js no exporta una función.');\r\n }\r\n } catch (error: any) {\r\n console.error('❌ Error loading config file:', error.message);\r\n if (error.code === 'MODULE_NOT_FOUND') {\r\n console.error('❌ Config file not found, please create a dbcube.config.js file');\r\n }\r\n }\r\n\r\n return configInstance.getDatabase(name);\r\n }\r\n\r\n getConfig() {\r\n return this.config;\r\n }\r\n\r\n async run(binary: string, args: string[]): Promise<ResponseEngine> {\r\n // Detectar si es una acción execute\r\n const actionIndex = args.findIndex(arg => arg === '--action');\r\n const isExecuteAction = actionIndex !== -1 && args[actionIndex + 1] === 'execute';\r\n\r\n if (isExecuteAction) {\r\n // Para consultas execute, usar servidor TCP\r\n return this.executeWithTcpServer(args);\r\n } else {\r\n // Para otras acciones (connect, disconnect, list), usar método normal\r\n return this.createProcess(binary, args);\r\n }\r\n }\r\n\r\n private async executeWithTcpServer(args: string[]): Promise<ResponseEngine> {\r\n const serverInfo = globalTcpServers.get(this.connectionId);\r\n\r\n // Si no hay servidor TCP, iniciarlo\r\n if (!serverInfo || !serverInfo.process || serverInfo.process.killed) {\r\n await this.startTcpServer();\r\n // Solo verificar la primera vez\r\n await this.waitForServerReady();\r\n }\r\n\r\n // Enviar consulta via TCP (sin verificaciones adicionales para máxima velocidad)\r\n return this.sendTcpRequestFast(args);\r\n }\r\n\r\n private async waitForServerReady(): Promise<void> {\r\n const maxRetries = 10;\r\n const retryDelay = 500; // 500ms entre intentos\r\n\r\n for (let i = 0; i < maxRetries; i++) {\r\n if (await this.isServerResponding()) {\r\n return;\r\n }\r\n await this.sleep(retryDelay);\r\n }\r\n throw new Error('TCP server failed to become ready within timeout');\r\n }\r\n\r\n private async isServerResponding(): Promise<boolean> {\r\n return new Promise((resolve) => {\r\n const client = new net.Socket();\r\n const timeout = setTimeout(() => {\r\n client.destroy();\r\n resolve(false);\r\n }, 1000);\r\n\r\n client.connect(this.tcpPort, '127.0.0.1', () => {\r\n clearTimeout(timeout);\r\n client.destroy();\r\n resolve(true);\r\n });\r\n\r\n client.on('error', () => {\r\n clearTimeout(timeout);\r\n resolve(false);\r\n });\r\n });\r\n }\r\n\r\n private sleep(ms: number): Promise<void> {\r\n return new Promise(resolve => setTimeout(resolve, ms));\r\n }\r\n\r\n private getCachedDML(dmlJson: string): any {\r\n // Buscar en cache primero\r\n if (queryCache.has(dmlJson)) {\r\n return queryCache.get(dmlJson);\r\n }\r\n\r\n // Parse y guardar en cache si hay espacio\r\n const parsed = JSON.parse(dmlJson);\r\n\r\n if (cacheSize < MAX_CACHE_SIZE) {\r\n queryCache.set(dmlJson, parsed);\r\n cacheSize++;\r\n }\r\n\r\n return parsed;\r\n }\r\n\r\n private async startTcpServer(): Promise<void> {\r\n await this.initializeBinary();\r\n\r\n if (!this.binary) {\r\n throw new Error('Binary not initialized');\r\n }\r\n\r\n // Buscar puerto disponible\r\n this.tcpPort = await this.findAvailablePort(this.tcpPort);\r\n\r\n return new Promise((resolve, reject) => {\r\n // Iniciar el query-engine como servidor TCP\r\n const serverArgs = [...this.arguments, '--action', 'server', '--port', this.tcpPort.toString()];\r\n const serverProcess = spawn(this.binary!['query_engine' as keyof BinaryType], serverArgs);\r\n\r\n let started = false;\r\n\r\n const timeout = setTimeout(() => {\r\n if (!started) {\r\n serverProcess.kill();\r\n reject(new Error('TCP server startup timeout'));\r\n }\r\n }, 15000); // Aumentar timeout a 15 segundos\r\n\r\n serverProcess.stdout.on('data', (data) => {\r\n const output = data.toString();\r\n\r\n if (output.includes('TCP Server listening on')) {\r\n if (!started) {\r\n started = true;\r\n clearTimeout(timeout);\r\n globalTcpServers.set(this.connectionId, {\r\n port: this.tcpPort,\r\n process: serverProcess\r\n });\r\n resolve();\r\n }\r\n }\r\n });\r\n\r\n serverProcess.stderr.on('data', (data) => {\r\n console.error(`[TCP Server Error] ${data.toString().trim()}`);\r\n });\r\n\r\n serverProcess.on('close', (code) => {\r\n globalTcpServers.delete(this.connectionId);\r\n });\r\n\r\n serverProcess.on('error', (error) => {\r\n if (!started) {\r\n clearTimeout(timeout);\r\n reject(error);\r\n }\r\n });\r\n });\r\n }\r\n\r\n private async sendTcpRequest(args: string[]): Promise<ResponseEngine> {\r\n return new Promise((resolve, reject) => {\r\n const client = new net.Socket();\r\n let responseBuffer = '';\r\n let isResolved = false;\r\n\r\n const timeout = setTimeout(() => {\r\n if (!isResolved) {\r\n isResolved = true;\r\n client.destroy();\r\n reject(new Error('TCP request timeout'));\r\n }\r\n }, this.timeout * 2); // Doble timeout para TCP requests\r\n\r\n client.connect(this.tcpPort, '127.0.0.1', () => {\r\n // Extraer el DML de los argumentos\r\n const dmlIndex = args.findIndex(arg => arg === '--dml');\r\n const dmlJson = dmlIndex !== -1 ? args[dmlIndex + 1] : null;\r\n\r\n if (!dmlJson) {\r\n if (!isResolved) {\r\n isResolved = true;\r\n clearTimeout(timeout);\r\n client.destroy();\r\n reject(new Error('No DML found in arguments'));\r\n }\r\n return;\r\n }\r\n\r\n // Enviar comando con el formato correcto que espera el servidor TCP\r\n const command = {\r\n action: 'execute',\r\n dml: dmlJson\r\n };\r\n client.write(JSON.stringify(command));\r\n });\r\n\r\n client.on('data', (data) => {\r\n responseBuffer += data.toString();\r\n\r\n // Buscar respuesta con formato PROCESS_RESPONSE: igual que en Engine\r\n const match = responseBuffer.match(/PROCESS_RESPONSE:(\\{.*\\})/);\r\n if (match && !isResolved) {\r\n isResolved = true;\r\n clearTimeout(timeout);\r\n client.destroy();\r\n\r\n try {\r\n const response: ProcessResponse = JSON.parse(match[1]);\r\n resolve({\r\n status: response.status,\r\n message: response.message,\r\n data: response.data\r\n });\r\n } catch (error) {\r\n reject(new Error('Failed to parse TCP response'));\r\n }\r\n }\r\n });\r\n\r\n client.on('error', (error) => {\r\n if (!isResolved) {\r\n isResolved = true;\r\n clearTimeout(timeout);\r\n reject(error);\r\n }\r\n });\r\n\r\n client.on('close', () => {\r\n if (!isResolved) {\r\n clearTimeout(timeout);\r\n // Solo rechazar si no hay una respuesta válida\r\n if (!responseBuffer.includes('PROCESS_RESPONSE:')) {\r\n reject(new Error('Connection closed without valid response'));\r\n }\r\n }\r\n });\r\n });\r\n }\r\n\r\n private async sendTcpRequestFast(args: string[]): Promise<ResponseEngine> {\r\n // Intentar usar conexión persistente primero\r\n const existingConnection = globalTcpConnections.get(this.connectionId);\r\n\r\n if (existingConnection && existingConnection.readyState === 'open') {\r\n try {\r\n return await this.sendOnExistingConnection(existingConnection, args);\r\n } catch (error) {\r\n // Si falla, remover y crear nueva conexión\r\n globalTcpConnections.delete(this.connectionId);\r\n existingConnection.destroy();\r\n }\r\n }\r\n\r\n // Crear nueva conexión persistente\r\n return await this.createPersistentConnection(args);\r\n }\r\n\r\n private async sendOnExistingConnection(connection: net.Socket, args: string[]): Promise<ResponseEngine> {\r\n return new Promise((resolve, reject) => {\r\n const dmlIndex = args.findIndex(arg => arg === '--dml');\r\n const dmlJson = dmlIndex !== -1 ? args[dmlIndex + 1] : null;\r\n\r\n if (!dmlJson) {\r\n reject(new Error('No DML found in arguments'));\r\n return;\r\n }\r\n\r\n let responseBuffer = '';\r\n let isResolved = false;\r\n\r\n const timeout = setTimeout(() => {\r\n if (!isResolved) {\r\n isResolved = true;\r\n reject(new Error('TCP request timeout'));\r\n }\r\n }, 100); // Timeout ultra-corto para detectar conexiones muertas\r\n\r\n const onData = (data: Buffer) => {\r\n responseBuffer += data.toString();\r\n const match = responseBuffer.match(/PROCESS_RESPONSE:(\\{.*\\})/);\r\n if (match && !isResolved) {\r\n isResolved = true;\r\n clearTimeout(timeout);\r\n connection.off('data', onData);\r\n connection.off('error', onError);\r\n\r\n try {\r\n const response: ProcessResponse = JSON.parse(match[1]);\r\n resolve({\r\n status: response.status,\r\n message: response.message,\r\n data: response.data\r\n });\r\n } catch (error) {\r\n reject(new Error('Failed to parse TCP response'));\r\n }\r\n }\r\n