@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,336 lines (1,328 loc) • 55.3 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
CubeValidator: () => CubeValidator,
DependencyResolver: () => DependencyResolver,
Schema: () => Schema,
UIUtils: () => UIUtils,
default: () => index_default
});
module.exports = __toCommonJS(index_exports);
// src/lib/Schema.ts
var import_fs4 = __toESM(require("fs"));
var import_core = require("@dbcube/core");
var import_path3 = __toESM(require("path"));
// src/lib/FileUtils.ts
var fs = __toESM(require("fs"));
var path = __toESM(require("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
var import_chalk2 = __toESM(require("chalk"));
// src/lib/UIUtils.ts
var import_chalk = __toESM(require("chalk"));
var import_fs = __toESM(require("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(`${import_chalk.default.blue("\u251C\u2500")} ${import_chalk.default.cyan(itemName)} `);
let dotCount = 0;
const interval = setInterval(() => {
if (dotCount < maxDots) {
process.stdout.write(import_chalk.default.gray("."));
dotCount++;
} else {
clearInterval(interval);
resolve2();
}
}, 10);
});
}
/**
* Shows success for a processed item
*/
static showItemSuccess(itemName) {
process.stdout.write(` ${import_chalk.default.green("\u2713")} ${import_chalk.default.gray("OK")}
`);
}
/**
* Shows error for an item (simplified - only shows X)
*/
static showItemError(itemName, error) {
process.stdout.write(` ${import_chalk.default.red("\u2717")}
`);
}
/**
* Shows operation header
*/
static showOperationHeader(operationName, databaseName, icon = "\u{1F5D1}\uFE0F") {
console.log(`
${import_chalk.default.cyan(icon)} ${import_chalk.default.bold.green(operationName.toUpperCase())}`);
console.log(import_chalk.default.gray("\u2500".repeat(60)));
console.log(`${import_chalk.default.blue("\u250C\u2500")} ${import_chalk.default.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(`
${import_chalk.default.cyan("\u{1F4CA}")} ${import_chalk.default.bold.green(`SUMMARY OF ${operationName.toUpperCase()}`)}`);
console.log(import_chalk.default.gray("\u2500".repeat(60)));
if (successCount > 0) {
console.log(`${import_chalk.default.green("\u250C\u2500")} ${import_chalk.default.bold("Successful processing:")}`);
console.log(`${import_chalk.default.green("\u251C\u2500")} ${import_chalk.default.cyan(`Items processed: ${successCount}`)}`);
console.log(`${import_chalk.default.green("\u251C\u2500")} ${import_chalk.default.gray(`Database: ${databaseName}`)}`);
if (processedItems.length > 0) {
console.log(`${import_chalk.default.green("\u251C\u2500")} ${import_chalk.default.yellow("Items updated:")}`);
processedItems.forEach((item, index) => {
const isLast = index === processedItems.length - 1;
const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
console.log(`${import_chalk.default.green("\u2502 ")} ${import_chalk.default.gray(connector)} ${import_chalk.default.cyan(item)}`);
});
}
}
if (errorCount > 0) {
console.log(`${import_chalk.default.red("\u251C\u2500")} ${import_chalk.default.bold.red(`Errors: ${errorCount}`)}`);
}
console.log(`${import_chalk.default.blue("\u251C\u2500")} ${import_chalk.default.gray(`Total time: ${totalTime}s`)}`);
console.log(`${import_chalk.default.blue("\u2514\u2500")} ${import_chalk.default.bold(totalProcessed > 0 ? import_chalk.default.green("\u2705 Completed") : import_chalk.default.yellow("\u26A0\uFE0F No changes"))}`);
if (summary.errors && summary.errors.length > 0) {
console.log(`
${import_chalk.default.red("\u{1F6AB}")} ${import_chalk.default.bold.red("ERRORS FOUND")}`);
console.log(import_chalk.default.red("\u2500".repeat(60)));
summary.errors.forEach((error, index) => {
console.log(`${import_chalk.default.red("[error]")} ${import_chalk.default.red(error.error)}`);
console.log("");
if (error.filePath) {
const location = error.lineNumber ? `${error.filePath}:${error.lineNumber}:7` : error.filePath;
console.log(`${import_chalk.default.cyan("[code]")} ${import_chalk.default.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 = import_fs.default.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(`${import_chalk.default.gray(lineNumStr)} ${import_chalk.default.red("<-")} ${import_chalk.default.white(line)}`);
} else {
console.log(`${import_chalk.default.gray(lineNumStr)} ${import_chalk.default.white(line)}`);
}
}
} catch (error) {
console.log(import_chalk.default.gray(" (unable to show code context)"));
}
}
};
// src/lib/CubeValidator.ts
var import_fs2 = __toESM(require("fs"));
var import_path = __toESM(require("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 = import_fs2.default.readFileSync(filePath, "utf8");
const lines = content.split("\n");
const fileName = import_path.default.basename(filePath, import_path.default.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: import_path.default.basename(filePath, import_path.default.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
var import_fs3 = __toESM(require("fs"));
var import_path2 = __toESM(require("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 (import_path2.default.isAbsolute(file)) {
filePath = file;
} else if (import_fs3.default.existsSync(file)) {
filePath = import_path2.default.resolve(file);
} else {
filePath = import_path2.default.join(process.cwd(), "dbcube", file);
}
try {
const tableNameResult = FileUtils_default.extracTableNameFromCube(filePath);
const tableName = tableNameResult.status === 200 ? tableNameResult.message : import_path2.default.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 = import_fs3.default.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 = import_path2.default.join(projectRoot, ".dbcube");
const orderFile = import_path2.default.join(dbcubeDir, "orderexecute.json");
if (!import_fs3.default.existsSync(dbcubeDir)) {
import_fs3.default.mkdirSync(dbcubeDir, { recursive: true });
}
import_fs3.default.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 = import_path2.default.join(projectRoot, ".dbcube", "orderexecute.json");
if (!import_fs3.default.existsSync(orderFile)) {
return null;
}
const content = import_fs3.default.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 = import_path2.default.isAbsolute(file) ? file : import_path2.default.join(process.cwd(), "dbcube", file);
const tableNameResult = FileUtils_default.extracTableNameFromCube(filePath);
const tableName = tableNameResult.status === 200 ? tableNameResult.message : import_path2.default.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
var import_module = require("module");
var Schema = class {
name;
engine;
constructor(name) {
this.name = name;
this.engine = new import_core.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: import_path3.default.basename(filePath, import_path3.default.extname(filePath)),
error: `Error reading database directive: ${dbResult.message}`,
filePath,
lineNumber: this.findDatabaseLineNumber(filePath)
}
};
}
const cubeDbName = dbResult.message;
const configInstance = new import_core.Config();
const configFilePath = import_path3.default.resolve(process.cwd(), "dbcube.config.js");
const requireUrl = typeof __filename !== "undefined" ? __filename : process.cwd();
const require2 = (0, import_module.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: import_path3.default.basename(filePath, import_path3.default.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: import_path3.default.basename(filePath, import_path3.default.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 = import_fs4.default.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 = import_fs4.default.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 = import_fs4.default.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 = import_path3.default.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 = import_path3.default.join(process.cwd(), "dbcube");
if (!import_fs4.default.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 = import_path3.default.isAbsolute(file) ? file : import_path3.default.join(cubesDir, file);
const stats = import_fs4.default.statSync(filePath);
if (stats.isFile()) {
const getTableName = FileUtils_default.extracTableNameFromCube(filePath);
const tableName = getTableName.status === 200 ? getTableName.message : import_path3.default.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 import_core.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 = import_path3.default.join(process.cwd(), "dbcube");
if (!import_fs4.default.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 = import_path3.default.isAbsolute(file) ? file : import_path3.default.join(cubesDir, file);
const stats = import_fs4.default.statSync(filePath);
if (stats.isFile()) {
const getTableName = FileUtils_default.extracTableNameFromCube(filePath);
const tableName = getTableName.status === 200 ? getTableName.message : import_path3.default.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 import_core.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 = import_path3.default.join(process.cwd(), "dbcube");
if (!import_fs4.default.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 = import_path3.default.isAbsolute(file) ? file : import_path3.default.join(cubesDir, file);
const stats = import_fs4.default.statSync(filePath);
if (stats.isFile()) {
const getSeederName = FileUtils_default.extracTableNameFromCube(filePath);
const seederName = getSeederName.status === 200 ? getSeederName.message : import_path3.default.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++