diff --git a/README.md b/README.md index f59b40c..1b1f574 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ A svelte-kit based alternative to phpMyAdmin with multi DB support # TODO -- Switch to connection pool +- [x] Switch to connection pool # Installation diff --git a/src/lib/db/mssql/database.ts b/src/lib/db/mssql/database.ts index d4b120b..8f7d54e 100644 --- a/src/lib/db/mssql/database.ts +++ b/src/lib/db/mssql/database.ts @@ -1,6 +1,7 @@ import mssql from 'mssql'; import type { Database } from 'src/app'; import { Logger } from '../helper/helper'; +import { poolManager } from '../pool_manager'; const logger = new Logger(); @@ -10,26 +11,17 @@ export async function get_all_dbs_mssql( password: string, port: string ): Promise> { - const sqlConfig = { - user: user, - password: password, - database: 'master', // this is the default database - server: ip, - port: port, - pool: { - max: 1, - min: 0, - idleTimeoutMillis: 30000 - }, - options: { - encrypt: true, // for azure - trustServerCertificate: true // change to true for local dev / self-signed certs - } - }; try { // make sure that any items are correctly URL encoded in the connection string - await mssql.connect(sqlConfig); - const result = await mssql.query`SELECT name FROM master.dbo.sysdatabases`; + const pool = await poolManager.getMssqlPool({ + type: 'mssql', + host: ip, + user: user, + password: password, + port: parseInt(port), + database: 'master' + }); + const result = await pool.query`SELECT name FROM master.dbo.sysdatabases`; return result.recordset.map((x: Database) => x.name); } catch (err) { logger.Error(err); @@ -43,27 +35,16 @@ export async function create_db_mssql( db: string, port: string ) { - - const sqlConfig = { - user: user, - password: password, - database: 'master', // this is the default database - server: ip, - port: port, - pool: { - max: 1, - min: 0, - idleTimeoutMillis: 30000 - }, - options: { - encrypt: true, // for azure - trustServerCertificate: true // change to true for local dev / self-signed certs - } - }; try { - // make sure that any items are correctly URL encoded in the connection string - await mssql.connect(sqlConfig); - await mssql.query('CREATE DATABASE ' + db); + const pool = await poolManager.getMssqlPool({ + type: 'mssql', + host: ip, + user: user, + password: password, + port: parseInt(port), + database: 'master' + }); + await pool.query('CREATE DATABASE ' + db); } catch (err) { logger.Error(err); } diff --git a/src/lib/db/mysql/database.ts b/src/lib/db/mysql/database.ts index 4010be1..574c19c 100644 --- a/src/lib/db/mysql/database.ts +++ b/src/lib/db/mysql/database.ts @@ -1,5 +1,6 @@ import mysql from 'mysql2/promise'; import { Logger } from '../helper/helper'; +import { poolManager } from '../pool_manager'; const logger = new Logger(); export async function get_all_dbs_mysql( @@ -11,19 +12,19 @@ export async function get_all_dbs_mysql( try { if (port == null) port = '3306'; const databases: Array = []; - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, - database: 'sys', password: pass, - port: parseInt(port) + port: parseInt(port), + database: 'sys' }); - const [databases_raw] = await connection.query('SHOW DATABASES;'); // Get all databases - Array.from(databases_raw).forEach((db) => { + const [databases_raw] = await pool.query('SHOW DATABASES;'); // Get all databases + Array.from(databases_raw).forEach((db: any) => { databases.push(db.Database); }); - connection.destroy(); return databases; } catch (error) { logger.Error(error); @@ -39,16 +40,15 @@ export async function create_db_mysql( ) { try { if (port == null) port = '3306'; - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, password: pass, - database: 'sys', // Default database of MySQL, we don't know what db is selected since we want to create a new one - port: parseInt(port) + port: parseInt(port), + database: 'sys' }); - connection.connect(); - await connection.query('CREATE DATABASE ' + db); - connection.destroy(); + await pool.query('CREATE DATABASE ' + db); } catch (error) { logger.Error(error); } diff --git a/src/lib/db/mysql/record.ts b/src/lib/db/mysql/record.ts index 65abbc9..05de970 100644 --- a/src/lib/db/mysql/record.ts +++ b/src/lib/db/mysql/record.ts @@ -1,5 +1,6 @@ import mysql from 'mysql2/promise'; import { Logger } from '../helper/helper'; +import { poolManager } from '../pool_manager'; const logger = new Logger(); export async function records_mysql( @@ -16,7 +17,8 @@ export async function records_mysql( // eslint-disable-next-line @typescript-eslint/no-explicit-any const rows: Array = []; - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, database: db, @@ -25,7 +27,7 @@ export async function records_mysql( }); // get version - const [records, cols_raw] = await connection.query('SELECT * FROM ' + table); + const [records, cols_raw] = await pool.query('SELECT * FROM ' + table); if (records instanceof Array) // Get all records for (let i = 0; i < records.length; i++) { @@ -33,7 +35,6 @@ export async function records_mysql( } Array.from(cols_raw).forEach((col) => cols.push(col.name)); - connection.destroy(); // We need to close the connection to prevent saturation of max connections return { cols: cols, rows: rows, cols_raw: cols_raw }; } catch (error) { logger.Error(error); @@ -50,7 +51,8 @@ export async function struct_mysql( ) { try { if (port == null) port = '3306'; - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, database: db, @@ -59,8 +61,7 @@ export async function struct_mysql( }); // get version - const [fields] = await connection.query('SHOW FIELDS FROM ' + table); - connection.destroy(); // We need to close the connection to prevent saturation of max connections + const [fields] = await pool.query('SHOW FIELDS FROM ' + table); return fields; } catch (error) { logger.Error(error); @@ -77,15 +78,15 @@ export async function add_record_mysql( port: string ) { try { - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, database: db, password: pass, port: parseInt(port) }); - await connection.query('INSERT INTO ' + table + ' SET ?', JSON.parse(records)); - connection.destroy(); // We need to close the connection to prevent saturation of max connections + await pool.query('INSERT INTO ' + table + ' SET ?', JSON.parse(records)); } catch (error) { logger.Error(error); } @@ -101,7 +102,8 @@ export async function delete_record_mysql( ) { try { if (port == null) port = '3306'; - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, database: db, @@ -109,8 +111,7 @@ export async function delete_record_mysql( port: parseInt(port) }); - await connection.query(query); - connection.destroy(); // We need to close the connection to prevent saturation of max connections + await pool.query(query); } catch (error) { logger.Error(error); } @@ -126,7 +127,8 @@ export async function update_record_mysql( ) { try { if (port == null) port = '3306'; - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, database: db, @@ -134,8 +136,7 @@ export async function update_record_mysql( port: parseInt(port) }); - await connection.query(query); - connection.destroy(); // We need to close the connection to prevent saturation of max connections + await pool.query(query); } catch (error) { logger.Error(error); } diff --git a/src/lib/db/mysql/table.ts b/src/lib/db/mysql/table.ts index 8fbacbf..1e87d09 100644 --- a/src/lib/db/mysql/table.ts +++ b/src/lib/db/mysql/table.ts @@ -1,6 +1,7 @@ import mysql from 'mysql2/promise'; import { parse_query } from '../helper/helper'; import { Logger } from '../helper/helper'; +import { poolManager } from '../pool_manager'; const logger = new Logger(); export async function get_all_tables_mysql( @@ -13,7 +14,8 @@ export async function get_all_tables_mysql( try { if (port == null) port = '3306'; const tables: Array = []; // The tables in database - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, database: db, @@ -22,10 +24,9 @@ export async function get_all_tables_mysql( }); // get version - const [tables_raw] = await connection.query('SHOW TABLES;'); + const [tables_raw] = await pool.query('SHOW TABLES;'); - Array.from(tables_raw).forEach((table) => tables.push(Object.values(table)[0])); // Get all tables in a db - connection.destroy(); // We need to close the connection to prevent saturation of max connections + Array.from(tables_raw).forEach((table: any) => tables.push(Object.values(table)[0] as string)); // Get all tables in a db return tables; } catch (error) { logger.Error(error); @@ -43,7 +44,8 @@ export async function create_table_mysql( ) { try { if (port == null) port = '3306'; - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, database: db, @@ -54,8 +56,7 @@ export async function create_table_mysql( fields.forEach((field) => (query += field + ',')); query = query.slice(0, -1) + ''; query += ')'; - await connection.query(query); - connection.destroy(); // We need to close the connection to prevent saturation of max connections + await pool.query(query); } catch (error) { logger.Error(error); } @@ -71,15 +72,15 @@ export async function drop_table_mysql( ) { try { if (port == null) port = '3306'; - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, database: db, password: pass, port: parseInt(port) }); - await connection.query('DROP TABLE ' + table); - connection.destroy(); // We need to close the connection to prevent saturation of max connections + await pool.query('DROP TABLE ' + table); } catch (error) { logger.Error(error); } @@ -96,7 +97,8 @@ export async function delete_field_mysql( ) { try { if (port == null) port = '3306'; - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, database: db, @@ -104,8 +106,7 @@ export async function delete_field_mysql( port: parseInt(port) }); - await connection.query('ALTER TABLE ' + table + ' DROP COLUMN ' + col); // Drop a column in a table - connection.destroy(); // We need to close the connection to prevent saturation of max connections + await pool.query('ALTER TABLE ' + table + ' DROP COLUMN ' + col); // Drop a column in a table } catch (error) { logger.Error(error); } @@ -121,15 +122,15 @@ export async function truncate_table_mysql( ) { try { if (port == null) port = '3306'; - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, database: db, password: pass, port: parseInt(port) }); - await connection.query('TRUNCATE TABLE ' + table); - connection.destroy(); // We need to close the connection to prevent saturation of max connections + await pool.query('TRUNCATE TABLE ' + table); } catch (error) { logger.Error(error); } @@ -150,15 +151,15 @@ export async function search_in_table_mysql( const rows = Object.values(records); let query = parse_query(keys, rows, table); query = query.replace('DELETE FROM', 'SELECT * FROM'); - const connection = await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, database: db, password: pass, port: parseInt(port) }); - const [rows_from_db] = await connection.query(query); - connection.destroy(); // We need to close the connection to prevent saturation of max connections + const [rows_from_db] = await pool.query(query); return rows_from_db; } catch (error) {} } diff --git a/src/lib/db/pool_manager.ts b/src/lib/db/pool_manager.ts new file mode 100644 index 0000000..ff8d178 --- /dev/null +++ b/src/lib/db/pool_manager.ts @@ -0,0 +1,126 @@ +import mysql from 'mysql2/promise'; +import postgres from 'postgres'; +import mssql from 'mssql'; + +type DbType = 'mysql' | 'postgres' | 'mssql'; + +interface PoolKey { + type: DbType; + host: string; + port: number; + user: string; + password?: string; + database?: string; +} + +class PoolManager { + private mysqlPools: Map = new Map(); + private postgresSqls: Map = new Map(); + private mssqlPools: Map = new Map(); + + private getKey(config: PoolKey): string { + // Include password in key because different users might connect to same DB + return `${config.type}|${config.host}|${config.port}|${config.user}|${config.password}|${config.database || ''}`; + } + + public async getMySQLConnection(config: PoolKey): Promise { + const key = this.getKey(config); + let pool = this.mysqlPools.get(key); + + if (!pool) { + pool = mysql.createPool({ + host: config.host, + user: config.user, + password: config.password, + database: config.database, + port: config.port, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 + }); + this.mysqlPools.set(key, pool); + } + + return await pool.getConnection(); + } + + // Helper to get the pool itself if needed, or just use getConnection above + public getMySQLPool(config: PoolKey): mysql.Pool { + const key = this.getKey(config); + let pool = this.mysqlPools.get(key); + + if (!pool) { + pool = mysql.createPool({ + host: config.host, + user: config.user, + password: config.password, + database: config.database, + port: config.port, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 + }); + this.mysqlPools.set(key, pool); + } + return pool; + } + + public getPostgresSql(config: PoolKey): postgres.Sql { + const key = this.getKey(config); + let sql = this.postgresSqls.get(key); + + if (!sql) { + sql = postgres(`postgres://${config.user}:${config.password}@${config.host}:${config.port}/${config.database || 'postgres'}`, { + host: config.host, + port: config.port, + database: config.database || 'postgres', + username: config.user, + password: config.password, + max: 10, // Connection pool size + }); + this.postgresSqls.set(key, sql); + } + return sql; + } + + public async getMssqlPool(config: PoolKey): Promise { + const key = this.getKey(config); + let pool = this.mssqlPools.get(key); + + if (!pool) { + const sqlConfig = { + user: config.user, + password: config.password, + database: config.database || 'master', + server: config.host, + port: config.port, + pool: { + max: 10, + min: 0, + idleTimeoutMillis: 30000 + }, + options: { + encrypt: true, // for azure + trustServerCertificate: true // change to true for local dev / self-signed certs + } + }; + pool = new mssql.ConnectionPool(sqlConfig); + await pool.connect(); + this.mssqlPools.set(key, pool); + + // Handle pool errors + pool.on('error', err => { + console.error('MSSQL Pool Error:', err); + // Maybe remove from map? + }); + } + + if (!pool.connected && !pool.connecting) { + await pool.connect(); + } + + return pool; + } +} + +export const poolManager = new PoolManager(); diff --git a/src/lib/db/postgres/database.ts b/src/lib/db/postgres/database.ts index 5ee86b4..59b18aa 100644 --- a/src/lib/db/postgres/database.ts +++ b/src/lib/db/postgres/database.ts @@ -1,5 +1,6 @@ import postgres from 'postgres'; import { Logger } from '../helper/helper'; +import { poolManager } from '../pool_manager'; const logger = new Logger(); export async function get_all_dbs_postgres( @@ -9,15 +10,15 @@ export async function get_all_dbs_postgres( port: string | undefined ) { if (port == null) throw new Error('Invalid port'); - const sql = postgres(`postgres://${user}:${pass}@${ip}:${port}/postgres`, { + const sql = poolManager.getPostgresSql({ + type: 'postgres', host: ip, + user: user, + password: pass, port: parseInt(port), - database: 'postgres', // default db - username: user, - password: pass + database: 'postgres' }); const databases = await sql`SELECT datname FROM pg_database;`; - sql.end(); return databases.map((db) => db.datname); } @@ -29,13 +30,13 @@ export async function create_db_postgres( db: string ) { if (port == null) throw new Error('Invalid port'); - const sql = postgres(`postgres://${user}:${pass}@${ip}:${port}/postgres`, { + const sql = poolManager.getPostgresSql({ + type: 'postgres', host: ip, + user: user, + password: pass, port: parseInt(port), - database: 'postgres', // default db - username: user, - password: pass + database: 'postgres' }); await sql`CREATE DATABASE ${sql(db)};`; - sql.end(); } diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts index 1bc790a..0e8a648 100644 --- a/src/routes/login/+page.server.ts +++ b/src/routes/login/+page.server.ts @@ -5,6 +5,7 @@ import { login_mssql } from '$lib/db/mssql/login'; import postgres from 'postgres'; import { Logger } from '$lib/db/helper/helper'; import fs from 'fs'; +import { poolManager } from '$lib/db/pool_manager'; const logger = new Logger(); @@ -36,29 +37,41 @@ export const actions = { if (port == null) { port = 3306; // default port } - await mysql.createConnection({ + const pool = poolManager.getMySQLPool({ + type: 'mysql', host: ip, user: user, - database: 'sys', // default db password: pass, - port: port + port: parseInt(port), + database: 'sys' }); + const conn = await pool.getConnection(); + conn.release(); } else if (type == 'MSSQL') { if (port == null) { port = 1433; } - login_mssql(user, pass, ip, port); + await poolManager.getMssqlPool({ + type: 'mssql', + host: ip, + user: user, + password: pass, + port: parseInt(port), + database: 'master' + }); } else if (type == 'PostgreSQL') { if (port == null) { port = 5432; } - postgres(`postgres://${user}:${pass}@${ip}:${port}/postgres`, { + const sql = poolManager.getPostgresSql({ + type: 'postgres', host: ip, - port: port, - database: 'postgres', // default db - username: user, - password: pass + user: user, + password: pass, + port: parseInt(port), + database: 'postgres' }); + await sql`SELECT 1`; } else if(type == "SQLite"){ if(!fs.existsSync(ip)) return { success: false }; }