autosql
Version:
An auto-parser of JSON into SQL.
371 lines (370 loc) • 14.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PostgresDatabase = exports.MySQLDatabase = exports.Database = void 0;
const validateQuery_1 = require("./utils/validateQuery");
const utilities_1 = require("../helpers/utilities");
const defaults_1 = require("../config/defaults");
const ssh_1 = require("../helpers/ssh");
// Abstract database class to define common methods.
class Database {
connection = null;
config;
autoSQLHandler;
startDate = new Date();
constructor(config) {
this.config = (0, utilities_1.validateConfig)(config);
}
getConfig() {
return this.config;
}
updateTableMetadata(table, metaData, type = "metaData") {
if (!this.config[type]) {
this.config[type] = {};
}
this.config[type][table] = {
...this.config[type][table],
...metaData
};
}
updateSchema(schema) {
this.config.schema = schema;
}
static create(config) {
const DIALECTS = {
mysql: mysql_1.MySQLDatabase,
pgsql: pgsql_1.PostgresDatabase
};
const dialect = config.sqlDialect?.toLowerCase();
if (!DIALECTS[dialect]) {
throw new Error(`Unsupported SQL dialect: ${dialect}`);
}
return new DIALECTS[dialect](config);
}
getDialect() {
return this.config.sqlDialect;
}
async establishConnection() {
let attempts = 0;
const maxAttempts = defaults_1.maxQueryAttempts || 3;
while (attempts < maxAttempts) {
try {
if (this.config.sshConfig) {
const { stream, sshClient } = await (0, ssh_1.setSSH)(this.config.sshConfig);
this.config.sshStream = stream;
this.config.sshClient = sshClient;
}
await this.establishDatabaseConnection();
return;
}
catch (error) {
console.error(`Database connection attempt ${attempts + 1} failed:`, error.message);
const permanentErrors = await this.getPermanentErrors();
if (permanentErrors.includes(error.code)) {
console.error("Permanent error detected. Aborting retry.");
throw error;
}
attempts++;
if (attempts < maxAttempts) {
await new Promise((res) => setTimeout(res, 1000)); // Wait 1 second before retrying
}
}
}
throw new Error("Database connection failed after multiple attempts.");
}
// Test database connection.
async testConnection() {
try {
const result = await this.runQuery({ query: "SELECT 1 AS solution;" });
return !!result;
}
catch (error) {
console.error("Test connection failed:", error);
return false;
}
}
// Check if schema(s) exist.
async checkSchemaExists(schemaName) {
try {
const QueryInput = this.getCheckSchemaQuery(schemaName);
const result = await this.runQuery(QueryInput);
if (!result.success || !result.results) {
throw new Error(`Failed to check schema existence: ${result.error}`);
}
const resultsArray = result.results;
if (Array.isArray(schemaName)) {
return schemaName.reduce((acc, db) => {
acc[db] = resultsArray[0]?.[db] === 1;
return acc;
}, {});
}
return { [schemaName]: resultsArray[0]?.[schemaName] === 1 };
}
catch (error) {
return { [schemaName.toString()]: false };
}
}
async createSchema(schemaName) {
try {
const QueryInput = this.getCreateSchemaQuery(schemaName);
const result = await this.runQuery(QueryInput);
return { success: true };
}
catch (error) {
throw new Error(`Failed to create schema: ${error}`);
}
}
createTableQuery(table, headers) {
if (!table || !headers || Object.keys(headers).length === 0) {
throw new Error("Invalid table configuration: table name and headers are required.");
}
return this.getCreateTableQuery(table, headers);
}
async alterTableQuery(table, oldHeaders, newHeaders) {
if (!table || !oldHeaders || !newHeaders) {
throw new Error("Invalid table configuration: table name and headers are required.");
}
return await this.getAlterTableQuery(table, oldHeaders, newHeaders);
}
dropTableQuery(table) {
if (!table) {
throw new Error("Invalid table configuration: table name is required.");
}
return this.getDropTableQuery(table);
}
// Begin transaction.
async startTransaction() {
await this.executeQuery("START TRANSACTION;");
}
// Commit transaction.
async commit() {
await this.executeQuery("COMMIT;");
}
// Rollback transaction.
async rollback() {
await this.executeQuery("ROLLBACK;");
}
async closeConnection() {
if (!this.connection)
return { success: true };
try {
if ("end" in this.connection) {
await this.connection.end(); // MySQL & PostgreSQL close method
}
if (this.config.sshClient) {
this.config.sshClient.end();
}
return { success: true };
}
catch (error) {
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
finally {
this.connection = undefined;
this.config.sshClient = undefined;
this.config.sshStream = undefined;
}
}
async runQuery(queryOrParams) {
const results = [];
const maxAttempts = defaults_1.maxQueryAttempts || 3;
let attempts = 0;
let _error;
const start = new Date();
let end;
let affectedRows = undefined;
const QueryInput = typeof queryOrParams === "string" ? { query: queryOrParams, params: [] } : queryOrParams;
if (!(0, validateQuery_1.isValidSingleQuery)(QueryInput.query)) {
return { start, end: new Date(), duration: 0, success: false, error: "Multiple SQL statements detected. Use transactions instead." };
}
while (attempts < maxAttempts) {
try {
const { rows, affectedRows } = await this.executeQuery(QueryInput);
end = new Date();
return {
start,
end,
duration: end.getTime() - start.getTime(),
affectedRows,
success: true,
results: rows
};
}
catch (error) {
const permanentErrors = await this.getPermanentErrors();
if (permanentErrors.includes(error.code)) {
end = new Date();
return { start, end, duration: end.getTime() - start.getTime(), success: false, error: error.message };
}
attempts++;
if (attempts < maxAttempts) {
await new Promise(res => setTimeout(res, 1000)); // Wait before retry
}
else {
_error = error;
}
}
}
end = new Date();
return { start, end, duration: end.getTime() - start.getTime(), success: false, error: _error?.message };
}
async runTransaction(queriesOrStrings) {
if (!this.connection) {
await this.establishConnection();
}
let results = [];
const maxAttempts = defaults_1.maxQueryAttempts || 3;
let _error;
const start = new Date();
let end;
let totalAffectedRows = 0;
// Convert string queries into QueryInput
const queries = queriesOrStrings.map(query => typeof query === "string" ? { query, params: [] } : query);
try {
await this.startTransaction();
for (const QueryInput of queries) {
if (!QueryInput?.query?.trim())
continue; // Skip empty queries
let attempts = 0;
while (attempts < maxAttempts) {
try {
const { rows, affectedRows } = await this.executeQuery(QueryInput);
results = results.concat(rows);
totalAffectedRows += affectedRows || 0;
break; // Exit retry loop on success
}
catch (error) {
attempts++;
const permanentErrors = await this.getPermanentErrors();
if (permanentErrors.includes(error.code)) {
throw error; // Stop retrying for permanent errors
}
if (attempts >= maxAttempts) {
_error = error;
}
else {
await new Promise(res => setTimeout(res, 1000)); // Wait before retry
}
}
}
if (_error) {
throw _error;
}
}
await this.commit();
end = new Date();
return {
start,
end,
duration: end.getTime() - start.getTime(),
affectedRows: totalAffectedRows || results.length,
success: true,
results
};
}
catch (error) {
console.log(error);
await this.rollback();
end = new Date();
return {
start,
end,
duration: end.getTime() - start.getTime(),
success: false,
error: error.message
};
}
}
async runTransactionsWithConcurrency(queryGroups) {
if (!this.connection) {
await this.establishConnection();
}
const results = [];
let running = [];
const poolSize = this.getMaxConnections();
let index = 0;
const runNext = async () => {
const i = index++;
if (i >= queryGroups.length)
return;
console.log(`🔹 Running transaction for group #${i + 1}`);
try {
const result = await this.runTransaction(queryGroups[i]);
results[i] = result;
}
catch (error) {
results[i] = {
start: new Date(),
end: new Date(),
duration: 0,
success: false,
error: error?.message || "Unknown error"
};
}
// Recursively trigger the next task when one finishes
await runNext();
};
// Start the first N (poolSize) tasks
for (let i = 0; i < Math.min(poolSize, queryGroups.length); i++) {
running.push(runNext());
}
// Wait for all active tasks to complete
await Promise.all(running);
return results;
}
async getTableMetaData(schema, table) {
try {
if (!this.connection)
throw new Error("Database connection not established.");
const existsQueryInput = this.getTableExistsQuery(schema, table);
const exists = await this.runQuery(existsQueryInput);
if (!exists)
return null;
const MetaQueryInput = this.getTableMetaDataQuery(schema, table);
const result = await this.runQuery(MetaQueryInput);
if (!result.success || !result.results) {
throw new Error(`Failed to fetch metadata: ${result.error}`);
}
const parsedMetadata = (0, utilities_1.parseDatabaseMetaData)(result.results, this.getDialectConfig());
// Ensure return type is always MetadataHeader | null
if (!parsedMetadata)
return null;
if (typeof parsedMetadata === "object" && !Array.isArray(parsedMetadata)) {
return parsedMetadata; // Single table case
}
throw new Error("Unexpected metadata format: Multiple tables returned for a single-table query.");
}
catch (error) {
console.error("Error fetching table metadata:", error);
return null;
}
}
get autoSQL() {
return this.autoSQLHandler.autoSQL.bind(this.autoSQLHandler);
}
get autoCreateTable() {
return this.autoSQLHandler.autoCreateTable.bind(this.autoSQLHandler);
}
get autoAlterTable() {
return this.autoSQLHandler.autoAlterTable.bind(this.autoSQLHandler);
}
get fetchTableMetadata() {
return this.autoSQLHandler.fetchTableMetadata.bind(this.autoSQLHandler);
}
get splitTableData() {
return this.autoSQLHandler.splitTableData.bind(this.autoSQLHandler);
}
get autoInsertData() {
return this.autoSQLHandler.autoInsertData.bind(this.autoSQLHandler);
}
get handleMetadata() {
return this.autoSQLHandler.handleMetadata.bind(this.autoSQLHandler);
}
get autoConfigureTable() {
return this.autoSQLHandler.autoConfigureTable.bind(this.autoSQLHandler);
}
}
exports.Database = Database;
const mysql_1 = require("./mysql");
Object.defineProperty(exports, "MySQLDatabase", { enumerable: true, get: function () { return mysql_1.MySQLDatabase; } });
const pgsql_1 = require("./pgsql");
Object.defineProperty(exports, "PostgresDatabase", { enumerable: true, get: function () { return pgsql_1.PostgresDatabase; } });