From 298a96054e24397c50de40c0908bec20b5369ec3 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Wed, 17 Dec 2025 08:59:52 +0100 Subject: [PATCH] refactor(db): move to core package (#4589) --- apps/nextjs/next.config.ts | 2 +- e2e/shared/e2e-db.ts | 3 +- packages/core/package.json | 11 +- .../core/src/infrastructure/db/constants.ts | 3 + .../src/infrastructure/db/drivers/index.ts | 27 +++++ .../src/infrastructure/db/drivers/mysql.ts | 33 ++++++ .../infrastructure/db/drivers/postgresql.ts | 38 ++++++ .../src/infrastructure/db/drivers/shared.ts | 15 +++ .../src/infrastructure/db/drivers/sqlite.ts | 10 ++ .../{ => core/src/infrastructure}/db/env.ts | 23 ++-- packages/core/src/infrastructure/db/index.ts | 9 ++ .../core/src/infrastructure/db/mapping.ts | 9 ++ packages/db/collection.ts | 8 +- packages/db/configs/mysql.config.ts | 19 +-- packages/db/configs/postgresql.config.ts | 19 +-- packages/db/configs/sqlite.config.ts | 7 +- packages/db/driver.ts | 110 +----------------- packages/db/index.ts | 12 +- packages/db/migrations/custom/run-custom.ts | 4 +- packages/db/migrations/mysql/migrate.ts | 24 +--- packages/db/migrations/postgresql/migrate.ts | 24 +--- packages/db/migrations/run-seed.ts | 4 +- packages/db/migrations/sqlite/migrate.ts | 10 +- packages/db/package.json | 3 +- packages/db/schema/index.ts | 15 ++- packages/db/test/db-mock.ts | 4 +- packages/db/test/mysql-migration.spec.ts | 4 +- packages/db/test/postgresql-migration.spec.ts | 4 +- pnpm-lock.yaml | 18 +++ scripts/run.sh | 3 +- 30 files changed, 258 insertions(+), 217 deletions(-) create mode 100644 packages/core/src/infrastructure/db/constants.ts create mode 100644 packages/core/src/infrastructure/db/drivers/index.ts create mode 100644 packages/core/src/infrastructure/db/drivers/mysql.ts create mode 100644 packages/core/src/infrastructure/db/drivers/postgresql.ts create mode 100644 packages/core/src/infrastructure/db/drivers/shared.ts create mode 100644 packages/core/src/infrastructure/db/drivers/sqlite.ts rename packages/{ => core/src/infrastructure}/db/env.ts (74%) create mode 100644 packages/core/src/infrastructure/db/index.ts create mode 100644 packages/core/src/infrastructure/db/mapping.ts diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts index 54449b397..a3dabe151 100644 --- a/apps/nextjs/next.config.ts +++ b/apps/nextjs/next.config.ts @@ -1,6 +1,6 @@ // Importing env files here to validate on build import "@homarr/auth/env"; -import "@homarr/db/env"; +import "@homarr/core/infrastructure/db/env"; import "@homarr/common/env"; import "@homarr/core/infrastructure/logs/env"; import "@homarr/docker/env"; diff --git a/e2e/shared/e2e-db.ts b/e2e/shared/e2e-db.ts index 0abc87823..ea59efe6f 100644 --- a/e2e/shared/e2e-db.ts +++ b/e2e/shared/e2e-db.ts @@ -5,6 +5,7 @@ import Database from "better-sqlite3"; import { BetterSQLite3Database, drizzle } from "drizzle-orm/better-sqlite3"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { DB_CASING } from "../../packages/core/src/infrastructure/db/constants"; import * as sqliteSchema from "../../packages/db/schema/sqlite"; export const createSqliteDbFileAsync = async () => { @@ -16,7 +17,7 @@ export const createSqliteDbFileAsync = async () => { const connection = new Database(localDbUrl); const db = drizzle(connection, { schema: sqliteSchema, - casing: "snake_case", + casing: DB_CASING, }); await migrate(db, { diff --git a/packages/core/package.json b/packages/core/package.json index f405b7cc9..82adb30d7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,7 +10,10 @@ "./infrastructure/logs": "./src/infrastructure/logs/index.ts", "./infrastructure/logs/constants": "./src/infrastructure/logs/constants.ts", "./infrastructure/logs/env": "./src/infrastructure/logs/env.ts", - "./infrastructure/logs/error": "./src/infrastructure/logs/error.ts" + "./infrastructure/logs/error": "./src/infrastructure/logs/error.ts", + "./infrastructure/db": "./src/infrastructure/db/index.ts", + "./infrastructure/db/env": "./src/infrastructure/db/env.ts", + "./infrastructure/db/constants": "./src/infrastructure/db/constants.ts" }, "typesVersions": { "*": { @@ -28,7 +31,11 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@t3-oss/env-nextjs": "^0.13.8", + "better-sqlite3": "^12.5.0", + "drizzle-orm": "^0.45.1", "ioredis": "5.8.2", + "mysql2": "3.15.3", + "pg": "^8.16.3", "superjson": "2.2.6", "winston": "3.19.0", "zod": "^4.1.13" @@ -37,6 +44,8 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", + "@types/better-sqlite3": "7.6.13", + "@types/pg": "^8.16.0", "eslint": "^9.39.1", "typescript": "^5.9.3" } diff --git a/packages/core/src/infrastructure/db/constants.ts b/packages/core/src/infrastructure/db/constants.ts new file mode 100644 index 000000000..264303126 --- /dev/null +++ b/packages/core/src/infrastructure/db/constants.ts @@ -0,0 +1,3 @@ +import type { Casing } from "drizzle-orm"; + +export const DB_CASING: Casing = "snake_case"; diff --git a/packages/core/src/infrastructure/db/drivers/index.ts b/packages/core/src/infrastructure/db/drivers/index.ts new file mode 100644 index 000000000..0c5449c7e --- /dev/null +++ b/packages/core/src/infrastructure/db/drivers/index.ts @@ -0,0 +1,27 @@ +import { DB_CASING } from "../constants"; +import { createDbMapping } from "../mapping"; +import { createMysqlDb } from "./mysql"; +import { createPostgresDb } from "./postgresql"; +import type { SharedDrizzleConfig } from "./shared"; +import { WinstonDrizzleLogger } from "./shared"; +import { createSqliteDb } from "./sqlite"; + +export type Database> = ReturnType>; + +export const createSharedConfig = >( + schema: TSchema, +): SharedDrizzleConfig => ({ + logger: new WinstonDrizzleLogger(), + casing: DB_CASING, + schema, +}); + +export const createDb = >(schema: TSchema) => { + const config = createSharedConfig(schema); + + return createDbMapping({ + mysql2: () => createMysqlDb(config), + "node-postgres": () => createPostgresDb(config), + "better-sqlite3": () => createSqliteDb(config), + }); +}; diff --git a/packages/core/src/infrastructure/db/drivers/mysql.ts b/packages/core/src/infrastructure/db/drivers/mysql.ts new file mode 100644 index 000000000..d34c4f51b --- /dev/null +++ b/packages/core/src/infrastructure/db/drivers/mysql.ts @@ -0,0 +1,33 @@ +import { drizzle } from "drizzle-orm/mysql2"; +import mysql from "mysql2"; +import type { PoolOptions } from "mysql2"; + +import { dbEnv } from "../env"; +import type { SharedDrizzleConfig } from "./shared"; + +export const createMysqlDb = >(config: SharedDrizzleConfig) => { + const connection = createMysqlDbConnection(); + return drizzle(connection, { + ...config, + mode: "default", + }); +}; + +const createMysqlDbConnection = () => { + const defaultOptions = { + maxIdle: 0, + idleTimeout: 60000, + enableKeepAlive: true, + } satisfies Partial; + + if (!dbEnv.HOST) { + return mysql.createPool({ ...defaultOptions, uri: dbEnv.URL }); + } + + return mysql.createPool({ + ...defaultOptions, + port: dbEnv.PORT, + user: dbEnv.USER, + password: dbEnv.PASSWORD, + }); +}; diff --git a/packages/core/src/infrastructure/db/drivers/postgresql.ts b/packages/core/src/infrastructure/db/drivers/postgresql.ts new file mode 100644 index 000000000..764b1641c --- /dev/null +++ b/packages/core/src/infrastructure/db/drivers/postgresql.ts @@ -0,0 +1,38 @@ +import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres"; +import type { PoolOptions as PostgresPoolOptions } from "pg"; +import { Pool as PostgresPool } from "pg"; + +import { dbEnv } from "../env"; +import type { SharedDrizzleConfig } from "./shared"; + +export const createPostgresDb = >(config: SharedDrizzleConfig) => { + const connection = createPostgresDbConnection(); + return drizzlePostgres({ + ...config, + client: connection, + }); +}; + +const createPostgresDbConnection = () => { + const defaultOptions = { + max: 0, + idleTimeoutMillis: 60000, + allowExitOnIdle: false, + } satisfies Partial; + + if (!dbEnv.HOST) { + return new PostgresPool({ + ...defaultOptions, + connectionString: dbEnv.URL, + }); + } + + return new PostgresPool({ + ...defaultOptions, + host: dbEnv.HOST, + port: dbEnv.PORT, + database: dbEnv.NAME, + user: dbEnv.USER, + password: dbEnv.PASSWORD, + }); +}; diff --git a/packages/core/src/infrastructure/db/drivers/shared.ts b/packages/core/src/infrastructure/db/drivers/shared.ts new file mode 100644 index 000000000..90d5470bd --- /dev/null +++ b/packages/core/src/infrastructure/db/drivers/shared.ts @@ -0,0 +1,15 @@ +import type { DrizzleConfig, Logger } from "drizzle-orm"; + +import { createLogger } from "../../logs"; + +export type SharedDrizzleConfig> = Required< + Pick, "logger" | "casing" | "schema"> +>; + +const logger = createLogger({ module: "db" }); + +export class WinstonDrizzleLogger implements Logger { + logQuery(query: string, _: unknown[]): void { + logger.debug("Executed SQL query", { query }); + } +} diff --git a/packages/core/src/infrastructure/db/drivers/sqlite.ts b/packages/core/src/infrastructure/db/drivers/sqlite.ts new file mode 100644 index 000000000..ce95fe5a0 --- /dev/null +++ b/packages/core/src/infrastructure/db/drivers/sqlite.ts @@ -0,0 +1,10 @@ +import Database from "better-sqlite3"; +import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3"; + +import { dbEnv } from "../env"; +import type { SharedDrizzleConfig } from "./shared"; + +export const createSqliteDb = >(config: SharedDrizzleConfig) => { + const connection = new Database(dbEnv.URL); + return drizzleSqlite(connection, config); +}; diff --git a/packages/db/env.ts b/packages/core/src/infrastructure/db/env.ts similarity index 74% rename from packages/db/env.ts rename to packages/core/src/infrastructure/db/env.ts index 9d8932a7d..a4142ade7 100644 --- a/packages/db/env.ts +++ b/packages/core/src/infrastructure/db/env.ts @@ -1,7 +1,6 @@ import { z } from "zod/v4"; -import { env as commonEnv } from "@homarr/common/env"; -import { createEnv } from "@homarr/core/infrastructure/env"; +import { createEnv, runtimeEnvWithPrefix } from "@homarr/core/infrastructure/env"; const drivers = { betterSqlite3: "better-sqlite3", @@ -15,40 +14,40 @@ const onlyAllowUrl = isDriver(drivers.betterSqlite3); const urlRequired = onlyAllowUrl || !isUsingDbHost; const hostRequired = isUsingDbHost && !onlyAllowUrl; -export const env = createEnv({ +export const dbEnv = createEnv({ /** * Specify your server-side environment variables schema here. This way you can ensure the app isn't * built with invalid env vars. */ server: { - DB_DRIVER: z + DRIVER: z .union([z.literal(drivers.betterSqlite3), z.literal(drivers.mysql2), z.literal(drivers.nodePostgres)], { message: `Invalid database driver, supported are ${Object.keys(drivers).join(", ")}`, }) .default(drivers.betterSqlite3), ...(urlRequired ? { - DB_URL: + URL: // Fallback to the default sqlite file path in production - commonEnv.NODE_ENV === "production" && isDriver("better-sqlite3") + process.env.NODE_ENV === "production" && isDriver("better-sqlite3") ? z.string().default("/appdata/db/db.sqlite") : z.string().nonempty(), } : {}), ...(hostRequired ? { - DB_HOST: z.string(), - DB_PORT: z + HOST: z.string(), + PORT: z .string() .regex(/\d+/) .transform(Number) .refine((number) => number >= 1) .default(isDriver(drivers.mysql2) ? 3306 : 5432), - DB_USER: z.string(), - DB_PASSWORD: z.string(), - DB_NAME: z.string(), + USER: z.string(), + PASSWORD: z.string(), + NAME: z.string(), } : {}), }, - experimental__runtimeEnv: process.env, + runtimeEnv: runtimeEnvWithPrefix("DB_"), }); diff --git a/packages/core/src/infrastructure/db/index.ts b/packages/core/src/infrastructure/db/index.ts new file mode 100644 index 000000000..0cad6180f --- /dev/null +++ b/packages/core/src/infrastructure/db/index.ts @@ -0,0 +1,9 @@ +import { createDbMapping } from "./mapping"; + +export { createDb } from "./drivers"; +export const createSchema = createDbMapping; + +export { createMysqlDb } from "./drivers/mysql"; +export { createSqliteDb } from "./drivers/sqlite"; +export { createPostgresDb } from "./drivers/postgresql"; +export { createSharedConfig as createSharedDbConfig } from "./drivers"; diff --git a/packages/core/src/infrastructure/db/mapping.ts b/packages/core/src/infrastructure/db/mapping.ts new file mode 100644 index 000000000..9c82a30b4 --- /dev/null +++ b/packages/core/src/infrastructure/db/mapping.ts @@ -0,0 +1,9 @@ +import { dbEnv } from "./env"; + +type DbMappingInput = Record unknown>; + +export const createDbMapping = (input: TInput) => { + // The DRIVER can be undefined when validation of env vars is skipped + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return input[dbEnv.DRIVER ?? "better-sqlite3"]() as ReturnType; +}; diff --git a/packages/db/collection.ts b/packages/db/collection.ts index 6a44b44c2..00e5aba18 100644 --- a/packages/db/collection.ts +++ b/packages/db/collection.ts @@ -1,9 +1,9 @@ import type { InferInsertModel } from "drizzle-orm"; import { objectEntries } from "@homarr/common"; +import { dbEnv } from "@homarr/core/infrastructure/db/env"; import type { HomarrDatabase, HomarrDatabaseMysql, HomarrDatabasePostgresql } from "./driver"; -import { env } from "./env"; import * as schema from "./schema"; type TableKey = { @@ -11,11 +11,11 @@ type TableKey = { }[keyof typeof schema]; export function isMysql(): boolean { - return env.DB_DRIVER === "mysql2"; + return dbEnv.DRIVER === "mysql2"; } export function isPostgresql(): boolean { - return env.DB_DRIVER === "node-postgres"; + return dbEnv.DRIVER === "node-postgres"; } export const createDbInsertCollectionForTransaction = ( @@ -66,7 +66,7 @@ export const createDbInsertCollectionWithoutTransaction = { - switch (env.DB_DRIVER) { + switch (dbEnv.DRIVER) { case "mysql2": case "node-postgres": // For mysql2 and node-postgres, we can use the async insertAllAsync method diff --git a/packages/db/configs/mysql.config.ts b/packages/db/configs/mysql.config.ts index 1eb3aa943..8954080e5 100644 --- a/packages/db/configs/mysql.config.ts +++ b/packages/db/configs/mysql.config.ts @@ -1,19 +1,20 @@ import type { Config } from "drizzle-kit"; -import { env } from "../env"; +import { DB_CASING } from "@homarr/core/infrastructure/db/constants"; +import { dbEnv } from "@homarr/core/infrastructure/db/env"; export default { dialect: "mysql", schema: "./schema", - casing: "snake_case", - dbCredentials: env.DB_URL - ? { url: env.DB_URL } + casing: DB_CASING, + dbCredentials: dbEnv.URL + ? { url: dbEnv.URL } : { - host: env.DB_HOST, - user: env.DB_USER, - password: env.DB_PASSWORD, - database: env.DB_NAME, - port: env.DB_PORT, + host: dbEnv.HOST, + port: dbEnv.PORT, + database: dbEnv.NAME, + user: dbEnv.USER, + password: dbEnv.PASSWORD, }, out: "./migrations/mysql", } satisfies Config; diff --git a/packages/db/configs/postgresql.config.ts b/packages/db/configs/postgresql.config.ts index a37115dfa..74cec4a5c 100644 --- a/packages/db/configs/postgresql.config.ts +++ b/packages/db/configs/postgresql.config.ts @@ -1,20 +1,21 @@ import type { Config } from "drizzle-kit"; -import { env } from "../env"; +import { DB_CASING } from "@homarr/core/infrastructure/db/constants"; +import { dbEnv } from "@homarr/core/infrastructure/db/env"; export default { dialect: "postgresql", schema: "./schema", - casing: "snake_case", + casing: DB_CASING, - dbCredentials: env.DB_URL - ? { url: env.DB_URL } + dbCredentials: dbEnv.URL + ? { url: dbEnv.URL } : { - host: env.DB_HOST, - port: env.DB_PORT, - user: env.DB_USER, - password: env.DB_PASSWORD, - database: env.DB_NAME, + host: dbEnv.HOST, + port: dbEnv.PORT, + database: dbEnv.NAME, + user: dbEnv.USER, + password: dbEnv.PASSWORD, }, out: "./migrations/postgresql", } satisfies Config; diff --git a/packages/db/configs/sqlite.config.ts b/packages/db/configs/sqlite.config.ts index 6b38860f2..b731928db 100644 --- a/packages/db/configs/sqlite.config.ts +++ b/packages/db/configs/sqlite.config.ts @@ -1,11 +1,12 @@ import type { Config } from "drizzle-kit"; -import { env } from "../env"; +import { DB_CASING } from "@homarr/core/infrastructure/db/constants"; +import { dbEnv } from "@homarr/core/infrastructure/db/env"; export default { dialect: "sqlite", schema: "./schema", - casing: "snake_case", - dbCredentials: { url: env.DB_URL }, + casing: DB_CASING, + dbCredentials: { url: dbEnv.URL }, out: "./migrations/sqlite", } satisfies Config; diff --git a/packages/db/driver.ts b/packages/db/driver.ts index 7950e1183..6c2f97178 100644 --- a/packages/db/driver.ts +++ b/packages/db/driver.ts @@ -1,115 +1,11 @@ -import type { Database as BetterSqlite3Connection } from "better-sqlite3"; -import Database from "better-sqlite3"; -import type { Logger } from "drizzle-orm"; import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; -import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3"; import type { MySql2Database } from "drizzle-orm/mysql2"; -import { drizzle as drizzleMysql } from "drizzle-orm/mysql2"; import type { NodePgDatabase } from "drizzle-orm/node-postgres"; -import { drizzle as drizzlePg } from "drizzle-orm/node-postgres"; -import type { Pool as MysqlConnectionPool } from "mysql2"; -import mysql from "mysql2"; -import { Pool as PostgresPool } from "pg"; -import { createLogger } from "@homarr/core/infrastructure/logs"; - -import { env } from "./env"; -import * as mysqlSchema from "./schema/mysql"; -import * as pgSchema from "./schema/postgresql"; -import * as sqliteSchema from "./schema/sqlite"; - -const logger = createLogger({ module: "db" }); +import type * as mysqlSchema from "./schema/mysql"; +import type * as pgSchema from "./schema/postgresql"; +import type * as sqliteSchema from "./schema/sqlite"; export type HomarrDatabase = BetterSQLite3Database; export type HomarrDatabaseMysql = MySql2Database; export type HomarrDatabasePostgresql = NodePgDatabase; - -const init = () => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!connection) { - switch (env.DB_DRIVER) { - case "mysql2": - initMySQL2(); - break; - case "node-postgres": - initNodePostgres(); - break; - default: - initBetterSqlite(); - break; - } - } -}; - -export let connection: BetterSqlite3Connection | MysqlConnectionPool | PostgresPool; -export let database: HomarrDatabase; - -class WinstonDrizzleLogger implements Logger { - logQuery(query: string, _: unknown[]): void { - logger.debug("Executed SQL query", { query }); - } -} - -const initBetterSqlite = () => { - connection = new Database(env.DB_URL); - database = drizzleSqlite(connection, { - schema: sqliteSchema, - logger: new WinstonDrizzleLogger(), - casing: "snake_case", - }) as unknown as never; -}; - -const initMySQL2 = () => { - if (!env.DB_HOST) { - connection = mysql.createPool({ uri: env.DB_URL, maxIdle: 0, idleTimeout: 60000, enableKeepAlive: true }); - } else { - connection = mysql.createPool({ - host: env.DB_HOST, - database: env.DB_NAME, - port: env.DB_PORT, - user: env.DB_USER, - password: env.DB_PASSWORD, - maxIdle: 0, - idleTimeout: 60000, - enableKeepAlive: true, - }); - } - - database = drizzleMysql(connection, { - schema: mysqlSchema, - mode: "default", - logger: new WinstonDrizzleLogger(), - casing: "snake_case", - }) as unknown as HomarrDatabase; -}; - -const initNodePostgres = () => { - if (!env.DB_HOST) { - connection = new PostgresPool({ - connectionString: env.DB_URL, - max: 0, - idleTimeoutMillis: 60000, - allowExitOnIdle: false, - }); - } else { - connection = new PostgresPool({ - host: env.DB_HOST, - database: env.DB_NAME, - port: env.DB_PORT, - user: env.DB_USER, - password: env.DB_PASSWORD, - max: 0, - idleTimeoutMillis: 60000, - allowExitOnIdle: false, - }); - } - - database = drizzlePg({ - logger: new WinstonDrizzleLogger(), - schema: pgSchema, - casing: "snake_case", - client: connection, - }) as unknown as HomarrDatabase; -}; - -init(); diff --git a/packages/db/index.ts b/packages/db/index.ts index b796d2fa4..d32e46b34 100644 --- a/packages/db/index.ts +++ b/packages/db/index.ts @@ -1,12 +1,12 @@ -import Database from "better-sqlite3"; +import { createDb } from "@homarr/core/infrastructure/db"; -import { database } from "./driver"; +import { schema } from "./schema"; export * from "drizzle-orm"; - -export const db = database; - -export type Database = typeof db; export type { HomarrDatabaseMysql, HomarrDatabasePostgresql } from "./driver"; +export const db = createDb(schema); + +export type Database = typeof db; + export { handleDiffrentDbDriverOperationsAsync as handleTransactionsAsync } from "./transactions"; diff --git a/packages/db/migrations/custom/run-custom.ts b/packages/db/migrations/custom/run-custom.ts index 659a9d6c3..70bb5d415 100644 --- a/packages/db/migrations/custom/run-custom.ts +++ b/packages/db/migrations/custom/run-custom.ts @@ -1,7 +1,7 @@ import { applyCustomMigrationsAsync } from "."; -import { database } from "../../driver"; +import { db } from "../.."; -applyCustomMigrationsAsync(database) +applyCustomMigrationsAsync(db) .then(() => { console.log("Custom migrations applied successfully"); process.exit(0); diff --git a/packages/db/migrations/mysql/migrate.ts b/packages/db/migrations/mysql/migrate.ts index afca2d7d4..3ad588dd4 100644 --- a/packages/db/migrations/mysql/migrate.ts +++ b/packages/db/migrations/mysql/migrate.ts @@ -1,9 +1,8 @@ -import { drizzle } from "drizzle-orm/mysql2"; import { migrate } from "drizzle-orm/mysql2/migrator"; -import mysql from "mysql2"; + +import { createMysqlDb, createSharedDbConfig } from "@homarr/core/infrastructure/db"; import type { Database } from "../.."; -import { env } from "../../env"; import * as mysqlSchema from "../../schema/mysql"; import { applyCustomMigrationsAsync } from "../custom"; import { seedDataAsync } from "../seed"; @@ -11,23 +10,8 @@ import { seedDataAsync } from "../seed"; const migrationsFolder = process.argv[2] ?? "."; const migrateAsync = async () => { - const mysql2 = mysql.createConnection( - env.DB_URL - ? { uri: env.DB_URL } - : { - host: env.DB_HOST, - database: env.DB_NAME, - port: env.DB_PORT, - user: env.DB_USER, - password: env.DB_PASSWORD, - }, - ); - - const db = drizzle(mysql2, { - mode: "default", - schema: mysqlSchema, - casing: "snake_case", - }); + const config = createSharedDbConfig(mysqlSchema); + const db = createMysqlDb(config); await migrate(db, { migrationsFolder }); await seedDataAsync(db as unknown as Database); diff --git a/packages/db/migrations/postgresql/migrate.ts b/packages/db/migrations/postgresql/migrate.ts index 6e97f7a1b..afdbac1ed 100644 --- a/packages/db/migrations/postgresql/migrate.ts +++ b/packages/db/migrations/postgresql/migrate.ts @@ -1,9 +1,8 @@ -import { drizzle } from "drizzle-orm/node-postgres"; import { migrate } from "drizzle-orm/node-postgres/migrator"; -import { Pool } from "pg"; + +import { createPostgresDb, createSharedDbConfig } from "@homarr/core/infrastructure/db"; import type { Database } from "../.."; -import { env } from "../../env"; import * as pgSchema from "../../schema/postgresql"; import { applyCustomMigrationsAsync } from "../custom"; import { seedDataAsync } from "../seed"; @@ -11,23 +10,8 @@ import { seedDataAsync } from "../seed"; const migrationsFolder = process.argv[2] ?? "."; const migrateAsync = async () => { - const pool = new Pool( - env.DB_URL - ? { connectionString: env.DB_URL } - : { - host: env.DB_HOST, - database: env.DB_NAME, - port: env.DB_PORT, - user: env.DB_USER, - password: env.DB_PASSWORD, - }, - ); - - const db = drizzle({ - schema: pgSchema, - casing: "snake_case", - client: pool, - }); + const config = createSharedDbConfig(pgSchema); + const db = createPostgresDb(config); await migrate(db, { migrationsFolder }); await seedDataAsync(db as unknown as Database); diff --git a/packages/db/migrations/run-seed.ts b/packages/db/migrations/run-seed.ts index 7a0e346bc..8a834189e 100644 --- a/packages/db/migrations/run-seed.ts +++ b/packages/db/migrations/run-seed.ts @@ -1,7 +1,7 @@ -import { database } from "../driver"; +import { db } from ".."; import { seedDataAsync } from "./seed"; -seedDataAsync(database) +seedDataAsync(db) .then(() => { console.log("Seed complete"); process.exit(0); diff --git a/packages/db/migrations/sqlite/migrate.ts b/packages/db/migrations/sqlite/migrate.ts index 90d745746..e9c44d9f7 100644 --- a/packages/db/migrations/sqlite/migrate.ts +++ b/packages/db/migrations/sqlite/migrate.ts @@ -1,8 +1,7 @@ -import Database from "better-sqlite3"; -import { drizzle } from "drizzle-orm/better-sqlite3"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; -import { env } from "../../env"; +import { createSharedDbConfig, createSqliteDb } from "@homarr/core/infrastructure/db"; + import * as sqliteSchema from "../../schema/sqlite"; import { applyCustomMigrationsAsync } from "../custom"; import { seedDataAsync } from "../seed"; @@ -10,9 +9,8 @@ import { seedDataAsync } from "../seed"; const migrationsFolder = process.argv[2] ?? "."; const migrateAsync = async () => { - const sqlite = new Database(env.DB_URL.replace("file:", "")); - - const db = drizzle(sqlite, { schema: sqliteSchema, casing: "snake_case" }); + const config = createSharedDbConfig(sqliteSchema); + const db = createSqliteDb(config); migrate(db, { migrationsFolder }); diff --git a/packages/db/package.json b/packages/db/package.json index 79d8f6b6c..741ce4e3a 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -10,8 +10,7 @@ "./schema": "./schema/index.ts", "./test": "./test/index.ts", "./queries": "./queries/index.ts", - "./validationSchemas": "./validationSchemas.ts", - "./env": "./env.ts" + "./validationSchemas": "./validationSchemas.ts" }, "main": "./index.ts", "types": "./index.ts", diff --git a/packages/db/schema/index.ts b/packages/db/schema/index.ts index 52a1b9cdb..9929ccb86 100644 --- a/packages/db/schema/index.ts +++ b/packages/db/schema/index.ts @@ -1,20 +1,19 @@ import type { InferSelectModel } from "drizzle-orm"; -import { env } from "../env"; +import { createSchema } from "@homarr/core/infrastructure/db"; + import * as mysqlSchema from "./mysql"; import * as pgSchema from "./postgresql"; import * as sqliteSchema from "./sqlite"; export type PostgreSqlSchema = typeof pgSchema; export type MySqlSchema = typeof mysqlSchema; -type Schema = typeof sqliteSchema; -const schema = - env.DB_DRIVER === "mysql2" - ? (mysqlSchema as unknown as Schema) - : env.DB_DRIVER === "node-postgres" - ? (pgSchema as unknown as Schema) - : sqliteSchema; +export const schema = createSchema({ + "better-sqlite3": () => sqliteSchema, + mysql2: () => mysqlSchema, + "node-postgres": () => pgSchema, +}); // Sadly we can't use export * from here as we have multiple possible exports export const { diff --git a/packages/db/test/db-mock.ts b/packages/db/test/db-mock.ts index 176a27534..4001534a6 100644 --- a/packages/db/test/db-mock.ts +++ b/packages/db/test/db-mock.ts @@ -2,11 +2,13 @@ import Database from "better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { DB_CASING } from "@homarr/core/infrastructure/db/constants"; + import * as sqliteSchema from "../schema/sqlite"; export const createDb = (debug?: boolean) => { const sqlite = new Database(":memory:"); - const db = drizzle(sqlite, { schema: sqliteSchema, logger: debug, casing: "snake_case" }); + const db = drizzle(sqlite, { schema: sqliteSchema, logger: debug, casing: DB_CASING }); migrate(db, { migrationsFolder: "./packages/db/migrations/sqlite", }); diff --git a/packages/db/test/mysql-migration.spec.ts b/packages/db/test/mysql-migration.spec.ts index 8b178664b..a235f00fc 100644 --- a/packages/db/test/mysql-migration.spec.ts +++ b/packages/db/test/mysql-migration.spec.ts @@ -5,6 +5,8 @@ import { migrate } from "drizzle-orm/mysql2/migrator"; import mysql from "mysql2"; import { describe, test } from "vitest"; +import { DB_CASING } from "@homarr/core/infrastructure/db/constants"; + import * as mysqlSchema from "../schema/mysql"; describe("Mysql Migration", () => { @@ -22,7 +24,7 @@ describe("Mysql Migration", () => { const database = drizzle(connection, { schema: mysqlSchema, mode: "default", - casing: "snake_case", + casing: DB_CASING, }); // Run migrations and check if it works diff --git a/packages/db/test/postgresql-migration.spec.ts b/packages/db/test/postgresql-migration.spec.ts index 40dd28e46..c7d4ce4ee 100644 --- a/packages/db/test/postgresql-migration.spec.ts +++ b/packages/db/test/postgresql-migration.spec.ts @@ -5,6 +5,8 @@ import { migrate } from "drizzle-orm/node-postgres/migrator"; import { Pool } from "pg"; import { describe, test } from "vitest"; +import { DB_CASING } from "@homarr/core/infrastructure/db/constants"; + import * as pgSchema from "../schema/postgresql"; describe("PostgreSql Migration", () => { @@ -26,7 +28,7 @@ describe("PostgreSql Migration", () => { const database = drizzle({ schema: pgSchema, - casing: "snake_case", + casing: DB_CASING, client: pool, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63bbd743c..61ba928fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -920,9 +920,21 @@ importers: '@t3-oss/env-nextjs': specifier: ^0.13.8 version: 0.13.8(arktype@2.1.20)(typescript@5.9.3)(zod@4.1.13) + better-sqlite3: + specifier: ^12.5.0 + version: 12.5.0 + drizzle-orm: + specifier: ^0.45.1 + version: 0.45.1(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@12.5.0)(gel@2.0.0)(mysql2@3.15.3)(pg@8.16.3) ioredis: specifier: 5.8.2 version: 5.8.2 + mysql2: + specifier: 3.15.3 + version: 3.15.3 + pg: + specifier: ^8.16.3 + version: 8.16.3 superjson: specifier: 2.2.6 version: 2.2.6 @@ -942,6 +954,12 @@ importers: '@homarr/tsconfig': specifier: workspace:^0.1.0 version: link:../../tooling/typescript + '@types/better-sqlite3': + specifier: 7.6.13 + version: 7.6.13 + '@types/pg': + specifier: ^8.16.0 + version: 8.16.0 eslint: specifier: ^9.39.1 version: 9.39.1 diff --git a/scripts/run.sh b/scripts/run.sh index f1220d9eb..78659a04a 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -10,7 +10,8 @@ if [ "$DB_MIGRATIONS_DISABLED" = "true" ]; then echo "DB migrations are disabled, skipping" else echo "Running DB migrations" - node ./db/migrations/$DB_DIALECT/migrate.cjs ./db/migrations/$DB_DIALECT + # We disable redis logs during migration as the redis client is not yet started + DISABLE_REDIS_LOGS=true node ./db/migrations/$DB_DIALECT/migrate.cjs ./db/migrations/$DB_DIALECT fi # Auth secret is generated every time the container starts as it is required, but not used because we don't need JWTs or Mail hashing