refactor(db): move to core package (#4589)

This commit is contained in:
Meier Lukas
2025-12-17 08:59:52 +01:00
committed by GitHub
parent e954ea861c
commit 298a96054e
30 changed files with 258 additions and 217 deletions

View File

@@ -0,0 +1,3 @@
import type { Casing } from "drizzle-orm";
export const DB_CASING: Casing = "snake_case";

View File

@@ -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<TSchema extends Record<string, unknown>> = ReturnType<typeof createSqliteDb<TSchema>>;
export const createSharedConfig = <TSchema extends Record<string, unknown>>(
schema: TSchema,
): SharedDrizzleConfig<TSchema> => ({
logger: new WinstonDrizzleLogger(),
casing: DB_CASING,
schema,
});
export const createDb = <TSchema extends Record<string, unknown>>(schema: TSchema) => {
const config = createSharedConfig(schema);
return createDbMapping({
mysql2: () => createMysqlDb(config),
"node-postgres": () => createPostgresDb(config),
"better-sqlite3": () => createSqliteDb(config),
});
};

View File

@@ -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 = <TSchema extends Record<string, unknown>>(config: SharedDrizzleConfig<TSchema>) => {
const connection = createMysqlDbConnection();
return drizzle<TSchema>(connection, {
...config,
mode: "default",
});
};
const createMysqlDbConnection = () => {
const defaultOptions = {
maxIdle: 0,
idleTimeout: 60000,
enableKeepAlive: true,
} satisfies Partial<PoolOptions>;
if (!dbEnv.HOST) {
return mysql.createPool({ ...defaultOptions, uri: dbEnv.URL });
}
return mysql.createPool({
...defaultOptions,
port: dbEnv.PORT,
user: dbEnv.USER,
password: dbEnv.PASSWORD,
});
};

View File

@@ -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 = <TSchema extends Record<string, unknown>>(config: SharedDrizzleConfig<TSchema>) => {
const connection = createPostgresDbConnection();
return drizzlePostgres({
...config,
client: connection,
});
};
const createPostgresDbConnection = () => {
const defaultOptions = {
max: 0,
idleTimeoutMillis: 60000,
allowExitOnIdle: false,
} satisfies Partial<PostgresPoolOptions>;
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,
});
};

View File

@@ -0,0 +1,15 @@
import type { DrizzleConfig, Logger } from "drizzle-orm";
import { createLogger } from "../../logs";
export type SharedDrizzleConfig<TSchema extends Record<string, unknown>> = Required<
Pick<DrizzleConfig<TSchema>, "logger" | "casing" | "schema">
>;
const logger = createLogger({ module: "db" });
export class WinstonDrizzleLogger implements Logger {
logQuery(query: string, _: unknown[]): void {
logger.debug("Executed SQL query", { query });
}
}

View File

@@ -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 = <TSchema extends Record<string, unknown>>(config: SharedDrizzleConfig<TSchema>) => {
const connection = new Database(dbEnv.URL);
return drizzleSqlite<TSchema>(connection, config);
};

View File

@@ -0,0 +1,53 @@
import { z } from "zod/v4";
import { createEnv, runtimeEnvWithPrefix } from "@homarr/core/infrastructure/env";
const drivers = {
betterSqlite3: "better-sqlite3",
mysql2: "mysql2",
nodePostgres: "node-postgres",
} as const;
const isDriver = (driver: (typeof drivers)[keyof typeof drivers]) => process.env.DB_DRIVER === driver;
const isUsingDbHost = Boolean(process.env.DB_HOST);
const onlyAllowUrl = isDriver(drivers.betterSqlite3);
const urlRequired = onlyAllowUrl || !isUsingDbHost;
const hostRequired = isUsingDbHost && !onlyAllowUrl;
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: {
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
? {
URL:
// Fallback to the default sqlite file path in production
process.env.NODE_ENV === "production" && isDriver("better-sqlite3")
? z.string().default("/appdata/db/db.sqlite")
: z.string().nonempty(),
}
: {}),
...(hostRequired
? {
HOST: z.string(),
PORT: z
.string()
.regex(/\d+/)
.transform(Number)
.refine((number) => number >= 1)
.default(isDriver(drivers.mysql2) ? 3306 : 5432),
USER: z.string(),
PASSWORD: z.string(),
NAME: z.string(),
}
: {}),
},
runtimeEnv: runtimeEnvWithPrefix("DB_"),
});

View File

@@ -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";

View File

@@ -0,0 +1,9 @@
import { dbEnv } from "./env";
type DbMappingInput = Record<typeof dbEnv.DRIVER, () => unknown>;
export const createDbMapping = <TInput extends DbMappingInput>(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<TInput["better-sqlite3"]>;
};