From 0ecc2e21e57b24bc907858e133ea046986485600 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 25 Mar 2026 13:47:01 -0700 Subject: [PATCH] Implemement TimescaleDB class --- server/src/db/TimescaleDB.ts | 105 +++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 server/src/db/TimescaleDB.ts diff --git a/server/src/db/TimescaleDB.ts b/server/src/db/TimescaleDB.ts new file mode 100644 index 000000000..b36c54292 --- /dev/null +++ b/server/src/db/TimescaleDB.ts @@ -0,0 +1,105 @@ +import { IDb } from "@/db/IDb.js"; +import { ILogger } from "@/utils/logger.js"; +import { EnvConfig } from "@/service/system/settingsService.js"; +import { Client, Pool } from "pg"; +import { runTimescaleDBMigrations } from "@/db/migration/timescaledb/index.js"; + +const SERVICE_NAME = "TimescaleDB"; + +class TimescaleDB implements IDb { + private pool: Pool | null = null; + + constructor( + private logger: ILogger, + private envSettings: EnvConfig + ) {} + + private ensureDatabaseExists = async (connectionString: string) => { + const url = new URL(connectionString); + const dbName = url.pathname.slice(1); // Remove leading "/" + + // Connect to the default "postgres" database + url.pathname = "/postgres"; + const client = new Client({ connectionString: url.toString() }); + await client.connect(); + + const result = await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [dbName]); + if (result.rowCount === 0) { + await client.query(`CREATE DATABASE "${dbName}"`); + this.logger.info({ + message: `Created database "${dbName}"`, + service: SERVICE_NAME, + method: "ensureDatabaseExists", + }); + } + await client.end(); + }; + + connect = async () => { + try { + const connectionString = this.envSettings.dbConnectionString || "postgresql://postgres:password@localhost:5432/checkmate"; + + await this.ensureDatabaseExists(connectionString); + + this.pool = new Pool({ connectionString }); + + const client = await this.pool.connect(); + client.release(); + + // Ensure TimescaleDB extension is enabled + await this.pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb;"); + + this.logger.info({ + message: "Connected to TimescaleDB", + service: SERVICE_NAME, + method: "connect", + }); + + await runTimescaleDBMigrations(this.pool, this.logger); + } catch (error: unknown) { + this.logger.error({ + message: error instanceof Error ? error.message : "Unknown error", + service: SERVICE_NAME, + method: "connect", + stack: error instanceof Error ? error.stack : undefined, + }); + throw error; + } + }; + + disconnect = async () => { + try { + if (this.pool === null) { + return; + } + this.logger.info({ + message: "Disconnecting from TimescaleDB", + service: SERVICE_NAME, + method: "disconnect", + }); + await this.pool.end(); + this.pool = null; + this.logger.info({ + message: "Disconnected from TimescaleDB", + service: SERVICE_NAME, + method: "disconnect", + }); + } catch (error: unknown) { + this.logger.error({ + message: error instanceof Error ? error.message : "Unknown error", + service: SERVICE_NAME, + method: "disconnect", + stack: error instanceof Error ? error.stack : undefined, + }); + } + }; + + getPool = (): Pool => { + if (this.pool === null) { + throw new Error("TimescaleDB is not connected. Call connect() first."); + } + return this.pool; + }; +} + +export default TimescaleDB;