diff --git a/apps/nextjs/next.config.mjs b/apps/nextjs/next.config.mjs index 2467471d8..8c4bd2800 100644 --- a/apps/nextjs/next.config.mjs +++ b/apps/nextjs/next.config.mjs @@ -1,5 +1,6 @@ // Importing env files here to validate on build import "@homarr/auth/env.mjs"; +import "@homarr/db/env.mjs"; import MillionLint from "@million/lint"; import createNextIntlPlugin from "next-intl/plugin"; diff --git a/apps/nextjs/src/env.mjs b/apps/nextjs/src/env.mjs index bdef43eb0..a82ae2dd5 100644 --- a/apps/nextjs/src/env.mjs +++ b/apps/nextjs/src/env.mjs @@ -1,10 +1,6 @@ import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; -const isUsingDbUrl = Boolean(process.env.DB_URL); -const isUsingDbHost = Boolean(process.env.DB_HOST); -const isUsingDbCredentials = process.env.DB_DRIVER === "mysql2"; - export const env = createEnv({ shared: { VERCEL_URL: z @@ -19,21 +15,6 @@ export const env = createEnv({ * built with invalid env vars. */ server: { - DB_DRIVER: z.enum(["better-sqlite3", "mysql2"]).default("better-sqlite3"), - // If the DB_HOST is set, the DB_URL is optional - DB_URL: isUsingDbHost ? z.string().optional() : z.string(), - DB_HOST: isUsingDbUrl ? z.string().optional() : z.string(), - DB_PORT: isUsingDbUrl - ? z.string().regex(/\d+/).transform(Number).optional() - : z - .string() - .regex(/\d+/) - .transform(Number) - .refine((number) => number >= 1) - .default("3306"), - DB_USER: isUsingDbCredentials ? z.string() : z.string().optional(), - DB_PASSWORD: isUsingDbCredentials ? z.string() : z.string().optional(), - DB_NAME: isUsingDbUrl ? z.string().optional() : z.string(), // Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...) DOCKER_HOSTNAMES: z.string().optional(), DOCKER_PORTS: z.number().optional(), @@ -51,13 +32,6 @@ export const env = createEnv({ runtimeEnv: { VERCEL_URL: process.env.VERCEL_URL, PORT: process.env.PORT, - DB_URL: process.env.DB_URL, - DB_HOST: process.env.DB_HOST, - DB_USER: process.env.DB_USER, - DB_PASSWORD: process.env.DB_PASSWORD, - DB_NAME: process.env.DB_NAME, - DB_PORT: process.env.DB_PORT, - DB_DRIVER: process.env.DB_DRIVER, NODE_ENV: process.env.NODE_ENV, DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES, DOCKER_PORTS: process.env.DOCKER_PORTS, diff --git a/packages/db/configs/mysql.config.ts b/packages/db/configs/mysql.config.ts index 95d0ab936..c04425119 100644 --- a/packages/db/configs/mysql.config.ts +++ b/packages/db/configs/mysql.config.ts @@ -1,19 +1,19 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import * as dotenv from "dotenv"; import type { Config } from "drizzle-kit"; -dotenv.config({ path: "../../.env" }); +import { env } from "../env.mjs"; export default { dialect: "mysql", schema: "./schema", casing: "snake_case", - dbCredentials: { - host: process.env.DB_HOST!, - user: process.env.DB_USER!, - password: process.env.DB_PASSWORD!, - database: process.env.DB_NAME!, - port: parseInt(process.env.DB_PORT!), - }, + dbCredentials: env.DB_URL + ? { url: env.DB_URL } + : { + host: env.DB_HOST, + user: env.DB_USER, + password: env.DB_PASSWORD, + database: env.DB_NAME, + port: env.DB_PORT, + }, out: "./migrations/mysql", } satisfies Config; diff --git a/packages/db/configs/sqlite.config.ts b/packages/db/configs/sqlite.config.ts index 8e71be989..052c926bb 100644 --- a/packages/db/configs/sqlite.config.ts +++ b/packages/db/configs/sqlite.config.ts @@ -1,13 +1,11 @@ -import * as dotenv from "dotenv"; import type { Config } from "drizzle-kit"; -dotenv.config({ path: "../../.env" }); +import { env } from "../env.mjs"; export default { dialect: "sqlite", schema: "./schema", casing: "snake_case", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - dbCredentials: { url: process.env.DB_URL! }, + dbCredentials: { url: env.DB_URL }, out: "./migrations/sqlite", } satisfies Config; diff --git a/packages/db/driver.ts b/packages/db/driver.ts index 9eac1531d..8e54f07b5 100644 --- a/packages/db/driver.ts +++ b/packages/db/driver.ts @@ -7,6 +7,7 @@ import mysql from "mysql2"; import { logger } from "@homarr/log"; +import { env } from "./env.mjs"; import * as mysqlSchema from "./schema/mysql"; import * as sqliteSchema from "./schema/sqlite"; @@ -15,7 +16,7 @@ type HomarrDatabase = BetterSQLite3Database; const init = () => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!connection) { - switch (process.env.DB_DRIVER) { + switch (env.DB_DRIVER) { case "mysql2": initMySQL2(); break; @@ -36,7 +37,7 @@ class WinstonDrizzleLogger implements Logger { } const initBetterSqlite = () => { - connection = new Database(process.env.DB_URL); + connection = new Database(env.DB_URL); database = drizzleSqlite(connection, { schema: sqliteSchema, logger: new WinstonDrizzleLogger(), @@ -45,16 +46,15 @@ const initBetterSqlite = () => { }; const initMySQL2 = () => { - if (!process.env.DB_HOST) { - connection = mysql.createConnection({ uri: process.env.DB_URL }); + if (!env.DB_HOST) { + connection = mysql.createConnection({ uri: env.DB_URL }); } else { connection = mysql.createConnection({ - host: process.env.DB_HOST, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - database: process.env.DB_NAME!, - port: Number(process.env.DB_PORT), - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, + host: env.DB_HOST, + database: env.DB_NAME, + port: env.DB_PORT, + user: env.DB_USER, + password: env.DB_PASSWORD, }); } diff --git a/packages/db/env.mjs b/packages/db/env.mjs new file mode 100644 index 000000000..fb5ec2923 --- /dev/null +++ b/packages/db/env.mjs @@ -0,0 +1,60 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +const drivers = { + betterSqlite3: "better-sqlite3", + mysql2: "mysql2", +}; + +const isDriver = (driver) => 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 env = 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 + .union([z.literal(drivers.betterSqlite3), z.literal(drivers.mysql2)], { + message: `Invalid database driver, supported are ${Object.keys(drivers).join(", ")}`, + }) + .default(drivers.betterSqlite3), + ...(urlRequired + ? { + DB_URL: z.string(), + } + : {}), + ...(hostRequired + ? { + DB_HOST: z.string(), + DB_PORT: z + .string() + .regex(/\d+/) + .transform(Number) + .refine((number) => number >= 1) + .default("3306"), + DB_USER: z.string(), + DB_PASSWORD: z.string(), + DB_NAME: z.string(), + } + : {}), + }, + /** + * Destructure all variables from `process.env` to make sure they aren't tree-shaken away. + */ + runtimeEnv: { + DB_DRIVER: process.env.DB_DRIVER, + DB_URL: process.env.DB_URL, + DB_HOST: process.env.DB_HOST, + DB_USER: process.env.DB_USER, + DB_PASSWORD: process.env.DB_PASSWORD, + DB_NAME: process.env.DB_NAME, + DB_PORT: process.env.DB_PORT, + }, + skipValidation: + Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint", +}); diff --git a/packages/db/migrations/mysql/migrate.ts b/packages/db/migrations/mysql/migrate.ts index 61a2401f7..daca71ee7 100644 --- a/packages/db/migrations/mysql/migrate.ts +++ b/packages/db/migrations/mysql/migrate.ts @@ -1,9 +1,9 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { drizzle } from "drizzle-orm/mysql2"; import { migrate } from "drizzle-orm/mysql2/migrator"; import mysql from "mysql2"; import type { Database } from "../.."; +import { env } from "../../env.mjs"; import * as mysqlSchema from "../../schema/mysql"; import { seedDataAsync } from "../seed"; @@ -11,15 +11,15 @@ const migrationsFolder = process.argv[2] ?? "."; const migrateAsync = async () => { const mysql2 = mysql.createConnection( - process.env.DB_HOST - ? { - host: process.env.DB_HOST, - database: process.env.DB_NAME!, - port: Number(process.env.DB_PORT), - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - } - : { uri: process.env.DB_URL }, + 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, { diff --git a/packages/db/migrations/sqlite/migrate.ts b/packages/db/migrations/sqlite/migrate.ts index 217fa1941..803f36215 100644 --- a/packages/db/migrations/sqlite/migrate.ts +++ b/packages/db/migrations/sqlite/migrate.ts @@ -2,13 +2,14 @@ import Database from "better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { env } from "../../env.mjs"; import * as sqliteSchema from "../../schema/sqlite"; import { seedDataAsync } from "../seed"; const migrationsFolder = process.argv[2] ?? "."; const migrateAsync = async () => { - const sqlite = new Database(process.env.DB_URL?.replace("file:", "")); + const sqlite = new Database(env.DB_URL.replace("file:", "")); const db = drizzle(sqlite, { schema: sqliteSchema, casing: "snake_case" }); diff --git a/packages/db/package.json b/packages/db/package.json index 3659823d0..e3db1df95 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -10,7 +10,8 @@ "./schema": "./schema/index.ts", "./test": "./test/index.ts", "./queries": "./queries/index.ts", - "./validationSchemas": "./validationSchemas.ts" + "./validationSchemas": "./validationSchemas.ts", + "./env.mjs": "./env.mjs" }, "main": "./index.ts", "types": "./index.ts", @@ -21,16 +22,16 @@ "clean": "rm -rf .turbo node_modules", "format": "prettier --check . --ignore-path ../../.gitignore", "lint": "eslint", - "migration:mysql:drop": "drizzle-kit drop --config ./configs/mysql.config.ts", - "migration:mysql:generate": "drizzle-kit generate --config ./configs/mysql.config.ts", - "migration:mysql:run": "drizzle-kit migrate --config ./configs/mysql.config.ts && pnpm run seed", - "migration:sqlite:drop": "drizzle-kit drop --config ./configs/sqlite.config.ts", - "migration:sqlite:generate": "drizzle-kit generate --config ./configs/sqlite.config.ts", - "migration:sqlite:run": "drizzle-kit migrate --config ./configs/sqlite.config.ts && pnpm run seed", - "push:mysql": "drizzle-kit push --config ./configs/mysql.config.ts", - "push:sqlite": "drizzle-kit push --config ./configs/sqlite.config.ts", + "migration:mysql:drop": "pnpm with-env drizzle-kit drop --config ./configs/mysql.config.ts", + "migration:mysql:generate": "pnpm with-env drizzle-kit generate --config ./configs/mysql.config.ts", + "migration:mysql:run": "pnpm with-env drizzle-kit migrate --config ./configs/mysql.config.ts && pnpm run seed", + "migration:sqlite:drop": "pnpm with-env drizzle-kit drop --config ./configs/sqlite.config.ts", + "migration:sqlite:generate": "pnpm with-env drizzle-kit generate --config ./configs/sqlite.config.ts", + "migration:sqlite:run": "pnpm with-env drizzle-kit migrate --config ./configs/sqlite.config.ts && pnpm run seed", + "push:mysql": "pnpm with-env drizzle-kit push --config ./configs/mysql.config.ts", + "push:sqlite": "pnpm with-env drizzle-kit push --config ./configs/sqlite.config.ts", "seed": "pnpm with-env tsx ./migrations/run-seed.ts", - "studio": "drizzle-kit studio --config ./configs/sqlite.config.ts", + "studio": "pnpm with-env drizzle-kit studio --config ./configs/sqlite.config.ts", "typecheck": "tsc --noEmit", "with-env": "dotenv -e ../../.env --" }, @@ -42,6 +43,7 @@ "@homarr/log": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", "@paralleldrive/cuid2": "^2.2.2", + "@t3-oss/env-nextjs": "^0.11.1", "@testcontainers/mysql": "^10.16.0", "better-sqlite3": "^11.7.0", "dotenv": "^16.4.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e6d4f582..eee5d15a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -895,6 +895,9 @@ importers: '@paralleldrive/cuid2': specifier: ^2.2.2 version: 2.2.2 + '@t3-oss/env-nextjs': + specifier: ^0.11.1 + version: 0.11.1(typescript@5.7.2)(zod@3.24.1) '@testcontainers/mysql': specifier: ^10.16.0 version: 10.16.0