2026-02-14 09:53:14 +01:00
import assert from 'node:assert' ;
import BoxError from './boxerror.js' ;
import constants from './constants.js' ;
import debugModule from 'debug' ;
import { execSync } from 'node:child_process' ;
import mysql from 'mysql2/promise' ;
import safe from 'safetydance' ;
import shellModule from './shell.js' ;
2015-07-20 00:09:47 -07:00
2026-02-14 09:53:14 +01:00
const debug = debugModule ( 'box:database' ) ;
const shell = shellModule ( 'database' ) ;
2021-08-27 09:52:24 -07:00
let gConnectionPool = null ;
2015-07-20 00:09:47 -07:00
2019-07-25 16:12:37 -07:00
const gDatabase = {
hostname : '127.0.0.1' ,
2024-06-03 10:35:50 +02:00
username : 'root' ,
2019-07-25 16:12:37 -07:00
password : 'password' ,
port : 3306 ,
2024-06-03 10:35:50 +02:00
name : 'box'
2019-07-25 16:12:37 -07:00
} ;
2026-02-14 16:34:34 +01:00
async function uninitialize ( ) {
if ( ! gConnectionPool ) return ;
await safe ( gConnectionPool . end ( ) , { debug } ) ;
gConnectionPool = null ;
debug ( 'pool closed' ) ;
}
async function query ( ... args ) {
assert . notStrictEqual ( gConnectionPool , null , 'Database connection is already closed' ) ;
const [ error , result ] = await safe ( gConnectionPool . query ( ... args ) ) ; // this is same as getConnection/query/release
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error , { sqlCode : error . code , sqlMessage : error . sqlMessage || null } ) ;
return result [ 0 ] ; // the promise version returns a tuple of [rows, fields]
}
async function transaction ( queries ) {
assert ( Array . isArray ( queries ) ) ;
const [ error , connection ] = await safe ( gConnectionPool . getConnection ( ) ) ;
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error , { sqlCode : error . code , sqlMessage : error . sqlMessage } ) ;
try {
await connection . beginTransaction ( ) ;
const results = [ ] ;
for ( const query of queries ) {
const [ rows /*, fields */ ] = await connection . query ( query . query , query . args ) ;
results . push ( rows ) ;
}
await connection . commit ( ) ;
connection . release ( ) ; // no await!
return results ;
} catch ( error ) {
await safe ( connection . rollback ( ) , { debug } ) ;
connection . release ( ) ; // no await!
throw new BoxError ( BoxError . DATABASE _ERROR , error , { sqlCode : error . code , sqlMessage : error . sqlMessage || null } ) ;
}
}
2021-08-22 17:14:00 -07:00
async function initialize ( ) {
if ( gConnectionPool !== null ) return ;
2015-07-20 00:09:47 -07:00
2019-07-25 16:12:37 -07:00
if ( constants . TEST ) {
// see setupTest script how the mysql-server is run
2025-05-18 14:45:17 +02:00
gDatabase . hostname = execSync ( 'docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server' ) . toString ( ) . trim ( ) ;
2019-07-25 16:12:37 -07:00
}
2020-07-03 13:07:39 -07:00
// https://github.com/mysqljs/mysql#pool-options
2015-07-20 00:09:47 -07:00
gConnectionPool = mysql . createPool ( {
2020-07-03 13:07:39 -07:00
connectionLimit : 5 ,
2021-06-25 16:46:49 -07:00
connectTimeout : 60000 ,
2019-07-25 16:12:37 -07:00
host : gDatabase . hostname ,
user : gDatabase . username ,
password : gDatabase . password ,
port : gDatabase . port ,
database : gDatabase . name ,
2015-07-20 00:09:47 -07:00
multipleStatements : false ,
2020-07-03 13:07:39 -07:00
waitForConnections : true , // getConnection() will wait until a connection is avaiable
2019-03-22 15:12:30 -07:00
ssl : false ,
2025-06-19 10:39:01 +02:00
timezone : 'Z' , // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
2025-06-19 12:31:54 +02:00
charset : 'utf8mb4' ,
jsonStrings : true , // for JSON types, JSONARRAYAGG will return string instead of JSON
2015-07-20 00:09:47 -07:00
} ) ;
2025-06-19 12:31:54 +02:00
// run one time setup commands on new connections. connections are reused and so we cannot use 'acquire' event of the pool
gConnectionPool . on ( 'connection' , async function ( connection ) { // https://github.com/sidorares/node-mysql2/issues/565, 567
const conn = connection . promise ( ) ; // convert PoolConnection to PromisePoolConnection
try {
// await connection.query('SET NAMES utf8mb4 COLLATE utf8mb4_bin');
await conn . query ( 'SET SESSION sql_mode = \'strict_all_tables\'' ) ; // disable type coercion etc
// GROUP_CONCAT has only 1024 default. we use it in the groups API which doesn't support pagination yet
2025-07-28 12:53:27 +02:00
// a crypto.randomUUID is 36 in length. so the value below provides for roughly 10k users
2025-06-19 12:31:54 +02:00
await conn . query ( 'SET SESSION group_concat_max_len = 360000' ) ;
} catch ( error ) {
debug ( ` failed to init new db connection ${ connection . threadId } : ` , error ) ; // only log. we will let the app handle the exception when it calls query()/transaction()
}
2016-08-05 12:37:44 +02:00
} ) ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
async function clear ( ) {
2025-06-11 15:35:00 +02:00
const tables = await query ( 'SELECT table_name FROM information_schema.tables WHERE table_schema = ? AND table_name != ?' , [ 'box' , 'migrations' ] ) ;
const queries = [ { query : 'SET FOREIGN_KEY_CHECKS = 0' } ] ;
for ( const t of tables ) queries . push ( { query : ` TRUNCATE TABLE ${ t . TABLE _NAME } ` } ) ;
queries . push ( { query : 'SET FOREIGN_KEY_CHECKS = 1' } ) ; // this is a session/connection variable, must be reset back
2016-09-20 14:07:39 -07:00
2025-06-10 11:47:05 +02:00
await transaction ( queries ) ;
2015-07-20 00:09:47 -07:00
}
2026-02-08 11:17:27 +01:00
async function runInTransaction ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
const [ error , connection ] = await safe ( gConnectionPool . getConnection ( ) ) ;
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error , { sqlCode : error . code , sqlMessage : error . sqlMessage } ) ;
try {
await connection . beginTransaction ( ) ;
const query = async ( ... args ) => {
const [ error , result ] = await safe ( connection . query ( ... args ) ) ; // this is same as getConnection/query/release
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error , { sqlCode : error . code , sqlMessage : error . sqlMessage || null } ) ;
return result [ 0 ] ; // the promise version returns a tuple of [rows, fields]
} ;
const result = await callback ( query ) ;
await connection . commit ( ) ;
connection . release ( ) ; // no await!
return result ;
} catch ( error ) {
await safe ( connection . rollback ( ) , { debug } ) ;
connection . release ( ) ; // no await!
throw new BoxError ( BoxError . DATABASE _ERROR , error , { sqlCode : error . code , sqlMessage : error . sqlMessage || null } ) ;
}
}
2021-09-16 13:59:03 -07:00
async function importFromFile ( file ) {
2017-11-22 10:57:56 -08:00
assert . strictEqual ( typeof file , 'string' ) ;
2021-08-20 09:19:44 -07:00
const cmd = ` /usr/bin/mysql -h " ${ gDatabase . hostname } " -u ${ gDatabase . username } -p ${ gDatabase . password } ${ gDatabase . name } < ${ file } ` ;
2021-09-16 13:59:03 -07:00
await query ( 'CREATE DATABASE IF NOT EXISTS box' ) ;
2024-10-16 10:25:07 +02:00
const [ error ] = await safe ( shell . bash ( cmd , { } ) ) ;
2021-09-16 13:59:03 -07:00
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error ) ;
2017-11-22 10:57:56 -08:00
}
2017-11-24 15:29:23 -08:00
2021-09-16 13:59:03 -07:00
async function exportToFile ( file ) {
2017-11-24 15:29:23 -08:00
assert . strictEqual ( typeof file , 'string' ) ;
2022-04-27 15:39:49 -07:00
// latest mysqldump enables column stats by default which is not present in 5.7 util
2024-10-15 10:10:15 +02:00
const mysqlDumpHelp = await shell . spawn ( '/usr/bin/mysqldump' , [ '--help' ] , { encoding : 'utf8' } ) ;
2022-04-27 15:39:49 -07:00
const hasColStats = mysqlDumpHelp . includes ( 'column-statistics' ) ;
const colStats = hasColStats ? '--column-statistics=0' : '' ;
2020-01-31 18:01:13 -08:00
2021-09-16 17:00:15 -07:00
const cmd = ` /usr/bin/mysqldump -h " ${ gDatabase . hostname } " -u root -p ${ gDatabase . password } ${ colStats } --single-transaction --routines --triggers ${ gDatabase . name } > " ${ file } " ` ;
2017-11-24 15:29:23 -08:00
2024-10-16 10:25:07 +02:00
const [ error ] = await safe ( shell . bash ( cmd , { } ) ) ;
2021-09-16 13:59:03 -07:00
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error ) ;
2017-11-24 15:29:23 -08:00
}
2026-02-14 15:43:24 +01:00
2026-02-14 16:34:34 +01:00
const _clear = clear ;
2026-02-14 15:43:24 +01:00
export default {
initialize ,
uninitialize ,
query ,
transaction ,
runInTransaction ,
importFromFile ,
exportToFile ,
_clear ,
} ;