From ab984578299d54e1342a4d1e4a129efa096783a3 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Sun, 14 Jun 2026 13:22:13 +0000 Subject: [PATCH] 1st --- MySQLClient.js | 115 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 8 ++++ secretsLoader.js | 34 ++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 MySQLClient.js create mode 100644 package.json create mode 100644 secretsLoader.js diff --git a/MySQLClient.js b/MySQLClient.js new file mode 100644 index 0000000..6ce7c24 --- /dev/null +++ b/MySQLClient.js @@ -0,0 +1,115 @@ +import mysql from 'mysql2/promise' +import { loadP42Secrets } from './secretsLoader.js' + +export class MySQLClient { + + constructor(db, kal = 0) { + this.db = db + this.lastInsertedId = -1 + this.kalIntervalId = null + if(kal > 0) { + this.kalIntervalId = setInterval(() => { + this.db.query('SELECT 1') + }, 1000 * kal) + } + } + + static #assertValidDbName(name) { + if(typeof(name) !== 'string' || !/^[A-Za-z0-9_]+$/.test(name)) { + throw(new Error(`Invalid MySQL database name: ${name}`)) + } + } + + static resolveDatabases(config = {}) { + loadP42Secrets() + const guiDatabase = process.env.mysql_guiDb || config.guiDatabase || config.database || 'p42GUI' + const simDatabase = process.env.mysql_simDb || config.simDatabase || 'p42SIM' + MySQLClient.#assertValidDbName(guiDatabase) + MySQLClient.#assertValidDbName(simDatabase) + return({ guiDatabase, simDatabase }) + } + + static resolveCredentials(config = {}) { + loadP42Secrets() + const user = process.env.mysql_user + const password = process.env.mysql_pass + if(!user || !password) { + throw(new Error('Missing MySQL credentials: set mysql_user and mysql_pass in environment')) + } + const { guiDatabase } = MySQLClient.resolveDatabases(config) + return({ + socketPath: config.socketPath, + host: config.host, + port: config.port, + user, + password, + database: guiDatabase, + waitForConnections: true, + connectionLimit: config.connectionLimit ?? 5, + queueLimit: 0, + }) + } + + static async createPool(config) { + return(await mysql.createPool(MySQLClient.resolveCredentials(config))) + } + + static async poolExecute(pool, query, values = []) { + const [rows] = await pool.execute(query, values) + return(rows) + } + + async close() { + if(this.db) await this.db.end() + if(this.kalIntervalId) clearInterval(this.kalIntervalId) + this.db = null + } + + escape(x) { + return(mysql.escape(x)) + } + + async execute(query, values = []) { + if(!this.db) return([]) + if(query.search(RegExp('(^|;) *(INSERT|REPLACE) +INTO', 'i')) > -1) this.lastInsertedId = -1 + const [result] = await this.db.execute(query, values) + + if('insertId' in result) { + this.lastInsertedId = result.insertId + } + return(result) + } + + async InsertObjectsStatement(table, rows) { + this.lastInsertedId = -1 + if((!this.db) || (rows.length == 0)) return([]) + const keys = Object.keys(rows[0]).map((key) => `\`${key}\``).join(', ') + const valuesArr = [] + for(const row of rows) { + valuesArr.push(Object.values(row)) + } + const query = `INSERT INTO ${table} (${keys}) VALUES ?` + const [result] = await this.db.query(query, [valuesArr]) + if('insertId' in result) { + this.lastInsertedId = result.insertId + } + return(result) + } + + async ReplaceObjectsStatement(table, rows) { + this.lastInsertedId = -1 + if((!this.db) || (rows.length == 0)) return([]) + const keys = Object.keys(rows[0]).map((key) => `\`${key}\``).join(', ') + const valuesArr = [] + for(const row of rows) { + valuesArr.push(Object.values(row)) + } + const query = `REPLACE INTO ${table} (${keys}) VALUES ?` + const [result] = await this.db.query(query, [valuesArr]) + if('insertId' in result) { + this.lastInsertedId = result.insertId + } + return(result) + } + +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c6a78f1 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "p42modules", + "version": "0.1.0", + "type": "module", + "dependencies": { + "mysql2": "^3.11.0" + } +} diff --git a/secretsLoader.js b/secretsLoader.js new file mode 100644 index 0000000..8ec7b11 --- /dev/null +++ b/secretsLoader.js @@ -0,0 +1,34 @@ +import fs from 'fs' + +const DEFAULT_SECRETS_PATH = '/etc/p42/secrets.env' + +function stripQuotes(value) { + if( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + return(value.slice(1, -1)) + } + return(value) +} + +export function loadP42Secrets(filePath = DEFAULT_SECRETS_PATH) { + if(process.env.mysql_user && process.env.mysql_pass) return(true) + + if(!fs.existsSync(filePath)) return(false) + + const text = fs.readFileSync(filePath, 'utf8') + for(const rawLine of text.split('\n')) { + const line = rawLine.trim() + if(!line || line.startsWith('#')) continue + const eq = line.indexOf('=') + if(eq < 1) continue + const key = line.slice(0, eq).trim() + const value = stripQuotes(line.slice(eq + 1).trim()) + if(key === 'mysql_user' || key === 'mysql_pass') { + process.env[key] = value + } + } + + return(Boolean(process.env.mysql_user && process.env.mysql_pass)) +}