UNPKG

autosql

Version:

An auto-parser of JSON into SQL.

371 lines (370 loc) 14.2 kB
"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; } });