ffffng/server/db/database.ts

179 lines
5.5 KiB
TypeScript
Raw Normal View History

import util from "util";
import fs from "graceful-fs";
import glob from "glob";
import path from "path";
import {config} from "../config";
import Logger from "../logger";
2022-07-18 17:49:42 +02:00
import {Database, ISqlite, open, Statement} from "sqlite";
import * as sqlite3 from "sqlite3";
2016-05-20 22:38:13 +02:00
const pglob = util.promisify(glob);
const pReadFile = util.promisify(fs.readFile);
2022-07-18 17:49:42 +02:00
export type RunResult = ISqlite.RunResult;
export type SqlType = ISqlite.SqlType;
2016-05-20 22:11:35 +02:00
2022-07-18 17:49:42 +02:00
export interface TypedDatabase {
/**
* @see Database.on
*/
on(event: string, listener: any): Promise<void>;
2016-05-24 16:40:57 +02:00
2022-07-18 17:49:42 +02:00
/**
* @see Database.run
*/
run(sql: SqlType, ...params: any[]): Promise<RunResult>;
2016-05-24 16:40:57 +02:00
2022-07-18 17:49:42 +02:00
/**
* @see Database.get
*/
get<T>(sql: SqlType, ...params: any[]): Promise<T | undefined>;
2016-05-20 22:11:35 +02:00
2022-07-18 17:49:42 +02:00
/**
* @see Database.each
*/
each<T>(sql: SqlType, callback: (err: any, row: T) => void): Promise<number>;
2016-05-24 16:40:57 +02:00
2022-07-18 17:49:42 +02:00
each<T>(sql: SqlType, param1: any, callback: (err: any, row: T) => void): Promise<number>;
2022-07-18 17:49:42 +02:00
each<T>(sql: SqlType, param1: any, param2: any, callback: (err: any, row: T) => void): Promise<number>;
2022-07-18 17:49:42 +02:00
each<T>(sql: SqlType, param1: any, param2: any, param3: any, callback: (err: any, row: T) => void): Promise<number>;
2022-07-18 17:49:42 +02:00
each<T>(sql: SqlType, ...params: any[]): Promise<number>;
2022-07-18 17:49:42 +02:00
/**
* @see Database.all
*/
all<T = never>(sql: SqlType, ...params: any[]): Promise<T[]>;
2022-07-18 17:49:42 +02:00
/**
* @see Database.exec
*/
exec(sql: SqlType, ...params: any[]): Promise<void>;
2022-07-18 17:49:42 +02:00
/**
* @see Database.prepare
*/
prepare(sql: SqlType, ...params: any[]): Promise<Statement>;
}
/**
2022-07-18 17:49:42 +02:00
* Typesafe database wrapper.
*
* @see Database
*/
2022-07-18 17:49:42 +02:00
class DatabasePromiseWrapper implements TypedDatabase {
private db: Promise<Database>;
constructor() {
this.db = new Promise<Database>((resolve, reject) => {
open({
filename: config.server.databaseFile,
driver: sqlite3.Database,
})
.then(resolve)
.catch(reject);
});
this.db.catch(err => {
Logger.tag('database', 'init').error('Error initializing database: ', err);
process.exit(1);
});
}
2022-07-18 17:49:42 +02:00
async on(event: string, listener: any): Promise<void> {
const db = await this.db;
2022-07-18 17:49:42 +02:00
db.on(event, listener);
}
2022-07-18 17:49:42 +02:00
async run(sql: SqlType, ...params: any[]): Promise<RunResult> {
const db = await this.db;
2022-07-18 17:49:42 +02:00
return db.run(sql, ...params);
}
2022-07-18 17:49:42 +02:00
async get<T>(sql: SqlType, ...params: any[]): Promise<T | undefined> {
const db = await this.db;
2022-07-18 17:49:42 +02:00
return await db.get<T>(sql, ...params);
}
2022-07-18 17:49:42 +02:00
async each<T>(sql: SqlType, callback: (err: any, row: T) => void): Promise<number>;
async each<T>(sql: SqlType, param1: any, callback: (err: any, row: T) => void): Promise<number>;
async each<T>(sql: SqlType, param1: any, param2: any, callback: (err: any, row: T) => void): Promise<number>;
async each<T>(sql: SqlType, param1: any, param2: any, param3: any, callback: (err: any, row: T) => void): Promise<number>;
async each<T>(sql: SqlType, ...params: any[]): Promise<number> {
const db = await this.db;
// @ts-ignore
2022-07-18 17:49:42 +02:00
return await db.each.apply(db, arguments);
}
2022-07-18 17:49:42 +02:00
async all<T>(sql: SqlType, ...params: any[]): Promise<T[]> {
const db = await this.db;
2022-07-18 17:49:42 +02:00
return (await db.all<T[]>(sql, ...params));
}
2022-07-18 17:49:42 +02:00
async exec(sql: SqlType, ...params: any[]): Promise<void> {
const db = await this.db;
2022-07-18 17:49:42 +02:00
return await db.exec(sql, ...params);
}
2022-07-18 17:49:42 +02:00
async prepare(sql: SqlType, ...params: any[]): Promise<Statement> {
const db = await this.db;
2022-07-18 17:49:42 +02:00
return await db.prepare(sql, ...params);
}
2022-07-18 17:49:42 +02:00
}
2022-07-18 17:49:42 +02:00
async function applyPatch(db: TypedDatabase, file: string): Promise<void> {
Logger.tag('database', 'migration').info('Checking if patch need to be applied: %s', file);
const contents = await pReadFile(file);
const version = path.basename(file, '.sql');
const row = await db.get('SELECT * FROM schema_version WHERE version = ?', version);
if (row) {
// patch is already applied. skip!
Logger.tag('database', 'migration').info('Patch already applied, skipping: %s', file);
return
}
2022-07-18 17:49:42 +02:00
const sql = 'BEGIN TRANSACTION;\n' +
contents.toString() + '\n' +
'INSERT INTO schema_version (version) VALUES (\'' + version + '\');\n' +
'END TRANSACTION;';
await db.exec(sql);
Logger.tag('database', 'migration').info('Patch successfully applied: %s', file);
}
async function applyMigrations(db: TypedDatabase): Promise<void> {
Logger.tag('database', 'migration').info('Migrating database...');
const sql = 'BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS schema_version (\n' +
' version VARCHAR(255) PRIMARY KEY ASC,\n' +
' applied_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL\n' +
'); END TRANSACTION;';
await db.exec(sql);
const files = await pglob(__dirname + '/patches/*.sql');
for (const file of files) {
await applyPatch(db, file)
}
2022-07-18 17:49:42 +02:00
}
2022-07-18 17:49:42 +02:00
export const db: TypedDatabase = new DatabasePromiseWrapper();
export async function init(): Promise<void> {
Logger.tag('database').info('Setting up database: %s', config.server.databaseFile);
await db.on('profile', (sql: string, time: number) => Logger.tag('database').profile('[%sms]\t%s', time, sql));
try {
await applyMigrations(db);
} catch (error) {
Logger.tag('database').error('Error migrating database:', error);
throw error;
}
2016-05-20 22:11:35 +02:00
}
2022-07-18 17:49:42 +02:00
export {Statement};