From cb5160c359d2abca4421d6058200934d09a08788 Mon Sep 17 00:00:00 2001 From: Bruno Bernardino Date: Mon, 1 Dec 2025 08:50:14 +0000 Subject: [PATCH] Add SQLite support This adds SQLite support, but a few queries are broken because they're PostgreSQL-exclusive/specific. I don't see enough value (at least for now) in making the queries harder to read (or the code around them) "just" to enable SQLite support, so I'll abandon this for now, but push the code up in case it helps anyone else add this kind of support (and then change the queries for themselves). Related to #49 --- .env.sample | 1 + bewcloud.config.sample.ts | 2 + lib/config.ts | 2 + lib/interfaces/database.ts | 86 +++++++++++++++++++++++++++++++++----- lib/types.ts | 4 ++ migrate-db.ts | 2 +- 6 files changed, 85 insertions(+), 12 deletions(-) diff --git a/.env.sample b/.env.sample index 0a501ae..7cfdd23 100644 --- a/.env.sample +++ b/.env.sample @@ -1,5 +1,6 @@ PORT=8000 +# These POSTGRESQL_* below are only used if the config.core.databaseEngine is 'postgresql' POSTGRESQL_HOST="postgresql" # docker container name or external hostname/IP POSTGRESQL_USER="postgres" POSTGRESQL_PASSWORD="fake" diff --git a/bewcloud.config.sample.ts b/bewcloud.config.sample.ts index a22412e..eb2df42 100644 --- a/bewcloud.config.sample.ts +++ b/bewcloud.config.sample.ts @@ -22,6 +22,8 @@ const config: PartialDeep = { // }, // core: { // enabledApps: ['news', 'notes', 'photos', 'expenses', 'contacts', 'calendar'], // dashboard and files cannot be disabled + // databaseEngine: 'postgresql', // The database engine to use. Currently only 'postgresql' and 'sqlite' are supported. + // sqliteFilePath: '', // The path to the SQLite database file. Only used if databaseEngine is 'sqlite'. // }, // visuals: { // title: 'My own cloud', diff --git a/lib/config.ts b/lib/config.ts index a37ef87..644690a 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -26,6 +26,8 @@ export class AppConfig { }, core: { enabledApps: ['news', 'notes', 'photos', 'expenses', 'contacts', 'calendar'], + databaseEngine: 'postgresql', + sqliteFilePath: '', }, visuals: { title: '', diff --git a/lib/interfaces/database.ts b/lib/interfaces/database.ts index a247f6c..436814d 100644 --- a/lib/interfaces/database.ts +++ b/lib/interfaces/database.ts @@ -1,6 +1,11 @@ import { Client } from 'postgres'; +import { DatabaseSync } from 'node:sqlite'; + import '@std/dotenv/load'; +import { Config } from '/lib/types.ts'; +import { AppConfig } from '/lib/config.ts'; + const POSTGRESQL_HOST = Deno.env.get('POSTGRESQL_HOST') || ''; const POSTGRESQL_USER = Deno.env.get('POSTGRESQL_USER') || ''; const POSTGRESQL_PASSWORD = Deno.env.get('POSTGRESQL_PASSWORD') || ''; @@ -20,8 +25,9 @@ const tls = POSTGRESQL_CAFILE }; export default class Database { - protected db?: Client; - protected throwOnConnectionError?: boolean; + protected db?: Client | DatabaseSync; + private databaseEngine: Config['core']['databaseEngine'] = 'postgresql'; + private throwOnConnectionError?: boolean; constructor( { connectNow = false, throwOnConnectionError = false }: { connectNow?: boolean; throwOnConnectionError?: boolean } = @@ -30,11 +36,43 @@ export default class Database { this.throwOnConnectionError = throwOnConnectionError; if (connectNow) { - this.connectToPostgres(); + this.connectToDatabase(); } } - protected async connectToPostgres() { + private async connectToDatabase() { + if (this.db) { + return this.db; + } + + const config = await AppConfig.getConfig(); + + this.databaseEngine = config.core.databaseEngine; + + if (this.databaseEngine === 'postgresql') { + await this.connectToPostgres(); + } else { + await this.connectToSQLite(); + } + } + + private async disconnectFromDatabase() { + if (!this.db) { + return; + } + + const config = await AppConfig.getConfig(); + + this.databaseEngine = config.core.databaseEngine; + + if (this.databaseEngine === 'postgresql') { + await this.disconnectFromPostgres(); + } else { + this.disconnectFromSQLite(); + } + } + + private async connectToPostgres() { if (this.db) { return this.db; } @@ -88,28 +126,54 @@ export default class Database { } } - protected async disconnectFromPostgres() { + private async connectToSQLite() { + if (this.db) { + return this.db; + } + + const config = await AppConfig.getConfig(); + + const sqliteDatabase = new DatabaseSync(config.core.sqliteFilePath); + + this.db = sqliteDatabase; + } + + private disconnectFromSQLite() { if (!this.db) { return; } - await this.db.end(); + (this.db as DatabaseSync).close(); + } + + private async disconnectFromPostgres() { + if (!this.db) { + return; + } + + await (this.db as Client).end(); this.db = undefined; } public close() { - this.disconnectFromPostgres(); + this.disconnectFromDatabase(); } - public async query(sql: string, args?: any[]) { + public async query(sql: string, args?: any[]): Promise { if (!this.db) { - await this.connectToPostgres(); + await this.connectToDatabase(); } - const result = await this.db!.queryObject(sql, args); + if (this.databaseEngine === 'postgresql') { + const result = await (this.db as Client).queryObject(sql, args); - return result.rows; + return result.rows; + } + + const result = (this.db as DatabaseSync).prepare(sql).all(...(args || [])) as T[]; + + return result; } } diff --git a/lib/types.ts b/lib/types.ts index 2f1b481..388b536 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -190,6 +190,10 @@ export interface Config { core: { /** dashboard and files cannot be disabled */ enabledApps: OptionalApp[]; + /** The database engine to use. Currently only 'postgresql' and 'sqlite' are supported. */ + databaseEngine: 'postgresql' | 'sqlite'; + /** The path to the SQLite database file. Only used if databaseEngine is 'sqlite'. */ + sqliteFilePath: string; }; visuals: { /** An override title of the application. Empty shows the default title. */ diff --git a/migrate-db.ts b/migrate-db.ts index e65fd2c..b44e160 100644 --- a/migrate-db.ts +++ b/migrate-db.ts @@ -72,7 +72,7 @@ async function runMigrations(missingMigrations: string[]): Promise { await db.query(migrationSql); - await db.query(sql`INSERT INTO "public"."bewcloud_migrations" ("name", "executed_at") VALUES ($1, NOW())`, [ + await db.query(sql`INSERT INTO "bewcloud_migrations" ("name", "executed_at") VALUES ($1, NOW())`, [ missingMigration, ]);