summary refs log tree commit diff
path: root/src/database/Database.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/database/Database.ts')
-rw-r--r--src/database/Database.ts140
1 files changed, 140 insertions, 0 deletions
diff --git a/src/database/Database.ts b/src/database/Database.ts
new file mode 100644

index 00000000..adafeac0 --- /dev/null +++ b/src/database/Database.ts
@@ -0,0 +1,140 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { config } from "dotenv"; +import path from "node:path"; +import { green, red, yellow } from "picocolors"; +import { DataSource } from "typeorm"; +// noinspection ES6PreferShortImport +import { ConfigEntity } from "../util/entities/Config"; +import fs from "node:fs"; +import { ProcessLifecycle } from "../util/util/ProcessLifecycle"; + +// UUID extension option is only supported with postgres +// We want to generate all id's with Snowflakes that's why we have our own BaseEntity class + +export let dbConnection: DataSource | undefined; + +let isHeadlessProcess = false; +// For typeorm cli +if (!process.env) { + isHeadlessProcess = true; + config({ quiet: true }); +} +if (process.argv[1]?.endsWith("scripts/openapi.js")) isHeadlessProcess = true; + +if (!process.env.DATABASE && !isHeadlessProcess) { + console.log( + red( + "DATABASE environment variable not set! Please set it to your database connection string.\n" + "Example for postgres: postgres://user:password@localhost:5432/database", + ), + ); + process.exit(1); +} + +const dbConnectionString = process.env.DATABASE!; +export const DatabaseType = isHeadlessProcess ? "postgres" : dbConnectionString.split(":")[0]?.replace("+srv", ""); +const applyMigrations = process.env.APPLY_DB_MIGRATIONS !== "false"; + +export const DataSourceOptions = isHeadlessProcess + ? (undefined as unknown as DataSource) + : new DataSource({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore type 'string' is not 'sqlite' | 'postgres' | etc etc + type: DatabaseType, + charset: "utf8mb4", + url: process.env.DATABASE, + entities: [path.join(__dirname, "..", "entities", "*.js")], + synchronize: !!process.env.DB_SYNC, + logging: !!process.env.DB_LOGGING, + bigNumberStrings: false, + supportBigNumbers: true, + name: "default", + migrations: applyMigrations ? [path.join(__dirname, "..", "migration", DatabaseType, "*.js")] : [], + }); + +// Gets the existing database connection +export function getDatabase(): DataSource | null { + // if (!dbConnection) throw new Error("Tried to get database before it was initialised"); + if (!dbConnection) return null; + return dbConnection; +} + +// Called once on server start +export async function initDatabase(): Promise<DataSource> { + if (dbConnection) return dbConnection; + + if (!process.env.DB_SYNC) { + const supported = ["postgres"]; + if (!supported.includes(DatabaseType)) { + console.log( + "[Database]" + + red( + ` We don't have migrations for DB type '${DatabaseType}'` + + ` To ignore, set DB_SYNC=true in your env. https://docs.spacebar.chat/setup/server/configuration/env/`, + ), + ); + process.exit(1); + } + } + + console.log(`[Database] ${yellow(`Connecting to ${DatabaseType} db`)}`); + + dbConnection = await DataSourceOptions.initialize(); + + // Crude way of detecting if the migrations table exists. + const dbExists = async () => { + try { + await ConfigEntity.count(); + return true; + } catch (e) { + return false; + } + }; + if (applyMigrations) { + if (!(await dbExists())) { + console.log("[Database] This appears to be a fresh database. Running initial DDL."); + const qr = dbConnection.createQueryRunner(); + const initialPath = path.join(__dirname, "..", "migration", DatabaseType + "-initial.js"); + if (fs.existsSync(initialPath)) { + console.log("[Database] Found initial migration file, running it."); + await new (require(`../migration/${DatabaseType}-initial`).initial0)().up(qr); + } else console.log("[Database] No initial migration file found at '", initialPath, "', skipping."); + await qr.release(); + } + + console.log("[Database] Applying missing migrations, if any.", process.env.APPLY_DB_MIGRATIONS); + await dbConnection.runMigrations(); + } else { + console.log("[Database] Skipping migrations as per config."); + while (!(await dbExists())) { + console.log("[Database] Database does not exist, and we are not running migrations... Waiting 1 seconds..."); + await new Promise((r) => void setTimeout(r, 5000)); + } + } + + ProcessLifecycle.eventEmitter.on("stopped", async () => await closeDatabase()); + + console.log(`[Database] ${green("Connected")}`); + return dbConnection; +} + +export async function closeDatabase() { + if (DataSourceOptions.isInitialized) await DataSourceOptions.destroy(); + if (dbConnection?.isInitialized) await dbConnection?.destroy(); +}