@dbcube/core
Version:
1,579 lines (1,572 loc) • 86.1 kB
JavaScript
// src/lib/Engine.ts
import path3 from "path";
// src/lib/Arquitecture.ts
import * as os from "os";
var Arquitecture = class {
systemInfo;
constructor() {
this.systemInfo = this.detectSystemInfo();
}
/**
* Detecta información completa del sistema
*/
detectSystemInfo() {
return {
platform: os.platform(),
arch: os.arch(),
release: os.release(),
type: os.type(),
endianness: os.endianness(),
cpus: os.cpus().length
};
}
/**
* Obtiene la plataforma normalizada
*/
getPlatform() {
const platform2 = this.systemInfo.platform;
switch (platform2) {
case "win32":
return "windows";
case "darwin":
return "macos";
case "linux":
return "linux";
case "freebsd":
return "freebsd";
case "openbsd":
return "openbsd";
case "sunos":
return "solaris";
default:
return platform2;
}
}
/**
* Obtiene la arquitectura normalizada
*/
getArchitecture() {
const arch2 = this.systemInfo.arch;
switch (arch2) {
case "x64":
return "x86_64";
case "x32":
case "ia32":
return "i686";
case "arm64":
return "aarch64";
case "arm":
return "armv7";
case "ppc64":
return "powerpc64";
case "ppc":
return "powerpc";
case "s390x":
return "s390x";
case "mips":
return "mips";
case "mipsel":
return "mipsel";
default:
return arch2;
}
}
/**
* Genera el nombre del binario basado en la arquitectura
*/
getBinaryName(baseName) {
const platform2 = this.getPlatform();
const arch2 = this.getArchitecture();
const extension = platform2 === "windows" ? ".exe" : "";
return `${baseName}-${platform2}-${arch2}${extension}`;
}
/**
* Obtiene información completa del sistema
*/
getSystemInfo() {
return { ...this.systemInfo };
}
/**
* Obtiene el triple de destino (target triple) para Rust
*/
getRustTargetTriple() {
const platform2 = this.getPlatform();
const arch2 = this.getArchitecture();
const targetMap = {
"linux-x86_64": "x86_64-unknown-linux-gnu",
"linux-i686": "i686-unknown-linux-gnu",
"linux-aarch64": "aarch64-unknown-linux-gnu",
"linux-armv7": "armv7-unknown-linux-gnueabihf",
"macos-x86_64": "x86_64-apple-darwin",
"macos-aarch64": "aarch64-apple-darwin",
"windows-x86_64": "x86_64-pc-windows-msvc",
"windows-i686": "i686-pc-windows-msvc",
"windows-aarch64": "aarch64-pc-windows-msvc",
"freebsd-x86_64": "x86_64-unknown-freebsd"
};
const key = `${platform2}-${arch2}`;
return targetMap[key] || `${arch2}-unknown-${platform2}`;
}
/**
* Muestra información detallada del sistema
*/
printSystemInfo() {
console.log("\u{1F5A5}\uFE0F System Information:");
console.log("\u251C\u2500 Platform:", this.getPlatform());
console.log("\u251C\u2500 Arquitecture:", this.getArchitecture());
console.log("\u251C\u2500 OS Type:", this.systemInfo.type);
console.log("\u251C\u2500 OS Release:", this.systemInfo.release);
console.log("\u251C\u2500 Endianness:", this.systemInfo.endianness);
console.log("\u251C\u2500 CPUs:", this.systemInfo.cpus);
console.log("\u2514\u2500 Rust Target:", this.getRustTargetTriple());
}
};
// src/lib/Donwloader.ts
import * as fs from "fs";
import * as path from "path";
import * as os2 from "os";
import followRedirects from "follow-redirects";
import * as unzipper from "unzipper";
import ora from "ora";
import chalk from "chalk";
import { fileURLToPath } from "url";
import { dirname } from "path";
var { https } = followRedirects;
var Downloader = class {
static mainSpinner = null;
static currentSpinner = null;
static VERSION_URLS = {
query: "https://raw.githubusercontent.com/Dbcube/binaries/main/query-engines.json",
schema: "https://raw.githubusercontent.com/Dbcube/binaries/main/schema-engines.json",
sqlite: "https://raw.githubusercontent.com/Dbcube/binaries/main/sqlite-engines.json"
};
/**
* Fetch latest version from GitHub
*/
static async fetchLatestVersion(prefix) {
const url = this.VERSION_URLS[prefix];
return new Promise((resolve5, reject) => {
https.get(url, (response) => {
let data = "";
response.on("data", (chunk) => {
data += chunk;
});
response.on("end", () => {
try {
const versions = JSON.parse(data);
if (versions && versions.length > 0) {
resolve5(versions[0].version);
} else {
reject(new Error("No versions found"));
}
} catch (error) {
reject(error);
}
});
response.on("error", reject);
}).on("error", reject);
});
}
/**
* Extract version from binary filename
* Example: schema-engine-v1.0.0-windows-x64.exe -> v1.0.0
*/
static extractVersionFromFilename(filename) {
const match = filename.match(/-(v\d+\.\d+\.\d+)-/);
return match ? match[1] : null;
}
/**
* Get local version of installed binary
*/
static getLocalVersion(binDir, prefix) {
try {
const files = fs.readdirSync(binDir);
const binaryPattern = new RegExp(`^${prefix}-engine-v`);
const binaryFile = files.find((f) => binaryPattern.test(f));
if (binaryFile) {
return this.extractVersionFromFilename(binaryFile);
}
} catch (error) {
}
return null;
}
/**
* Compare versions (returns true if remote is newer)
*/
static isNewerVersion(localVersion, remoteVersion) {
if (!localVersion) return true;
const cleanLocal = localVersion.replace(/^v/, "");
const cleanRemote = remoteVersion.replace(/^v/, "");
const localParts = cleanLocal.split(".").map(Number);
const remoteParts = cleanRemote.split(".").map(Number);
for (let i = 0; i < 3; i++) {
if (remoteParts[i] > localParts[i]) return true;
if (remoteParts[i] < localParts[i]) return false;
}
return false;
}
/**
* Delete old binary files
*/
static deleteOldBinary(binDir, prefix) {
try {
const files = fs.readdirSync(binDir);
const binaryPattern = new RegExp(`^${prefix}-engine-`);
files.forEach((file) => {
if (binaryPattern.test(file)) {
const filePath = path.join(binDir, file);
try {
fs.unlinkSync(filePath);
console.log(`\u{1F5D1}\uFE0F Deleted old binary: ${file}`);
} catch (err) {
console.warn(`\u26A0\uFE0F Could not delete: ${file}`);
}
}
});
} catch (error) {
}
}
static get(prefix) {
const arch2 = new Arquitecture();
const platform2 = arch2.getPlatform();
const architecture = arch2.getArchitecture();
const platformMap = {
windows: "windows",
linux: "linux",
darwin: "macos"
};
const archMap = {
x86_64: "x64",
aarch64: "arm64"
};
const plat = platformMap[platform2];
const archSuffix = archMap[architecture];
if (plat && archSuffix) {
const baseName = `${prefix}-engine-${plat}-${archSuffix}`;
const binaryName = platform2 === "windows" ? `${baseName}.exe` : baseName;
const url = `https://github.com/Dbcube/binaries/releases/download/${prefix}-engine/${prefix}-engine-latest-${plat}-${archSuffix}.zip`;
return {
name: binaryName,
url,
version: "latest",
query_engine: binaryName,
schema_engine: `${prefix}-engine-${plat}-${archSuffix}${platform2 === "windows" ? ".exe" : ""}`
};
}
return {
name: "",
url: "",
version: "",
query_engine: "",
schema_engine: ""
};
}
/**
* Get binary info with actual version from GitHub
*/
static async getWithVersion(prefix) {
const arch2 = new Arquitecture();
const platform2 = arch2.getPlatform();
const architecture = arch2.getArchitecture();
const platformMap = {
windows: "windows",
linux: "linux",
darwin: "macos"
};
const archMap = {
x86_64: "x64",
aarch64: "arm64"
};
const plat = platformMap[platform2];
const archSuffix = archMap[architecture];
if (plat && archSuffix) {
const version = await this.fetchLatestVersion(prefix);
const baseName = `${prefix}-engine-${version}-${plat}-${archSuffix}`;
const binaryName = platform2 === "windows" ? `${baseName}.exe` : baseName;
const url = `https://github.com/Dbcube/binaries/releases/download/${prefix}-engine/${prefix}-engine-${version}-${plat}-${archSuffix}.zip`;
return {
name: binaryName,
url,
version,
query_engine: binaryName,
schema_engine: binaryName
};
}
return {
name: "",
url: "",
version: "",
query_engine: "",
schema_engine: ""
};
}
static async download(targetDir) {
const binDir = targetDir || this.getDefaultBinDir();
fs.mkdirSync(binDir, { recursive: true });
this.mainSpinner = ora({
text: chalk.blue("Verificando versiones de binarios..."),
spinner: "dots12"
}).start();
const binariesToProcess = [];
for (const prefix of ["query", "schema", "sqlite"]) {
try {
const localVersion = this.getLocalVersion(binDir, prefix);
const remoteVersion = await this.fetchLatestVersion(prefix);
const needsUpdate = this.isNewerVersion(localVersion, remoteVersion);
binariesToProcess.push({
prefix,
needsUpdate,
localVersion,
remoteVersion
});
if (needsUpdate) {
console.log(`
\u{1F4E6} ${prefix}-engine: ${localVersion || "not installed"} \u2192 ${remoteVersion}`);
} else if (localVersion) {
console.log(`
\u2705 ${prefix}-engine: ${localVersion} (up to date)`);
}
} catch (error) {
console.warn(`\u26A0\uFE0F Could not check version for ${prefix}-engine, will attempt download`);
binariesToProcess.push({
prefix,
needsUpdate: true,
localVersion: null,
remoteVersion: "latest"
});
}
}
const binariesToDownload = binariesToProcess.filter((b) => b.needsUpdate);
if (binariesToDownload.length === 0) {
this.mainSpinner.succeed(chalk.green("All binaries are up to date"));
return;
}
this.mainSpinner.text = chalk.blue(`Updating ${binariesToDownload.length} binary(ies)...`);
try {
await Promise.all(binariesToDownload.map(async (binary) => {
const maxRetries = 3;
let attempt = 0;
while (attempt <= maxRetries) {
try {
const binaryInfo = await this.getWithVersion(binary.prefix);
if (!binaryInfo.name || !binaryInfo.url) {
throw new Error(`Platform or architecture not supported for ${binary.prefix}`);
}
const tempZipPath = path.join(os2.tmpdir(), `dbcube-${binary.prefix}-${Date.now()}.zip`);
const finalBinaryPath = path.join(binDir, binaryInfo.name);
this.deleteOldBinary(binDir, binary.prefix);
await this.downloadFileWithProgress(binaryInfo.url, tempZipPath, binary.prefix);
await this.extractBinary(tempZipPath, finalBinaryPath, binary.prefix);
console.log(`\u2705 ${binary.prefix}-engine updated to ${binary.remoteVersion}`);
break;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
if (attempt < maxRetries && (errorMessage.includes("ECONNRESET") || errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT") || errorMessage.includes("ENOTFOUND"))) {
attempt++;
console.log(`\u{1F504} Retrying ${binary.prefix}-engine (${attempt}/${maxRetries})...`);
await new Promise((resolve5) => setTimeout(resolve5, 1e3 + Math.random() * 1e3));
} else {
throw new Error(`Error downloading ${binary.prefix}: ${errorMessage}`);
}
}
}
}));
this.mainSpinner.succeed(chalk.green("Binaries updated successfully"));
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
this.mainSpinner.fail(chalk.red(`Error updating binaries: ${errorMessage}`));
throw error;
}
}
static updateMainProgress(binary, current, total, status, attempt) {
const progressBar = this.createProgressBar(current, total);
const statusEmojis = {
downloading: "\u{1F4E5}",
extracting: "\u{1F4E6}",
completed: "\u2705",
exists: "\u2705",
retrying: "\u{1F504}"
};
const statusMessages = {
downloading: chalk.blue("descargando"),
extracting: chalk.yellow("extrayendo"),
completed: chalk.green("completado"),
exists: chalk.gray("existe"),
retrying: chalk.yellow(`reintentando (${attempt}/${3})`)
};
const emoji = statusEmojis[status] || "\u{1F4E5}";
const message = statusMessages[status] || status;
this.mainSpinner.text = `${progressBar} ${emoji} ${chalk.bold(binary)} - ${message}`;
}
static createProgressBar(current, total, width = 20) {
const filled = Math.round(current / total * width);
const empty = width - filled;
const filledBar = chalk.green("\u2588".repeat(filled));
const emptyBar = chalk.gray("\u2591".repeat(empty));
const percentage = chalk.bold(`${current}/${total}`);
return `[${filledBar}${emptyBar}] ${percentage}`;
}
static downloadFileWithProgress(url, outputPath, prefix) {
return new Promise((resolve5, reject) => {
const request = https.get(url, { timeout: 0 }, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
const redirectUrl = response.headers.location;
if (redirectUrl) {
return this.downloadFileWithProgress(redirectUrl, outputPath, prefix).then(resolve5).catch(reject);
}
}
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode} para ${prefix}`));
return;
}
const file = fs.createWriteStream(outputPath);
const totalBytes = parseInt(response.headers["content-length"] || "0", 10);
let downloadedBytes = 0;
response.on("data", (chunk) => {
downloadedBytes += chunk.length;
file.write(chunk);
if (totalBytes > 0) {
const progress = {
downloaded: downloadedBytes,
total: totalBytes,
percentage: downloadedBytes / totalBytes * 100
};
this.updateDownloadProgress(prefix, progress);
}
});
response.on("end", () => {
file.end();
resolve5();
});
response.on("error", (err) => {
file.close();
this.cleanupFile(outputPath);
reject(err);
});
file.on("error", (err) => {
file.close();
this.cleanupFile(outputPath);
reject(err);
});
});
request.on("error", reject);
request.on("timeout", () => {
request.destroy();
reject(new Error(`Timeout descargando ${prefix}`));
});
});
}
static updateDownloadProgress(binary, progress) {
const percentage = progress.percentage.toFixed(1);
const downloaded = (progress.downloaded / 1024 / 1024).toFixed(1);
const total = (progress.total / 1024 / 1024).toFixed(1);
const barWidth = 15;
const filled = Math.round(progress.percentage / 100 * barWidth);
const empty = barWidth - filled;
const progressBar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty));
const progressText = `[${progressBar}] ${percentage}% (${downloaded}/${total}MB)`;
this.mainSpinner.text = `\u{1F4E5} ${chalk.bold(binary)} - ${progressText}`;
}
static extractBinary(zipPath, outputPath, prefix) {
return new Promise((resolve5, reject) => {
let extracted = false;
fs.createReadStream(zipPath).pipe(unzipper.Parse()).on("entry", (entry) => {
if (entry.type === "File" && !extracted) {
extracted = true;
const writeStream = fs.createWriteStream(outputPath);
entry.pipe(writeStream);
writeStream.on("finish", () => {
if (process.platform !== "win32") {
fs.chmodSync(outputPath, 493);
}
this.cleanupFile(zipPath);
resolve5();
});
writeStream.on("error", (err) => {
this.cleanupFile(zipPath);
reject(err);
});
} else {
entry.autodrain();
}
}).on("error", (err) => {
this.cleanupFile(zipPath);
reject(err);
}).on("close", () => {
if (!extracted) {
this.cleanupFile(zipPath);
reject(new Error(`No se encontr\xF3 archivo v\xE1lido en el ZIP para ${prefix}`));
}
});
});
}
static cleanupFile(filePath) {
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch {
}
}
static getDefaultBinDir() {
const __filename2 = typeof import.meta !== "undefined" && import.meta.url ? fileURLToPath(import.meta.url) : "";
const __dirname = __filename2 ? dirname(__filename2) : process.cwd();
const possibleDirs = [
path.resolve(process.cwd(), ".dbcube", "bin"),
path.resolve(process.cwd(), "node_modules", ".dbcube", "bin"),
path.resolve(__dirname, "..", "bin")
];
for (const dir of possibleDirs) {
try {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const testFile = path.join(dir, ".test");
fs.writeFileSync(testFile, "test");
fs.unlinkSync(testFile);
return dir;
} catch {
continue;
}
}
const tempDir = path.join(os2.tmpdir(), ".dbcube", "bin");
fs.mkdirSync(tempDir, { recursive: true });
return tempDir;
}
};
// src/lib/Binary.ts
import * as fs2 from "fs";
import * as path2 from "path";
import * as os3 from "os";
import { fileURLToPath as fileURLToPath2 } from "url";
import { dirname as dirname2 } from "path";
var Binary = class {
static isDownloading = false;
static downloadPromise = null;
static cachedBinaries = {};
static async ensureBinariesExist() {
if (this.isDownloading && this.downloadPromise) {
await this.downloadPromise;
return;
}
const binDir = this.getBinDir();
if (!this.isDownloading) {
this.isDownloading = true;
this.downloadPromise = this.downloadBinaries();
try {
await this.downloadPromise;
} finally {
this.isDownloading = false;
this.downloadPromise = null;
}
}
}
static async downloadBinaries() {
try {
const binDir = this.getBinDir();
await Downloader.download(binDir);
} catch (error) {
console.warn("\u26A0\uFE0F Dbcube: Error descargando binarios:", error.message);
console.log("\u{1F527} Los binarios se intentar\xE1n descargar en la pr\xF3xima ejecuci\xF3n");
}
}
static getBinDir() {
const __filename2 = typeof import.meta !== "undefined" && import.meta.url ? fileURLToPath2(import.meta.url) : "";
const __dirname = __filename2 ? dirname2(__filename2) : process.cwd();
const possibleDirs = [
path2.resolve(process.cwd(), ".dbcube", "bin"),
path2.resolve(process.cwd(), "node_modules", ".dbcube", "bin"),
path2.resolve(__dirname, "..", "bin")
];
for (const dir of possibleDirs) {
try {
if (!fs2.existsSync(dir)) {
fs2.mkdirSync(dir, { recursive: true });
}
return dir;
} catch {
continue;
}
}
const tempDir = path2.join(os3.tmpdir(), ".dbcube", "bin");
fs2.mkdirSync(tempDir, { recursive: true });
return tempDir;
}
static findVersionedBinary(binDir, prefix, platform2, arch2) {
const cacheKey = `${prefix}-${platform2}-${arch2}`;
if (this.cachedBinaries[cacheKey]) {
const cachedPath = path2.join(binDir, this.cachedBinaries[cacheKey]);
if (fs2.existsSync(cachedPath)) {
return cachedPath;
}
}
try {
const files = fs2.readdirSync(binDir);
const extension = platform2 === "windows" ? ".exe" : "";
const pattern = new RegExp(`^${prefix}-engine-v\\d+\\.\\d+\\.\\d+-${platform2}-${arch2}${extension.replace(".", "\\.")}$`);
const matchingFile = files.find((f) => pattern.test(f));
if (matchingFile) {
this.cachedBinaries[cacheKey] = matchingFile;
return path2.join(binDir, matchingFile);
}
} catch (error) {
}
const fallbackName = `${prefix}-engine-${platform2}-${arch2}${platform2 === "windows" ? ".exe" : ""}`;
return path2.join(binDir, fallbackName);
}
static async get() {
await this.ensureBinariesExist();
const arch2 = new Arquitecture();
const platform2 = arch2.getPlatform();
const architecture = arch2.getArchitecture();
const binDir = this.getBinDir();
const platformMap = {
windows: "windows",
linux: "linux",
darwin: "macos"
};
const archMap = {
x86_64: "x64",
aarch64: "arm64"
};
const plat = platformMap[platform2];
const archSuffix = archMap[architecture];
if (plat && archSuffix) {
return {
query_engine: this.findVersionedBinary(binDir, "query", plat, archSuffix),
schema_engine: this.findVersionedBinary(binDir, "schema", plat, archSuffix)
};
}
return {
query_engine: "",
schema_engine: ""
};
}
};
// src/lib/Config.ts
var Config = class {
data = {};
databases = {};
/**
* Establece la configuración
* @param configData - Datos de configuración
*/
set(configData) {
this.data = configData;
if (configData.databases) {
this.databases = configData.databases;
}
}
/**
* Obtiene un valor de configuración
* @param key - Clave de configuración
* @returns Valor de configuración
*/
get(key) {
return this.data[key];
}
/**
* Obtiene la configuración de una base de datos específica
* @param dbName - Nombre de la base de datos
* @returns Configuración de la base de datos o null
*/
getDatabase(dbName) {
return this.databases[dbName] || null;
}
/**
* Obtiene todas las bases de datos configuradas
* @returns Todas las configuraciones de bases de datos
*/
getAllDatabases() {
return this.databases;
}
};
// src/lib/Engine.ts
import { spawn } from "child_process";
import { createRequire } from "module";
var Engine = class {
name;
config;
arguments;
binary = null;
timeout;
constructor(name, timeout = 3e4) {
this.name = name;
this.config = this.setConfig(name);
this.arguments = this.setArguments();
this.timeout = timeout;
}
async initializeBinary() {
if (!this.binary) {
this.binary = await Binary.get();
}
}
setArguments() {
let args = [];
if (this.config.type == "sqlite") {
args = [
"--id",
"dbcube-" + this.name,
"--database-ref",
this.name,
"--database",
this.config.config.DATABASE + ".db",
"--motor",
this.config.type
];
} else {
args = [
"--id",
"dbcube-" + this.name,
"--database-ref",
this.name,
"--database",
this.config.config.DATABASE,
"--host",
this.config.config.HOST,
"--port",
this.config.config.PORT,
"--user",
this.config.config.USER,
"--password",
this.config.config.PASSWORD,
"--motor",
this.config.type
];
}
return args;
}
setConfig(name) {
const configInstance = new Config();
try {
const configFilePath = path3.resolve(process.cwd(), "dbcube.config.js");
const requireUrl = typeof __filename !== "undefined" ? __filename : process.cwd();
const require2 = createRequire(requireUrl);
if (require2.cache && require2.resolve) {
try {
delete require2.cache[require2.resolve(configFilePath)];
} catch (e) {
}
}
const configModule = require2(configFilePath);
const configFn = configModule.default || configModule;
if (typeof configFn === "function") {
configFn(configInstance);
} else {
console.error("\u274C El archivo dbcube.config.js no exporta una funci\xF3n.");
}
} catch (error) {
console.error("\u274C Error loading config file:", error.message);
if (error.code === "MODULE_NOT_FOUND") {
console.error("\u274C Config file not found, please create a dbcube.config.js file");
}
}
return configInstance.getDatabase(name);
}
getConfig() {
return this.config;
}
async run(binary, args) {
await this.initializeBinary();
if (!this.binary) {
throw new Error("Binary not initialized");
}
return new Promise((resolve5, reject) => {
const child = spawn(this.binary[binary], [...this.arguments, ...args]);
let stdoutBuffer = "";
let stderrBuffer = "";
let isResolved = false;
const timeoutId = setTimeout(() => {
if (!isResolved) {
isResolved = true;
child.kill();
reject(new Error("Process timeout"));
}
}, this.timeout);
const resolveOnce = (response) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeoutId);
resolve5(response);
}
};
child.stdout.on("data", (data) => {
stdoutBuffer += data.toString();
console.log(stdoutBuffer);
const match = stdoutBuffer.match(/PROCESS_RESPONSE:(\{.*\})/);
if (match) {
try {
const response = JSON.parse(match[1]);
resolveOnce({
status: response.status,
message: response.message,
data: response.data
});
} catch (error) {
resolveOnce({
status: 500,
message: "Failed to parse response JSON",
data: null
});
}
}
});
child.stderr.on("data", (data) => {
stderrBuffer += data.toString();
console.log(stderrBuffer);
const match = stderrBuffer.match(/PROCESS_RESPONSE:(\{.*\})/);
if (match) {
try {
const response = JSON.parse(match[1]);
resolveOnce({
status: response.status,
message: response.message,
data: response.data
});
} catch (error) {
resolveOnce({
status: 500,
message: "Failed to parse response JSON",
data: null
});
}
}
});
child.on("close", (code) => {
clearTimeout(timeoutId);
if (!isResolved) {
resolveOnce({
status: code === 0 ? 200 : 500,
message: code === 0 ? "Process completed" : `Process exited with code ${code}`,
data: null
});
}
});
child.on("error", (error) => {
clearTimeout(timeoutId);
if (!isResolved) {
resolveOnce({
status: 500,
message: `Process error: ${error.message}`,
data: null
});
}
});
child.unref();
});
}
};
// src/lib/QueryEngine.ts
import path4 from "path";
import { createRequire as createRequire2 } from "module";
import * as net from "net";
import { spawn as spawn2 } from "child_process";
var globalTcpServers = /* @__PURE__ */ new Map();
var globalTcpConnections = /* @__PURE__ */ new Map();
var queryCache = /* @__PURE__ */ new Map();
var cacheSize = 0;
var MAX_CACHE_SIZE = 500;
var QueryEngine = class {
name;
config;
arguments;
binary = null;
timeout;
connectionId;
tcpPort;
constructor(name, timeout = 3e4) {
this.name = name;
this.config = this.setConfig(name);
this.arguments = this.setArguments();
this.timeout = timeout;
this.connectionId = `${name}_${this.config.type}_${this.config.config.DATABASE}_${this.config.config.HOST || "localhost"}`;
this.tcpPort = this.generatePort();
}
generatePort() {
return 9944;
}
async findAvailablePort(startPort) {
for (let port = startPort; port >= 9900; port--) {
if (await this.isPortAvailable(port)) {
return port;
}
}
throw new Error("No available ports found in range 9900-9944");
}
isPortAvailable(port) {
return new Promise((resolve5) => {
const tester = net.createServer();
tester.once("error", () => resolve5(false));
tester.once("listening", () => {
tester.close();
resolve5(true);
});
tester.listen(port, "127.0.0.1");
});
}
async initializeBinary() {
if (!this.binary) {
this.binary = await Binary.get();
}
}
setArguments() {
let args = [];
if (this.config.type == "sqlite") {
args = [
"--id",
"dbcube-" + this.name,
"--database-ref",
this.name,
"--database",
this.config.config.DATABASE + ".db",
"--motor",
this.config.type
];
} else {
args = [
"--id",
"dbcube-" + this.name,
"--database-ref",
this.name,
"--database",
this.config.config.DATABASE,
"--host",
this.config.config.HOST,
"--port",
this.config.config.PORT,
"--user",
this.config.config.USER,
"--password",
this.config.config.PASSWORD,
"--motor",
this.config.type
];
}
return args;
}
setConfig(name) {
const configInstance = new Config();
try {
const configFilePath = path4.resolve(process.cwd(), "dbcube.config.js");
const requireUrl = typeof __filename !== "undefined" ? __filename : process.cwd();
const require2 = createRequire2(requireUrl);
if (require2.cache && require2.resolve) {
try {
delete require2.cache[require2.resolve(configFilePath)];
} catch (e) {
}
}
const configModule = require2(configFilePath);
const configFn = configModule.default || configModule;
if (typeof configFn === "function") {
configFn(configInstance);
} else {
console.error("\u274C El archivo dbcube.config.js no exporta una funci\xF3n.");
}
} catch (error) {
console.error("\u274C Error loading config file:", error.message);
if (error.code === "MODULE_NOT_FOUND") {
console.error("\u274C Config file not found, please create a dbcube.config.js file");
}
}
return configInstance.getDatabase(name);
}
getConfig() {
return this.config;
}
async run(binary, args) {
const actionIndex = args.findIndex((arg) => arg === "--action");
const isExecuteAction = actionIndex !== -1 && args[actionIndex + 1] === "execute";
if (isExecuteAction) {
return this.executeWithTcpServer(args);
} else {
return this.createProcess(binary, args);
}
}
async executeWithTcpServer(args) {
const serverInfo = globalTcpServers.get(this.connectionId);
if (!serverInfo || !serverInfo.process || serverInfo.process.killed) {
await this.startTcpServer();
await this.waitForServerReady();
}
return this.sendTcpRequestFast(args);
}
async waitForServerReady() {
const maxRetries = 10;
const retryDelay = 500;
for (let i = 0; i < maxRetries; i++) {
if (await this.isServerResponding()) {
return;
}
await this.sleep(retryDelay);
}
throw new Error("TCP server failed to become ready within timeout");
}
async isServerResponding() {
return new Promise((resolve5) => {
const client = new net.Socket();
const timeout = setTimeout(() => {
client.destroy();
resolve5(false);
}, 1e3);
client.connect(this.tcpPort, "127.0.0.1", () => {
clearTimeout(timeout);
client.destroy();
resolve5(true);
});
client.on("error", () => {
clearTimeout(timeout);
resolve5(false);
});
});
}
sleep(ms) {
return new Promise((resolve5) => setTimeout(resolve5, ms));
}
getCachedDML(dmlJson) {
if (queryCache.has(dmlJson)) {
return queryCache.get(dmlJson);
}
const parsed = JSON.parse(dmlJson);
if (cacheSize < MAX_CACHE_SIZE) {
queryCache.set(dmlJson, parsed);
cacheSize++;
}
return parsed;
}
async startTcpServer() {
await this.initializeBinary();
if (!this.binary) {
throw new Error("Binary not initialized");
}
this.tcpPort = await this.findAvailablePort(this.tcpPort);
return new Promise((resolve5, reject) => {
const serverArgs = [...this.arguments, "--action", "server", "--tcp-port", this.tcpPort.toString()];
const serverProcess = spawn2(this.binary["query_engine"], serverArgs);
let started = false;
const timeout = setTimeout(() => {
if (!started) {
serverProcess.kill();
reject(new Error("TCP server startup timeout"));
}
}, 15e3);
serverProcess.stdout.on("data", (data) => {
const output = data.toString();
if (output.includes("TCP Server listening on")) {
if (!started) {
started = true;
clearTimeout(timeout);
globalTcpServers.set(this.connectionId, {
port: this.tcpPort,
process: serverProcess
});
resolve5();
}
}
});
serverProcess.stderr.on("data", (data) => {
console.error(`[TCP Server Error] ${data.toString().trim()}`);
});
serverProcess.on("close", (code) => {
globalTcpServers.delete(this.connectionId);
});
serverProcess.on("error", (error) => {
if (!started) {
clearTimeout(timeout);
reject(error);
}
});
});
}
async sendTcpRequest(args) {
return new Promise((resolve5, reject) => {
const client = new net.Socket();
let responseBuffer = "";
let isResolved = false;
const timeout = setTimeout(() => {
if (!isResolved) {
isResolved = true;
client.destroy();
reject(new Error("TCP request timeout"));
}
}, this.timeout * 2);
client.connect(this.tcpPort, "127.0.0.1", () => {
const dmlIndex = args.findIndex((arg) => arg === "--dml");
const dmlJson = dmlIndex !== -1 ? args[dmlIndex + 1] : null;
if (!dmlJson) {
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
client.destroy();
reject(new Error("No DML found in arguments"));
}
return;
}
const command = {
action: "execute",
dml: dmlJson
};
client.write(JSON.stringify(command));
});
client.on("data", (data) => {
responseBuffer += data.toString();
const match = responseBuffer.match(/PROCESS_RESPONSE:(\{.*\})/);
if (match && !isResolved) {
isResolved = true;
clearTimeout(timeout);
client.destroy();
try {
const response = JSON.parse(match[1]);
resolve5({
status: response.status,
message: response.message,
data: response.data
});
} catch (error) {
reject(new Error("Failed to parse TCP response"));
}
}
});
client.on("error", (error) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
reject(error);
}
});
client.on("close", () => {
if (!isResolved) {
clearTimeout(timeout);
if (!responseBuffer.includes("PROCESS_RESPONSE:")) {
reject(new Error("Connection closed without valid response"));
}
}
});
});
}
async sendTcpRequestFast(args) {
const existingConnection = globalTcpConnections.get(this.connectionId);
if (existingConnection && existingConnection.readyState === "open") {
try {
return await this.sendOnExistingConnection(existingConnection, args);
} catch (error) {
globalTcpConnections.delete(this.connectionId);
existingConnection.destroy();
}
}
return await this.createPersistentConnection(args);
}
async sendOnExistingConnection(connection, args) {
return new Promise((resolve5, reject) => {
const dmlIndex = args.findIndex((arg) => arg === "--dml");
const dmlJson = dmlIndex !== -1 ? args[dmlIndex + 1] : null;
if (!dmlJson) {
reject(new Error("No DML found in arguments"));
return;
}
let responseBuffer = "";
let isResolved = false;
const timeout = setTimeout(() => {
if (!isResolved) {
isResolved = true;
reject(new Error("TCP request timeout"));
}
}, 100);
const onData = (data) => {
responseBuffer += data.toString();
const match = responseBuffer.match(/PROCESS_RESPONSE:(\{.*\})/);
if (match && !isResolved) {
isResolved = true;
clearTimeout(timeout);
connection.off("data", onData);
connection.off("error", onError);
try {
const response = JSON.parse(match[1]);
resolve5({
status: response.status,
message: response.message,
data: response.data
});
} catch (error) {
reject(new Error("Failed to parse TCP response"));
}
}
};
const onError = (error) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
connection.off("data", onData);
connection.off("error", onError);
reject(error);
}
};
connection.on("data", onData);
connection.on("error", onError);
const command = {
action: "execute",
dml: dmlJson
};
connection.write(JSON.stringify(command));
});
}
async createPersistentConnection(args) {
return new Promise((resolve5, reject) => {
let client = globalTcpConnections.get(this.connectionId);
let isNewConnection = false;
if (!client || client.destroyed) {
client = new net.Socket();
isNewConnection = true;
client.setNoDelay(true);
client.setKeepAlive(true, 6e4);
client.setMaxListeners(20);
}
let responseBuffer = "";
let isResolved = false;
const timeout = setTimeout(() => {
if (!isResolved) {
isResolved = true;
reject(new Error("TCP connection timeout"));
}
}, 5e3);
const executeQuery = () => {
const dmlIndex = args.findIndex((arg) => arg === "--dml");
const dmlJson = dmlIndex !== -1 ? args[dmlIndex + 1] : null;
if (!dmlJson) {
clearTimeout(timeout);
if (!isResolved) {
isResolved = true;
reject(new Error("No DML found in arguments"));
}
return;
}
const command = {
action: "execute",
dml: dmlJson
};
const commandStr = JSON.stringify(command);
const canWrite = client.write(commandStr);
if (!canWrite) {
client.once("drain", () => {
});
}
};
const dataHandler = (data) => {
responseBuffer += data.toString();
const match = responseBuffer.match(/PROCESS_RESPONSE:(\{.*\})/);
if (match && !isResolved) {
isResolved = true;
clearTimeout(timeout);
try {
const response = JSON.parse(match[1]);
responseBuffer = "";
client.removeListener("data", dataHandler);
resolve5({
status: response.status,
message: response.message,
data: response.data
});
} catch (error) {
client.removeListener("data", dataHandler);
reject(new Error("Failed to parse TCP response"));
}
}
};
const errorHandler = (error) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
globalTcpConnections.delete(this.connectionId);
client.removeListener("data", dataHandler);
reject(error);
}
};
const closeHandler = () => {
globalTcpConnections.delete(this.connectionId);
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
client.removeListener("data", dataHandler);
reject(new Error("Connection closed unexpectedly"));
}
};
if (isNewConnection) {
client.connect(this.tcpPort, "127.0.0.1", () => {
clearTimeout(timeout);
globalTcpConnections.set(this.connectionId, client);
client.on("error", errorHandler);
client.on("close", closeHandler);
client.on("data", dataHandler);
executeQuery();
});
} else {
clearTimeout(timeout);
client.on("data", dataHandler);
executeQuery();
}
});
}
async createProcess(binary, args) {
await this.initializeBinary();
if (!this.binary) {
throw new Error("Binary not initialized");
}
return new Promise((resolve5, reject) => {
const child = spawn2(this.binary[binary], [...this.arguments, ...args]);
let stdoutBuffer = "";
let stderrBuffer = "";
let isResolved = false;
const timeoutId = setTimeout(() => {
if (!isResolved) {
isResolved = true;
child.kill();
reject(new Error("Process timeout"));
}
}, this.timeout);
const resolveOnce = (response) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeoutId);
resolve5(response);
}
};
child.stdout.on("data", (data) => {
stdoutBuffer += data.toString();
const match = stdoutBuffer.match(/PROCESS_RESPONSE:(\{.*\})/);
if (match) {
try {
const response = JSON.parse(match[1]);
resolveOnce({
status: response.status,
message: response.message,
data: response.data
});
} catch (error) {
resolveOnce({
status: 500,
message: "Failed to parse response JSON",
data: null
});
}
}
});
child.stderr.on("data", (data) => {
stderrBuffer += data.toString();
const match = stderrBuffer.match(/PROCESS_RESPONSE:(\{.*\})/);
if (match) {
try {
const response = JSON.parse(match[1]);
resolveOnce({
status: response.status,
message: response.message,
data: response.data
});
} catch (error) {
resolveOnce({
status: 500,
message: "Failed to parse response JSON",
data: null
});
}
}
});
child.on("close", (code) => {
clearTimeout(timeoutId);
if (!isResolved) {
resolveOnce({
status: code === 0 ? 200 : 500,
message: code === 0 ? "Process completed" : `Process exited with code ${code}`,
data: null
});
}
});
child.on("error", (error) => {
clearTimeout(timeoutId);
if (!isResolved) {
resolveOnce({
status: 500,
message: `Process error: ${error.message}`,
data: null
});
}
});
child.unref();
});
}
async disconnect() {
const connection = globalTcpConnections.get(this.connectionId);
if (connection && connection.readyState === "open") {
connection.write(JSON.stringify({ action: "disconnect" }));
connection.destroy();
globalTcpConnections.delete(this.connectionId);
}
const serverInfo = globalTcpServers.get(this.connectionId);
if (serverInfo && serverInfo.process && !serverInfo.process.killed) {
serverInfo.process.kill();
globalTcpServers.delete(this.connectionId);
return {
status: 200,
message: "TCP server and connections stopped",
data: null
};
}
return {
status: 200,
message: "No TCP server to stop",
data: null
};
}
};
// src/lib/SqliteExecutor.ts
import { exec } from "child_process";
import * as path5 from "path";
import * as fs3 from "fs";
import { promisify } from "util";
import { createRequire as createRequire3 } from "module";
import { fileURLToPath as fileURLToPath3 } from "url";
import { dirname as dirname3 } from "path";
var execAsync = promisify(exec);
var SqliteExecutor = class {
binaryPath;
dbPath;
constructor(dbPath) {
this.dbPath = dbPath;
this.binaryPath = this.getBinaryPath();
}
getBinaryPath() {
const __filename2 = typeof import.meta !== "undefined" && import.meta.url ? fileURLToPath3(import.meta.url) : "";
const __dirname = __filename2 ? dirname3(__filename2) : process.cwd();
const possibleDirs = [
path5.resolve(process.cwd(), ".dbcube", "bin"),
path5.resolve(process.cwd(), "node_modules", ".dbcube", "bin"),
path5.resolve(__dirname, "..", "bin")
];
const platform2 = process.platform;
const extension = platform2 === "win32" ? ".exe" : "";
const binaryName = `sqlite-engine-${platform2 === "win32" ? "windows" : platform2 === "darwin" ? "macos" : "linux"}-x64${extension}`;
for (const dir of possibleDirs) {
const fullPath = path5.join(dir, binaryName);
if (fs3.existsSync(fullPath)) {
return fullPath;
}
}
const fallbackName = `sqlite-engine${extension}`;
for (const dir of possibleDirs) {
const fullPath = path5.join(dir, fallbackName);
if (fs3.existsSync(fullPath)) {
return fullPath;
}
}
return path5.join(possibleDirs[0], binaryName);
}
async executeBinary(args) {
const escapedArgs = args.map((arg) => {
if (arg.includes(" ") || arg.includes('"') || arg.includes("(") || arg.includes(")")) {
return `"${arg.replace(/"/g, '\\"')}"`;
}
return arg;
});
const command = `"${this.binaryPath}" ${escapedArgs.join(" ")}`;
try {
const { stdout, stderr } = await execAsync(command, {
maxBuffer: 10 * 1024 * 1024,
// 10MB buffer
timeout: 3e4
// 30s timeout
});
if (stderr && stderr.trim()) {
console.warn("SQLite Engine Warning:", stderr);
}
const result = JSON.parse(stdout.trim());
return result;
} catch (error) {
if (error.stdout) {
try {
const result = JSON.parse(error.stdout.trim());
return result;
} catch (parseError) {
}
}
throw new Error(`SQLite execution failed: ${error.message}`);
}
}
async connect() {
try {
const result = await this.executeBinary([
"--action",
"connect",
"--database",
this.dbPath
]);
return result.status === "success";
} catch (error) {
return false;
}
}
async exists() {
try {
const result = await this.executeBinary([
"--action",
"exists",
"--database",
this.dbPath
]);
return result.status === "success" && result.data === true;
} catch (error) {
return false;
}
}
async query(sql, params) {
const args = [
"--action",
"query",
"--database",
this.dbPath,
"--query",
sql
];
if (params && params.length > 0) {
args.push("--params", JSON.stringify(params));
}
return this.executeBinary(args);
}
// Método para múltiples queries (como lo hace better-sqlite3)
async queryMultiple(sql) {
return this.query(sql);
}
// Método prepare que simula prepared statements
prepare(sql) {
return {
all: async (...params) => {
const result = await this.query(sql, params);
if (result.status === "error") {
throw new Error(result.message);
}
return Array.isArray(result.data) ? result.data : [];
},
run: async (...params) => {
const result = await this.query(sql, params);
if (result.status === "error") {
throw new Error(result.message);
}
if (typeof result.data === "object" && result.data.hasOwnProperty("changes") && result.data.hasOwnProperty("lastID")) {
return result.data;
}
return { changes: 0, lastID: 0 };
}
};
}
// Para compatibilidad con better-sqlite3 API sincrona usando deasync si es necesario
prepareSync(sql) {
const __filename2 = typeof import.meta !== "undefined" && import.meta.url ? fileURLToPath3(import.meta.url) : "";
const requireUrl = __filename2 || import.meta.url;
const require2 = createRequire3(requireUrl);
const deasync = require2("deasync");
return {
all: (...params) => {
let result;
let done = false;
let error;
this.query(sql, params).then((res) => {
if (res.status === "error") {
error = new Error(res.m