diff --git a/backend/UserConfig.ts b/backend/UserConfig.ts index df4bce5..735088c 100644 --- a/backend/UserConfig.ts +++ b/backend/UserConfig.ts @@ -1,4 +1,4 @@ -import { UserConfiguration, UserConfigTypeChecker } from 'ass'; +import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration } from 'ass'; import fs from 'fs-extra'; import { path } from '@tycrek/joint'; @@ -56,7 +56,10 @@ const Checkers: UserConfigTypeChecker = { host: basicStringChecker, user: basicStringChecker, password: basicStringChecker, - database: basicStringChecker + database: basicStringChecker, + }, + postgres: { + port: (val) => numChecker(val) && val >= 1 && val <= 65535 } }, @@ -111,6 +114,11 @@ export class UserConfig { if (!Checkers.sql.mySql.user(config.database.options.user)) throw new Error('Invalid databse user'); if (!Checkers.sql.mySql.password(config.database.options.password)) throw new Error('Invalid database password'); if (!Checkers.sql.mySql.database(config.database.options.database)) throw new Error('Invalid database'); + if (config.database.kind == 'postgres') { + if (!Checkers.sql.postgres.port((config.database.options as PostgresConfiguration).port)) { + throw new Error("Invalid database port"); + } + } } else throw new Error('Database options missing'); } } diff --git a/backend/sql/postgres.ts b/backend/sql/postgres.ts index 45a3ed0..a92703f 100644 --- a/backend/sql/postgres.ts +++ b/backend/sql/postgres.ts @@ -1,20 +1,144 @@ +import { PostgresConfiguration } from 'ass'; + import { Client } from 'pg'; import { log } from '../log'; import { Database, DatabaseTable, DatabaseValue } from './database'; +import { UserConfig } from '../UserConfig'; +/** + * database adapter for postgresql + */ export class PostgreSQLDatabase implements Database { private _client: Client; - public open(): Promise { return Promise.resolve(); } - public close(): Promise { return Promise.resolve(); } + /** + * validate 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 'PostgreSQL configuration missing'; + if (UserConfig.config.database.kind != "postgres") return 'Database not set to PostgreSQL, but PostgreSQL is in use, something has gone terribly wrong'; + if (typeof UserConfig.config.database.options != 'object') return 'PostgreSQL configuration missing'; + + let config = UserConfig.config.database.options; + + // check the postgres config + const checker = (val: string) => val != null && val !== ''; + const issue = + !checker(config.host) ? 'Missing PostgreSQL Host' + : !checker(config.user) ? 'Missing PostgreSQL User' + : !checker(config.password) ? 'Missing PostgreSQL Password' + : !checker(config.database) ? 'Missing PostgreSQL Database' + // ! Blame VS Code for this weird indentation + : undefined; + + return issue; + + } + + public open(): Promise { + return new Promise(async (resolve, reject) => { + try { + // config check + let configError = this._validateConfig(); + if (configError) throw new Error(configError); + + // grab the config + let config = UserConfig.config.database!.options! as PostgresConfiguration; + + // set up the client + this._client = new Client({ + host: config.host, + port: config.port, + user: config.user, + password: config.password, + database: config.database, + }); + + // connect to the database + log.info('PostgreSQL', `connecting to ${config.host}:${config.port}`); + await this._client.connect(); + log.success('PostgreSQL', 'ok'); + + resolve(); + } catch (err) { + log.error('PostgreSQL', 'failed to connect'); + console.error(err); + reject(err); + } + }); + } + + public close(): Promise { + return new Promise(async (resolve, reject) => { + try { + // gracefully disconnect + await this._client.end(); + + resolve(); + } catch (err) { + log.error('PostgreSQL', 'failed to disconnect'); + console.error(err); + reject(err); + } + }); + } public configure(): Promise { - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { try { + await this._client.query( +`CREATE TABLE IF NOT EXISTS asstables ( + name TEXT PRIMARY KEY, + version INT NOT NULL +);`); + log.info('PostgreSQL', 'checking database'); + + // update tables + let seenRows = new Set(); + let versions = await this._client.query('SELECT * FROM asstables;'); + for (let row of versions.rows) { + seenRows.add(row.name); + } + + const assTableSchema = '(id TEXT PRIMARY KEY, data JSON NOT NULL)' + + // add missing tables + if (!seenRows.has('assfiles')) { + log.warn('PostgreSQL', 'assfiles missing, repairing...') + await this._client.query( + `CREATE TABLE assfiles ${assTableSchema};` + + `INSERT INTO asstables (name, version) VALUES ('assfiles', 1);` + ); + log.success('PostgreSQL', 'ok'); + } + + if (!seenRows.has('assusers')) { + log.warn('PostgreSQL', 'asstokens missing, repairing...') + await this._client.query( + `CREATE TABLE assusers ${assTableSchema};` + + `INSERT INTO asstables (name, version) VALUES ('assusers', 1);` + ); + log.success('PostgreSQL', 'ok'); + } + + if (!seenRows.has('asstokens')) { + log.warn('PostgreSQL', 'asstokens missing, repairing...') + await this._client.query( + `CREATE TABLE asstokens ${assTableSchema};` + + `INSERT INTO asstables (name, version) VALUES ('asstokens', 1);` + ); + log.success('PostgreSQL', 'ok'); + } + + log.success('PostgreSQL', 'database is ok').callback(() => { + resolve(); + }); } catch (err) { - log.error('PostgreSQL', 'failed to initialize'); + log.error('PostgreSQL', 'failed to set up'); console.error(err); reject(err); } @@ -22,14 +146,56 @@ export class PostgreSQLDatabase implements Database { } public put(table: DatabaseTable, key: string, data: DatabaseValue): Promise { - throw new Error("Method not implemented."); + return new Promise(async (resolve, reject) => { + try { + const queries = { + assfiles: 'INSERT INTO assfiles (id, data) VALUES ($1, $2)', + assusers: 'INSERT INTO assusers (id, data) VALUES ($1, $2)', + asstokens: 'INSERT INTO asstokens (id, data) VALUES ($1, $2)' + }; + + let result = await this._client.query(queries[table], [key, data]); + + resolve(); + } catch (err) { + reject(err); + } + }); } public get(table: DatabaseTable, key: string): Promise { - throw new Error("Method not implemented."); + return new Promise(async (resolve, reject) => { + try { + const queries = { + assfiles: 'SELECT data FROM assfiles WHERE id = $1::text', + assusers: 'SELECT data FROM assusers WHERE id = $1::text', + asstokens: 'SELECT data FROM asstokens WHERE id = $1::text' + }; + + let result = await this._client.query(queries[table], [key]); + + resolve(result.rowCount ? result.rows[0].data : void 0); + } catch (err) { + reject(err); + } + }); } public getAll(table: DatabaseTable): Promise<{ [index: string]: DatabaseValue; }> { - throw new Error("Method not implemented."); + return new Promise(async (resolve, reject) => { + try { + const queries = { + assfiles: 'SELECT json_object_agg(id, data) AS stuff FROM assfiles;', + assusers: 'SELECT json_object_agg(id, data) AS stuff FROM assusers;', + asstokens: 'SELECT json_object_agg(id, data) AS stuff FROM asstokens;' + }; + + let result = await this._client.query(queries[table]); + + resolve(result.rowCount ? result.rows[0].stuff : void 0); + } catch (err) { + reject(err); + } + }); } } \ No newline at end of file diff --git a/common/types.d.ts b/common/types.d.ts index 305f5e7..ddc3cd8 100644 --- a/common/types.d.ts +++ b/common/types.d.ts @@ -65,6 +65,7 @@ declare module 'ass' { interface PostgresConfiguration { host: string; + port: number; user: string; password: string; database: string; @@ -129,6 +130,9 @@ declare module 'ass' { password: (val: any) => boolean; database: (val: any) => boolean; } + postgres: { + port: (val: any) => boolean; + } } rateLimit: { endpoint: (val: any) => boolean; diff --git a/frontend/setup.mts b/frontend/setup.mts index ca33b7e..c4372d8 100644 --- a/frontend/setup.mts +++ b/frontend/setup.mts @@ -51,6 +51,7 @@ document.addEventListener('DOMContentLoaded', () => { pgsqlTab: document.querySelector('#pgsql-tab') as SlTab, pgsqlHost: document.querySelector('#pgsql-host') as SlInput, + pgsqlPort: document.querySelector('#pgsql-port') as SlInput, pgsqlUser: document.querySelector('#pgsql-user') as SlInput, pgsqlPassword: document.querySelector('#pgsql-password') as SlInput, pgsqlDatabase: document.querySelector('#pgsql-database') as SlInput, @@ -120,6 +121,7 @@ document.addEventListener('DOMContentLoaded', () => { kind: 'postgres', options: { host: Elements.pgsqlHost.value, + port: parseInt(Elements.pgsqlPort.value), user: Elements.pgsqlUser.value, password: Elements.pgsqlPassword.value, database: Elements.pgsqlDatabase.value diff --git a/views/setup.pug b/views/setup.pug index cd7cb6a..7a86a7b 100644 --- a/views/setup.pug +++ b/views/setup.pug @@ -56,6 +56,8 @@ block content sl-tab-panel(name='pgsql') h3.setup-text-item-title Host sl-input#pgsql-host(type='text' placeholder='postgres.example.com' clearable): sl-icon(slot='prefix' name='fas-server' library='fa') + h3.setup-text-item-title Port + sl-input#pgsql-port(type='number' placeholder='5432' min='1' max='65535' no-spin-buttons clearable): sl-icon(slot='prefix' name='fas-server' library='fa') h3.setup-text-item-title User sl-input#pgsql-user(type='text' placeholder='posgrassql' clearable): sl-icon(slot='prefix' name='fas-user' library='fa') h3.setup-text-item-title Password