Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/nextjs/next.config.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
3 changes: 2 additions & 1 deletion e2e/shared/e2e-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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, {
Expand Down
11 changes: 10 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
"*": {
Expand All @@ -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"
Expand All @@ -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"
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/infrastructure/db/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Casing } from "drizzle-orm";

export const DB_CASING: Casing = "snake_case";
27 changes: 27 additions & 0 deletions packages/core/src/infrastructure/db/drivers/index.ts
Original file line number Diff line number Diff line change
@@ -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),
});
};
33 changes: 33 additions & 0 deletions packages/core/src/infrastructure/db/drivers/mysql.ts
Original file line number Diff line number Diff line change
@@ -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,
});
};
38 changes: 38 additions & 0 deletions packages/core/src/infrastructure/db/drivers/postgresql.ts
Original file line number Diff line number Diff line change
@@ -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,
});
};
15 changes: 15 additions & 0 deletions packages/core/src/infrastructure/db/drivers/shared.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
10 changes: 10 additions & 0 deletions packages/core/src/infrastructure/db/drivers/sqlite.ts
Original file line number Diff line number Diff line change
@@ -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);
};
23 changes: 11 additions & 12 deletions packages/db/env.ts → packages/core/src/infrastructure/db/env.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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_"),
});
9 changes: 9 additions & 0 deletions packages/core/src/infrastructure/db/index.ts
Original file line number Diff line number Diff line change
@@ -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";
9 changes: 9 additions & 0 deletions packages/core/src/infrastructure/db/mapping.ts
Original file line number Diff line number Diff line change
@@ -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"]>;
};
8 changes: 4 additions & 4 deletions packages/db/collection.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
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 = {
[K in keyof typeof schema]: (typeof schema)[K] extends { _: { brand: "Table" } } ? K : never;
}[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 = <TTableKey extends TableKey>(
Expand Down Expand Up @@ -66,7 +66,7 @@ export const createDbInsertCollectionWithoutTransaction = <TTableKey extends Tab
return {
...collection,
insertAllAsync: async (db: HomarrDatabase) => {
switch (env.DB_DRIVER) {
switch (dbEnv.DRIVER) {
case "mysql2":
case "node-postgres":
// For mysql2 and node-postgres, we can use the async insertAllAsync method
Expand Down
19 changes: 10 additions & 9 deletions packages/db/configs/mysql.config.ts
Original file line number Diff line number Diff line change
@@ -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;
19 changes: 10 additions & 9 deletions packages/db/configs/postgresql.config.ts
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 4 additions & 3 deletions packages/db/configs/sqlite.config.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading