You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ass/backend/sql/mysql.ts

183 lines
6.1 KiB

import { AssFile, AssUser, NID, UploadToken } from 'ass';
import mysql, { Pool } from 'mysql2/promise';
import { log } from '../log.js';
import { UserConfig } from '../UserConfig.js';
import { Database, DatabaseTable, DatabaseValue } from './database.js';
export class MySQLDatabase implements Database {
private _pool: Pool;
private _ready: boolean = false;
public get ready() { return this._ready; }
/**
* Quick function for creating a simple JSON table
*/
private _tableManager(mode: 'create' | 'drop', name: string, schema = '( NanoID varchar(255), Data JSON )'): Promise<void> {
return new Promise((resolve, reject) =>
this._pool.query(
mode === 'create'
? `CREATE TABLE ${name} ${schema};`
: `DROP TABLE ${name};`)
.then(() => resolve())
.catch((err) => reject(err)));
}
/**
* validate the mysql config
*/
private _validateConfig(): string | undefined {
// make sure the configuration exists
if (!UserConfig.ready) return 'User configuration not ready';
if (typeof UserConfig.config.database != 'object') return 'MySQL configuration missing';
if (UserConfig.config.database.kind != 'mysql') return 'Database not set to MySQL, but MySQL is in use, something has gone terribly wrong';
if (typeof UserConfig.config.database.options != 'object') return 'MySQL configuration missing';
let mySqlConf = UserConfig.config.database.options;
// Check the MySQL configuration
const checker = (val: string) => val != null && val !== '';
const issue =
!checker(mySqlConf.host) ? 'Missing MySQL Host'
: !checker(mySqlConf.user) ? 'Missing MySQL User'
: !checker(mySqlConf.password) ? 'Missing MySQL Password'
: !checker(mySqlConf.database) ? 'Missing MySQL Database'
// ! Blame VS Code for this weird indentation
: undefined;
return issue;
}
public open() { return Promise.resolve(); }
public close() { return Promise.resolve(); }
/**
* Build the MySQL client and create the tables
*/
public configure(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
// Config check
let configError = this._validateConfig();
if (configError) throw new Error(configError);
// Create the pool
this._pool = mysql.createPool(UserConfig.config.database!.options!);
// Check if the pool is usable
const [rowz, _fields] = await this._pool.query(`SHOW FULL TABLES WHERE Table_Type LIKE 'BASE TABLE';`);
const rows_tableData = rowz as unknown as { [key: string]: string }[];
// Create tables if needed
if (rows_tableData.length === 0) {
log.warn('MySQL', 'Tables do not exist, creating');
await Promise.all([
this._tableManager('create', 'assfiles'),
this._tableManager('create', 'assusers'),
this._tableManager('create', 'asstokens')
]);
log.success('MySQL', 'Tables created').callback(resolve);
} else {
// There's at least one row, do further checks
const tablesExist = { files: false, users: false, tokens: false };
// Check which tables ACTUALLY do exist
for (let row of rows_tableData) {
const table = row[`Tables_in_${UserConfig.config.database!.options!.database}`
] as DatabaseTable;
if (table === 'assfiles') tablesExist.files = true;
if (table === 'assusers') tablesExist.users = true;
if (table === 'asstokens') tablesExist.tokens = true;
// ! Don't use `= table === ''` because this is a loop
}
// Mini-function for creating a one-off table
const createOneTable = async (name: DatabaseTable) => {
log.warn('MySQL', `Table '${name}' missing, creating`);
await this._tableManager('create', name);
log.success('MySQL', `Table '${name}' created`);
}
// Check & create tables
if (!tablesExist.files) await createOneTable('assfiles');
if (!tablesExist.users) await createOneTable('assusers');
if (!tablesExist.users) await createOneTable('asstokens');
// ! temp: drop tables for testing
/* await MySql._tableManager('drop', 'assfiles');
await MySql._tableManager('drop', 'assusers');
log.debug('Table dropped'); */
// Hopefully we are ready
if (tablesExist.files && tablesExist.users)
log.info('MySQL', 'Tables exist, ready').callback(() => {
this._ready = true;
resolve(void 0);
});
else throw new Error('Table(s) missing!');
}
} catch (err) {
log.error('MySQL', 'failed to initialize');
console.error(err);
reject(err);
}
});
}
public put(table: DatabaseTable, key: NID, data: DatabaseValue): Promise<void> {
return new Promise(async (resolve, reject) => {
if (!this._ready) return reject(new Error('MySQL not ready'));
if (await this.get(table, key)) reject(new Error(`${table == 'assfiles' ? 'File' : table == 'assusers' ? 'User' : 'Token'} key ${key} already exists`));
const query = `
INSERT INTO ${table} ( NanoID, Data )
VALUES ('${key}', '${JSON.stringify(data)}');
`;
return this._pool.query(query)
.then(() => resolve(void 0))
.catch((err) => reject(err));
});
}
public get(table: DatabaseTable, key: NID): Promise<DatabaseValue | undefined> {
return new Promise(async (resolve, reject) => {
try {
// Run query
const [rowz, _fields] = await this._pool.query(`SELECT Data FROM ${table} WHERE NanoID = '${key}';`);
// Disgustingly interpret the query results
const rows_tableData = (rowz as unknown as { [key: string]: string }[])[0] as unknown as ({ Data: UploadToken | AssFile | AssUser | undefined });
resolve(rows_tableData?.Data ?? undefined);
} catch (err) {
reject(err);
}
});
}
// todo: unknown if this works
public getAll(table: DatabaseTable): Promise<{ [index: string]: DatabaseValue }> {
return new Promise(async (resolve, reject) => {
try {
// Run query // ! this may not work as expected
const [rowz, _fields] = await this._pool.query(`SELECT Data FROM ${table}`);
// Interpret results this is pain
const rows = (rowz as unknown as { [key: string]: string }[]);
// console.log(rows);
// aaaaaaaaaaaa
resolve({});
} catch (err) {
reject(err);
}
});
}
}