refactor(backend): config validation joi to zod

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2025-03-29 23:11:01 +01:00 committed by Philip Molares
parent 91ebd519a8
commit 748702daf5
37 changed files with 1299 additions and 994 deletions

View file

@ -1,73 +1,92 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import z from 'zod';
import { DatabaseType } from './database-type.enum';
import { buildErrorMessage, parseOptionalNumber } from './utils';
import { parseOptionalNumber } from './utils';
import {
buildErrorMessage,
extractDescriptionFromZodIssue,
} from './zod-error-message';
export interface DatabaseConfig {
username: string;
password: string;
database: string;
host: string;
port: number;
type: DatabaseType;
}
const databaseSchema = Joi.object({
type: Joi.string()
.valid(...Object.values(DatabaseType))
.label('HD_DATABASE_TYPE'),
// This is the database name, except for SQLite,
// where it is the path to the database file.
database: Joi.string().label('HD_DATABASE_NAME'),
username: Joi.when('type', {
is: Joi.invalid(DatabaseType.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}).label('HD_DATABASE_USER'),
password: Joi.when('type', {
is: Joi.invalid(DatabaseType.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}).label('HD_DATABASE_PASS'),
host: Joi.when('type', {
is: Joi.invalid(DatabaseType.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}).label('HD_DATABASE_HOST'),
port: Joi.when('type', {
is: Joi.invalid(DatabaseType.SQLITE),
then: Joi.number(),
otherwise: Joi.optional(),
}).label('HD_DATABASE_PORT'),
const sqliteDbSchema = z.object({
type: z.literal(DatabaseType.SQLITE).describe('HD_DATABASE_TYPE'),
name: z.string().describe('HD_DATABASE_NAME'),
});
const postgresDbSchema = z.object({
type: z.literal(DatabaseType.POSTGRES).describe('HD_DATABASE_TYPE'),
name: z.string().describe('HD_DATABASE_NAME'),
username: z.string().describe('HD_DATABASE_USERNAME'),
password: z.string().describe('HD_DATABASE_PASSWORD'),
host: z.string().describe('HD_DATABASE_HOST'),
port: z
.number()
.positive()
.max(65535)
.default(5432)
.describe('HD_DATABASE_PORT'),
});
const mariaDbSchema = z.object({
type: z.literal(DatabaseType.MARIADB).describe('HD_DATABASE_TYPE'),
name: z.string().describe('HD_DATABASE_NAME'),
username: z.string().describe('HD_DATABASE_USERNAME'),
password: z.string().describe('HD_DATABASE_PASSWORD'),
host: z.string().describe('HD_DATABASE_HOST'),
port: z
.number()
.positive()
.max(65535)
.default(3306)
.describe('HD_DATABASE_PORT'),
});
const mysqlDbSchema = z.object({
type: z.literal(DatabaseType.MYSQL).describe('HD_DATABASE_TYPE'),
name: z.string().describe('HD_DATABASE_NAME'),
username: z.string().describe('HD_DATABASE_USERNAME'),
password: z.string().describe('HD_DATABASE_PASSWORD'),
host: z.string().describe('HD_DATABASE_HOST'),
port: z
.number()
.positive()
.max(65535)
.default(3306)
.describe('HD_DATABASE_PORT'),
});
const dbSchema = z.discriminatedUnion('type', [
sqliteDbSchema,
mariaDbSchema,
mysqlDbSchema,
postgresDbSchema,
]);
export type SqliteDatabaseConfig = z.infer<typeof sqliteDbSchema>;
export type PostgresDatabaseConfig = z.infer<typeof postgresDbSchema>;
export type MariadbDatabaseConfig = z.infer<typeof mariaDbSchema>;
export type MySQLDatabaseConfig = z.infer<typeof mysqlDbSchema>;
export type DatabaseConfig = z.infer<typeof dbSchema>;
export default registerAs('databaseConfig', () => {
const databaseConfig = databaseSchema.validate(
{
type: process.env.HD_DATABASE_TYPE,
username: process.env.HD_DATABASE_USER,
password: process.env.HD_DATABASE_PASS,
database: process.env.HD_DATABASE_NAME,
host: process.env.HD_DATABASE_HOST,
port: parseOptionalNumber(process.env.HD_DATABASE_PORT),
},
{
abortEarly: false,
presence: 'required',
},
);
const databaseConfig = dbSchema.safeParse({
type: process.env.HD_DATABASE_TYPE,
username: process.env.HD_DATABASE_USERNAME,
password: process.env.HD_DATABASE_PASSWORD,
name: process.env.HD_DATABASE_NAME,
host: process.env.HD_DATABASE_HOST,
port: parseOptionalNumber(process.env.HD_DATABASE_PORT),
});
if (databaseConfig.error) {
const errorMessages = databaseConfig.error.details.map(
(detail) => detail.message,
const errorMessages = databaseConfig.error.errors.map((issue) =>
extractDescriptionFromZodIssue(issue, 'HD_DATABASE'),
);
throw new Error(buildErrorMessage(errorMessages));
}
return databaseConfig.value as DatabaseConfig;
return databaseConfig.data;
});