@dbcube/query-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
870 lines (867 loc) • 26.7 kB
JavaScript
// src/lib/Database.ts
import fs from "fs";
import { Engine, ComputedFieldProcessor, TriggerProcessor } from "@dbcube/core";
// src/lib/Trigger.ts
import path from "path";
import { FileLogger } from "@dbcube/core";
import { createRequire } from "module";
var Trigger = class {
triggers;
databaseName;
instance;
constructor(instance, databaseName, metadata) {
this.triggers = metadata;
this.databaseName = databaseName;
this.instance = instance;
}
get(type) {
return this.triggers.find((tr) => tr.type === type);
}
async execute(type, row) {
const trigger = this.triggers.find((tr) => tr.type === type);
if (trigger) {
const logFilePath = path.resolve(
process.cwd(),
"dbcube",
"logs",
"triggers",
this.databaseName,
`${trigger.table_ref}_${trigger.type}.log`
);
const interceptor = FileLogger.interceptConsole(logFilePath, {
keepOriginal: false,
useBuffer: true
});
const pathFile = path.resolve(process.cwd(), "dbcube", "triggers", `${trigger.database_ref}_${trigger.table_ref}_${trigger.type}.js`);
const requireUrl = typeof __filename !== "undefined" ? __filename : process.cwd();
const require2 = createRequire(requireUrl);
delete require2.cache[require2.resolve(pathFile)];
const triggerModule = require2(pathFile);
const dataProcess = triggerModule.default || triggerModule;
await dataProcess({ db: this.instance, oldData: row, newData: row });
interceptor.restore();
return interceptor;
}
return null;
}
};
// src/lib/Database.ts
var Database = class {
name;
engine;
computedFields;
triggers;
constructor(name) {
this.name = name;
const engine = new Engine(name);
this.engine = engine;
this.computedFields = [];
this.triggers = [];
}
async useComputes() {
this.computedFields = await ComputedFieldProcessor.getComputedFields(this.name);
}
async useTriggers() {
this.triggers = await TriggerProcessor.getTriggers(this.name);
}
async connect() {
const response = await this.engine.run("query_engine", [
"--action",
"connect"
]);
if (response.status != 200) {
returnFormattedError(response.status, response.message);
}
return response.data;
}
async disconnect() {
return this.engine.run("query_engine", [
"--action",
"disconnect"
]);
}
/**
* Creates and returns a new instance of `Table` for the specified table.
* This method is used to start building queries for a specific table.
* It provides a fluent interface for common database operations like select, insert, update, and delete.
*
* @param {string} tableName - The name of the table to query.
* @returns {Table} - Returns a new instance of `Table` for the specified table.
*
* @example
* // Select all records from a table
* const users = await db.table('users').get();
*
* // Select records with conditions
* const activeUsers = await db.table('users')
* .where('status', '=', 'active')
* .orderBy('created_at', 'DESC')
* .limit(10)
* .get();
*
* // Insert records
* await db.table('users').insert([
* { name: 'John', email: 'john@example.com', age: 30 }
* ]);
*
* // Update records
* await db.table('users')
* .where('id', '=', 1)
* .update({ status: 'inactive' });
*
* // Delete records
* await db.table('users')
* .where('status', '=', 'deleted')
* .delete();
*
* // Access column management
* const columns = await db.table('users').columns().get();
*/
table(tableName) {
return new Table(this, this.name, tableName, this.engine, this.computedFields, this.triggers);
}
};
var Table = class _Table {
engine;
nextType = "AND";
dml;
computedFields = [];
trigger;
triggers;
constructor(instance, databaseName, tableName, engine = null, computedFields = [], triggers = []) {
this.engine = engine;
this.computedFields = computedFields;
this.triggers = triggers;
this.trigger = new Trigger(instance, databaseName, triggers);
this.nextType = "AND";
this.dml = {
type: "select",
database: databaseName,
table: tableName,
columns: ["*"],
distinct: false,
joins: [],
where: [],
orderBy: [],
groupBy: [],
limit: null,
offset: null,
data: null,
aggregation: null
};
}
/**
* Specifies the columns to select in a SELECT query.
*
* @param {string[]} fields - Array of column names to select. If empty, selects all columns.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').select(['id', 'name']).get();
* console.log(users); // [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
*/
select(fields = []) {
this.dml.type = "select";
this.dml.columns = fields.length > 0 ? fields : ["*"];
return this;
}
where(column, operator, value) {
this.dml.where.push({
column,
operator,
value,
type: this.nextType,
isGroup: false
});
this.nextType = "AND";
return this;
}
orWhere(column, operator, value) {
this.dml.where.push({
column,
operator,
value,
type: "OR",
isGroup: false
});
return this;
}
/**
* Adds a grouped WHERE condition to the query.
*
* @param {WhereCallback} callback - A callback function that receives a new Table instance to build the grouped conditions.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').whereGroup(query => {
* query.where('age', '>', 25).orWhere('name', '=', 'Jane');
* }).get();
* console.log(users); // [{ id: 1, name: 'John', age: 30 }, { id: 2, name: 'Jane', age: 25 }]
*/
whereGroup(callback) {
const groupQuery = new _Table(this.dml.database, this.dml.table, this.engine);
callback(groupQuery);
this.dml.where.push({
type: this.nextType,
isGroup: true,
conditions: groupQuery.dml.where
});
this.nextType = "AND";
return this;
}
or() {
this.nextType = "OR";
return this;
}
and() {
this.nextType = "AND";
return this;
}
/**
* Adds a WHERE BETWEEN condition to the query.
*
* @param {string} column - The column to filter by.
* @param {[any, any]} values - A tuple with two values representing the range.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').whereBetween('age', [20, 30]).get();
* console.log(users); // [{ id: 1, name: 'John', age: 30 }, { id: 2, name: 'Jane', age: 25 }]
*/
whereBetween(column, values) {
const [value1, value2] = values;
if (value1 !== void 0 && value2 !== void 0) {
this.dml.where.push({
column,
operator: "BETWEEN",
value: [value1, value2],
type: this.nextType,
isGroup: false
});
this.nextType = "AND";
}
return this;
}
/**
* Adds a WHERE IN condition to the query.
*
* @param {string} column - The column to filter by.
* @param {any[]} values - An array of values to match.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').whereIn('id', [1, 2]).get();
* console.log(users); // [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
*/
whereIn(column, values) {
if (Array.isArray(values) && values.length > 0) {
this.dml.where.push({
column,
operator: "IN",
value: values,
type: this.nextType,
isGroup: false
});
this.nextType = "AND";
}
return this;
}
/**
* Adds a WHERE IS NULL condition to the query.
*
* @param {string} column - The column to filter by.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').whereNull('email').get();
* console.log(users); // [{ id: 3, name: 'Alice', email: null }]
*/
whereNull(column) {
this.dml.where.push({
column,
operator: "IS NULL",
type: this.nextType,
isGroup: false
});
this.nextType = "AND";
return this;
}
/**
* Adds a WHERE IS NOT NULL condition to the query.
*
* @param {string} column - The column to filter by.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').whereNotNull('email').get();
* console.log(users); // [{ id: 1, name: 'John', email: 'john@example.com' }]
*/
whereNotNull(column) {
this.dml.where.push({
column,
operator: "IS NOT NULL",
type: this.nextType,
isGroup: false
});
this.nextType = "AND";
return this;
}
/**
* Adds a JOIN clause to the query.
*
* @param {string} table - The table to join.
* @param {string} column1 - The column from the current table.
* @param {string} operator - The comparison operator (e.g., '=', '>', '<').
* @param {string} column2 - The column from the joined table.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').join('orders', 'users.id', '=', 'orders.user_id').get();
* console.log(users); // [{ id: 1, name: 'John', order_id: 101 }]
*/
join(table, column1, operator, column2) {
this.dml.joins.push({
type: "INNER",
table,
on: {
column1,
operator,
column2
}
});
return this;
}
/**
* Adds a LEFT JOIN clause to the query.
*
* @param {string} table - The table to join.
* @param {string} column1 - The column from the current table.
* @param {string} operator - The comparison operator (e.g., '=', '>', '<').
* @param {string} column2 - The column from the joined table.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').leftJoin('orders', 'users.id', '=', 'orders.user_id').get();
* console.log(users); // [{ id: 1, name: 'John', order_id: 101 }, { id: 2, name: 'Jane', order_id: null }]
*/
leftJoin(table, column1, operator, column2) {
this.dml.joins.push({
type: "LEFT",
table,
on: {
column1,
operator,
column2
}
});
return this;
}
/**
* Adds a RIGHT JOIN clause to the query.
*
* @param {string} table - The table to join.
* @param {string} column1 - The column from the current table.
* @param {string} operator - The comparison operator (e.g., '=', '>', '<').
* @param {string} column2 - The column from the joined table.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').rightJoin('orders', 'users.id', '=', 'orders.user_id').get();
* console.log(users); // [{ id: 1, name: 'John', order_id: 101 }, { id: null, name: null, order_id: 102 }]
*/
rightJoin(table, column1, operator, column2) {
this.dml.joins.push({
type: "RIGHT",
table,
on: {
column1,
operator,
column2
}
});
return this;
}
/**
* Adds an ORDER BY clause to the query.
*
* @param {string} column - The column to order by.
* @param {'ASC' | 'DESC'} direction - The sorting direction ('ASC' or 'DESC').
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').orderBy('name', 'ASC').get();
* console.log(users); // [{ id: 2, name: 'Jane' }, { id: 1, name: 'John' }]
*/
orderBy(column, direction = "ASC") {
const validDirections = ["ASC", "DESC"];
if (validDirections.includes(direction.toUpperCase())) {
this.dml.orderBy.push({
column,
direction: direction.toUpperCase()
});
} else {
throw new Error(`Invalid direction: ${direction}. Use 'ASC' or 'DESC'.`);
}
return this;
}
/**
* Adds a GROUP BY clause to the query.
*
* @param {string} column - The column to group by.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').groupBy('age').get();
* console.log(users); // [{ age: 30, count: 1 }, { age: 25, count: 1 }]
*/
groupBy(column) {
this.dml.groupBy.push(column);
return this;
}
/**
* Adds a DISTINCT clause to the query.
*
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').distinct().select(['name']).get();
* console.log(users); // [{ name: 'John' }, { name: 'Jane' }]
*/
distinct() {
this.dml.distinct = true;
return this;
}
/**
* Adds a COUNT clause to the query.
*
* @param {string} column - The column to count (default is '*').
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const count = await db.table('users').count().first();
* console.log(count); // { count: 2 }
*/
count(column = "*") {
this.dml.type = "select";
this.dml.aggregation = {
type: "COUNT",
column,
alias: "count"
};
this.dml.columns = [`COUNT(${column}) AS count`];
return this;
}
/**
* Adds a SUM clause to the query.
*
* @param {string} column - The column to sum.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const totalAge = await db.table('users').sum('age').first();
* console.log(totalAge); // { sum: 55 }
*/
sum(column) {
this.dml.type = "select";
this.dml.aggregation = {
type: "SUM",
column,
alias: "sum"
};
this.dml.columns = [`SUM(${column}) AS sum`];
return this;
}
/**
* Adds an AVG clause to the query.
*
* @param {string} column - The column to calculate the average.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const avgAge = await db.table('users').avg('age').first();
* console.log(avgAge); // { avg: 27.5 }
*/
avg(column) {
this.dml.type = "select";
this.dml.aggregation = {
type: "AVG",
column,
alias: "avg"
};
this.dml.columns = [`AVG(${column}) AS avg`];
return this;
}
/**
* Adds a MAX clause to the query.
*
* @param {string} column - The column to find the maximum value.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const maxAge = await db.table('users').max('age').first();
* console.log(maxAge); // { max: 30 }
*/
max(column) {
this.dml.type = "select";
this.dml.aggregation = {
type: "MAX",
column,
alias: "max"
};
this.dml.columns = [`MAX(${column}) AS max`];
return this;
}
/**
* Adds a MIN clause to the query.
*
* @param {string} column - The column to find the minimum value.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const minAge = await db.table('users').min('age').first();
* console.log(minAge); // { min: 25 }
*/
min(column) {
this.dml.type = "select";
this.dml.aggregation = {
type: "MIN",
column,
alias: "min"
};
this.dml.columns = [`MIN(${column}) AS min`];
return this;
}
/**
* Adds a LIMIT clause to the query.
*
* @param {number} number - The maximum number of rows to return.
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').limit(1).get();
* console.log(users); // [{ id: 1, name: 'John', age: 30 }]
*/
limit(number) {
this.dml.limit = number;
return this;
}
/**
* Adds pagination to the query using LIMIT and OFFSET.
*
* @param {number} number - The page number (starting from 1).
* @returns {Table} - Returns the current instance of Table for method chaining.
*
* @example
* const users = await db.table('users').limit(1).page(2).get();
* console.log(users); // [{ id: 2, name: 'Jane', age: 25 }]
*/
page(number) {
if (this.dml.limit) {
this.dml.offset = (number - 1) * this.dml.limit;
}
return this;
}
/**
* Executes the query and returns all matching rows.
*
* @returns {Promise<DatabaseRecord[]>} - Returns an array of rows.
*
* @example
* const users = await db.table('users').get();
* console.log(users); // [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
*/
async get() {
try {
this.dml.type = "select";
this.dml.data = null;
const result = await this.getResponse();
return result;
} catch (error) {
throw error;
}
}
/**
* Executes the query and returns the first matching row.
*
* @returns {Promise<DatabaseRecord | null>} - Returns the first row or null if no rows match.
*
* @example
* const user = await db.table('users').first();
* console.log(user); // { id: 1, name: 'John' }
*/
async first() {
this.dml.type = "select";
this.dml.data = null;
this.dml.limit = 1;
try {
const result = await this.getResponse();
return result[0] || null;
} catch (error) {
throw error;
}
}
/**
* Finds a row by a specific column value.
*
* @param {any} value - The value to search for.
* @param {string} column - The column to search in (default is 'id').
* @returns {Promise<DatabaseRecord | null>} - Returns the first matching row or null if no rows match.
*
* @example
* const user = await db.table('users').find(1);
* console.log(user); // { id: 1, name: 'John' }
*/
async find(value, column = "id") {
this.dml.type = "select";
this.dml.data = null;
this.where(column, "=", value);
this.dml.limit = 1;
try {
const result = await this.getResponse();
return Array.isArray(result) ? result[0] || null : null;
} catch (error) {
throw error;
}
}
/**
* Inserts one or more rows into the table.
*
* @param {DatabaseRecord[]} data - An array of objects representing the rows to insert.
* @returns {Promise<DatabaseRecord[]>} - Returns an array of the inserted rows.
*
* @example
* const newUsers = await db.table('users').insert([
* { name: 'Alice', age: 28 },
* { name: 'Bob', age: 32 }
* ]);
* console.log(newUsers); // [{ id: 3, name: 'Alice', age: 28 }, { id: 4, name: 'Bob', age: 32 }]
*/
async insert(data) {
if (!Array.isArray(data)) {
throw new Error("The insert method requires an array of objects with key-value pairs.");
}
if (!data.every((item) => typeof item === "object" && item !== null)) {
throw new Error("The array must contain only valid objects.");
}
this.dml.type = "insert";
this.dml.data = data;
await this.getResponse(this.dml, "Add");
return data;
}
/**
* Updates rows in the table based on the defined conditions.
*
* @param {DatabaseRecord} data - An object with key-value pairs representing the fields to update.
* @returns {Promise<any>} - Returns the result of the update operation.
*
* @example
* const result = await db.table('users')
* .where('id', '=', 1)
* .update({ name: 'John Updated', age: 31 });
* console.log(result); // { affectedRows: 1 }
*/
async update(data) {
if (typeof data !== "object" || Array.isArray(data)) {
throw new Error("The update method requires an object with key-value pairs.");
}
if (this.dml.where.length === 0) {
throw new Error("You must specify at least one WHERE condition to perform an update.");
}
this.dml.type = "update";
this.dml.data = data;
await this.getResponse(this.dml, "Update");
return data;
}
/**
* Deletes rows from the table based on the defined conditions.
*
* @returns {Promise<any>} - Returns the result of the delete operation.
*
* @example
* const result = await db.table('users').where('id', '=', 1).delete();
* console.log(result); // { affectedRows: 1 }
*/
async delete() {
if (this.dml.where.length === 0) {
throw new Error("You must specify at least one WHERE condition to perform a delete.");
}
this.dml.type = "delete";
const newDml = {
...this.dml,
type: "select",
columns: ["*"],
distinct: false,
joins: [],
orderBy: [],
groupBy: [],
limit: null,
offset: null,
data: null,
aggregation: null
};
const deleteData = await this.getResponse(newDml, "Delete");
await this.getResponse();
return deleteData;
}
async getResponse(dml = null, type = null) {
const localDML = dml ? dml : this.dml;
const computedFieldsNeeded = [];
let dependeciesArrray = [];
if (this.computedFields.length > 0) {
let columns = localDML.columns;
for (const field of localDML.columns) {
const computedField = this.computedFields.find((cf) => cf.column === field);
if (computedField) {
computedFieldsNeeded.push(computedField);
const dependencies = ComputedFieldProcessor.extractDependencies(computedField.instruction);
dependeciesArrray = [...dependeciesArrray, ...dependencies];
columns = Array.from(/* @__PURE__ */ new Set([...columns, ...dependencies]));
columns = columns.filter((col) => col != field);
}
}
localDML.columns = columns;
}
let arrayResult = [];
if (type) {
const beffore = this.trigger.get("before" + type);
const after = this.trigger.get("after" + type);
if (this.triggers.length > 0 && (beffore || after)) {
const dataset = localDML.data;
for (let index = 0; index < dataset.length; index++) {
const data = dataset[index];
const newDML = { ...localDML, data: [data] };
if (beffore) {
const interceptor = await this.trigger.execute("before" + type, data);
const response = await this.engine.run("query_engine", [
"--action",
"execute",
"--dml",
JSON.stringify(newDML)
]);
if (response.status != 200) {
interceptor.discard();
returnFormattedError(response.status, response.message);
}
await interceptor.commit();
arrayResult = response.data;
}
if (after) {
const response = await this.engine.run("query_engine", [
"--action",
"execute",
"--dml",
JSON.stringify(newDML)
]);
if (response.status != 200) {
returnFormattedError(response.status, response.message);
}
const interceptor = await this.trigger.execute("after" + type, data);
await interceptor.commit();
}
}
} else {
const response = await this.engine.run("query_engine", [
"--action",
"execute",
"--dml",
JSON.stringify(localDML)
]);
if (response.status != 200) {
returnFormattedError(response.status, response.message);
}
arrayResult = response.data;
}
} else {
const response = await this.engine.run("query_engine", [
"--action",
"execute",
"--dml",
JSON.stringify(localDML)
]);
if (response.status != 200) {
returnFormattedError(response.status, response.message);
}
arrayResult = response.data;
}
if (computedFieldsNeeded.length > 0) {
let newDataset = ComputedFieldProcessor.computedFields(arrayResult, computedFieldsNeeded);
const result = newDataset.map((obj) => {
const newObj = { ...obj };
dependeciesArrray.forEach((key) => delete newObj[key]);
return newObj;
});
return result;
}
return arrayResult;
}
};
function returnFormattedError(status, message) {
const RESET = "\x1B[0m";
const RED = "\x1B[31m";
const YELLOW = "\x1B[33m";
const BOLD = "\x1B[1m";
const CYAN = "\x1B[36m";
const GRAY = "\x1B[90m";
const UNDERLINE = "\x1B[4m";
const MAGENTA = "\x1B[35m";
let output = "";
let help = "";
const color = status === 600 ? YELLOW : RED;
if (message.includes("[help]")) {
const parts = message.split("[help]");
output += `
${RED}${BOLD}${parts[0]}${RESET}`;
help += `
${MAGENTA}${BOLD}[help]${RESET} ${GRAY}${parts[1]}${RESET}
`;
} else {
output += `
${color}${BOLD}${message}${RESET}
`;
}
const err = new Error();
const stackLines = err.stack?.split("\n") || [];
const relevantStackLine = stackLines.find(
(line) => line.includes(".js:") && !line.includes("node_modules")
);
if (relevantStackLine) {
const match = relevantStackLine.match(/\((.*):(\d+):(\d+)\)/) || relevantStackLine.match(/at (.*):(\d+):(\d+)/);
if (match) {
const [, filePath, lineStr, columnStr] = match;
const lineNum = parseInt(lineStr, 10);
const errorLocation = `${filePath}:${lineStr}:${columnStr}`;
try {
const codeLines = fs.readFileSync(filePath, "utf-8").split("\n");
const start = Math.max(0, lineNum - 3);
const end = Math.min(codeLines.length, lineNum + 2);
output += `
${CYAN}${BOLD}[code] ${RESET}${YELLOW} ${UNDERLINE}${errorLocation}${RESET}
`;
for (let i = start; i < end; i++) {
const line = codeLines[i];
const lineLabel = `${i + 1}`.padStart(4, " ");
const pointer = i + 1 === lineNum ? `${RED}<-${RESET}` : " ";
output += `${GRAY}${lineLabel}${RESET} ${pointer} ${line}
`;
}
} catch (err2) {
output += `${YELLOW}\u26A0\uFE0F No se pudo leer el archivo de origen: ${filePath}${RESET}
`;
output += `
${CYAN}${BOLD}Stack Trace:${RESET}
${stackLines.slice(2).join("\n")}
`;
}
}
}
output += help;
console.error(output);
}
// src/index.ts
var index_default = Database;
export {
Database,
Table,
index_default as default
};
//# sourceMappingURL=index.js.map