2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
2021-05-01 11:21:09 -07:00
initialize ,
uninitialize ,
query ,
transaction ,
2015-07-20 00:09:47 -07:00
2021-05-01 11:21:09 -07:00
importFromFile ,
exportToFile ,
2017-11-22 10:57:56 -08:00
2015-07-20 00:09:47 -07:00
_clear : clear
} ;
2021-05-02 21:12:38 -07:00
const assert = require ( 'assert' ) ,
2020-07-03 13:47:56 -07:00
BoxError = require ( './boxerror.js' ) ,
2019-07-25 16:12:37 -07:00
constants = require ( './constants.js' ) ,
2020-06-03 16:14:05 -07:00
debug = require ( 'debug' ) ( 'box:database' ) ,
2025-05-18 14:45:17 +02:00
execSync = require ( 'child_process' ) . execSync ,
2025-06-19 12:31:54 +02:00
mysql = require ( 'mysql2/promise' ) ,
2021-09-16 13:59:03 -07:00
safe = require ( 'safetydance' ) ,
2024-10-14 19:10:31 +02:00
shell = require ( './shell.js' ) ( 'database' ) ;
2015-07-20 00:09:47 -07:00
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
} ;
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
debug ( 'new connection' ) ;
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-22 17:14:00 -07:00
async function uninitialize ( ) {
if ( ! gConnectionPool ) return ;
2015-07-20 00:09:47 -07:00
2025-06-19 12:31:54 +02:00
await safe ( gConnectionPool . end ( ) , { debug } ) ;
2020-07-03 13:07:39 -07:00
gConnectionPool = null ;
2025-06-19 12:31:54 +02:00
debug ( 'pool closed' ) ;
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
}
2025-06-19 12:31:54 +02:00
async function query ( ... args ) {
2025-06-19 10:12:59 +02:00
assert . notStrictEqual ( gConnectionPool , null , 'Database connection is already closed' ) ;
2021-05-02 21:12:38 -07:00
2025-06-19 12:31:54 +02:00
const [ error , result ] = await safe ( gConnectionPool . query ( ... args ) ) ; // this is same as getConnection/query/release
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error , { code : error . code , sqlMessage : error . sqlMessage || null } ) ;
return result [ 0 ] ; // the promise version returns a tuple of [rows, fields]
2020-06-11 09:50:49 -07:00
}
2021-09-03 18:10:04 -07:00
async function transaction ( queries ) {
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( queries ) ) ;
2020-06-03 16:10:25 -07:00
2025-06-19 12:31:54 +02:00
const [ error , connection ] = await safe ( gConnectionPool . getConnection ( ) ) ;
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error , { code : 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 , { code : error . code , sqlMessage : error . sqlMessage || null } ) ;
}
2015-07-20 00:09:47 -07:00
}
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
}