refactor(core): Move backend config to a separate package (no-changelog) (#9325)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-07-05 11:43:27 +02:00
committed by GitHub
parent 1d5b9836ca
commit c7d4b471c4
48 changed files with 941 additions and 556 deletions

View File

@@ -0,0 +1,25 @@
import { Config, Env, Nested } from '../decorators';
@Config
class CredentialsOverwrite {
/**
* Prefilled data ("overwrite") in credential types. End users cannot view or change this data.
* Format: { CREDENTIAL_NAME: { PARAMETER: VALUE }}
*/
@Env('CREDENTIALS_OVERWRITE_DATA')
readonly data: string = '{}';
/** Internal API endpoint to fetch overwritten credential types from. */
@Env('CREDENTIALS_OVERWRITE_ENDPOINT')
readonly endpoint: string = '';
}
@Config
export class CredentialsConfig {
/** Default name for credentials */
@Env('CREDENTIALS_DEFAULT_NAME')
readonly defaultName: string = 'My credentials';
@Nested
readonly overwrite: CredentialsOverwrite;
}

View File

@@ -0,0 +1,151 @@
import { Config, Env, Nested } from '../decorators';
@Config
class LoggingConfig {
/** Whether database logging is enabled. */
@Env('DB_LOGGING_ENABLED')
readonly enabled: boolean = false;
/**
* Database logging level. Requires `DB_LOGGING_MAX_EXECUTION_TIME` to be higher than `0`.
*/
@Env('DB_LOGGING_OPTIONS')
readonly options: 'query' | 'error' | 'schema' | 'warn' | 'info' | 'log' | 'all' = 'error';
/**
* Only queries that exceed this time (ms) will be logged. Set `0` to disable.
*/
@Env('DB_LOGGING_MAX_EXECUTION_TIME')
readonly maxQueryExecutionTime: number = 0;
}
@Config
class PostgresSSLConfig {
/**
* Whether to enable SSL.
* If `DB_POSTGRESDB_SSL_CA`, `DB_POSTGRESDB_SSL_CERT`, or `DB_POSTGRESDB_SSL_KEY` are defined, `DB_POSTGRESDB_SSL_ENABLED` defaults to `true`.
*/
@Env('DB_POSTGRESDB_SSL_ENABLED')
readonly enabled: boolean = false;
/** SSL certificate authority */
@Env('DB_POSTGRESDB_SSL_CA')
readonly ca: string = '';
/** SSL certificate */
@Env('DB_POSTGRESDB_SSL_CERT')
readonly cert: string = '';
/** SSL key */
@Env('DB_POSTGRESDB_SSL_KEY')
readonly key: string = '';
/** If unauthorized SSL connections should be rejected */
@Env('DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED')
readonly rejectUnauthorized: boolean = true;
}
@Config
class PostgresConfig {
/** Postgres database name */
@Env('DB_POSTGRESDB_DATABASE')
database: string = 'n8n';
/** Postgres database host */
@Env('DB_POSTGRESDB_HOST')
readonly host: string = 'localhost';
/** Postgres database password */
@Env('DB_POSTGRESDB_PASSWORD')
readonly password: string = '';
/** Postgres database port */
@Env('DB_POSTGRESDB_PORT')
readonly port: number = 5432;
/** Postgres database user */
@Env('DB_POSTGRESDB_USER')
readonly user: string = 'postgres';
/** Postgres database schema */
@Env('DB_POSTGRESDB_SCHEMA')
readonly schema: string = 'public';
/** Postgres database pool size */
@Env('DB_POSTGRESDB_POOL_SIZE')
readonly poolSize = 2;
@Nested
readonly ssl: PostgresSSLConfig;
}
@Config
class MysqlConfig {
/** @deprecated MySQL database name */
@Env('DB_MYSQLDB_DATABASE')
database: string = 'n8n';
/** MySQL database host */
@Env('DB_MYSQLDB_HOST')
readonly host: string = 'localhost';
/** MySQL database password */
@Env('DB_MYSQLDB_PASSWORD')
readonly password: string = '';
/** MySQL database port */
@Env('DB_MYSQLDB_PORT')
readonly port: number = 3306;
/** MySQL database user */
@Env('DB_MYSQLDB_USER')
readonly user: string = 'root';
}
@Config
class SqliteConfig {
/** SQLite database file name */
@Env('DB_SQLITE_DATABASE')
readonly database: string = 'database.sqlite';
/** SQLite database pool size. Set to `0` to disable pooling. */
@Env('DB_SQLITE_POOL_SIZE')
readonly poolSize: number = 0;
/**
* Enable SQLite WAL mode.
*/
@Env('DB_SQLITE_ENABLE_WAL')
readonly enableWAL: boolean = this.poolSize > 1;
/**
* Run `VACUUM` on startup to rebuild the database, reducing file size and optimizing indexes.
*
* @warning Long-running blocking operation that will increase startup time.
*/
@Env('DB_SQLITE_VACUUM_ON_STARTUP')
readonly executeVacuumOnStartup: boolean = false;
}
@Config
export class DatabaseConfig {
/** Type of database to use */
@Env('DB_TYPE')
type: 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb' = 'sqlite';
/** Prefix for table names */
@Env('DB_TABLE_PREFIX')
readonly tablePrefix: string = '';
@Nested
readonly logging: LoggingConfig;
@Nested
readonly postgresdb: PostgresConfig;
@Nested
readonly mysqldb: MysqlConfig;
@Nested
readonly sqlite: SqliteConfig;
}

View File

@@ -0,0 +1,78 @@
import { Config, Env, Nested } from '../decorators';
@Config
export class SmtpAuth {
/** SMTP login username */
@Env('N8N_SMTP_USER')
readonly user: string = '';
/** SMTP login password */
@Env('N8N_SMTP_PASS')
readonly pass: string = '';
/** SMTP OAuth Service Client */
@Env('N8N_SMTP_OAUTH_SERVICE_CLIENT')
readonly serviceClient: string = '';
/** SMTP OAuth Private Key */
@Env('N8N_SMTP_OAUTH_PRIVATE_KEY')
readonly privateKey: string = '';
}
@Config
export class SmtpConfig {
/** SMTP server host */
@Env('N8N_SMTP_HOST')
readonly host: string = '';
/** SMTP server port */
@Env('N8N_SMTP_PORT')
readonly port: number = 465;
/** Whether to use SSL for SMTP */
@Env('N8N_SMTP_SSL')
readonly secure: boolean = true;
/** Whether to use STARTTLS for SMTP when SSL is disabled */
@Env('N8N_SMTP_STARTTLS')
readonly startTLS: boolean = true;
/** How to display sender name */
@Env('N8N_SMTP_SENDER')
readonly sender: string = '';
@Nested
readonly auth: SmtpAuth;
}
@Config
export class TemplateConfig {
/** Overrides default HTML template for inviting new people (use full path) */
@Env('N8N_UM_EMAIL_TEMPLATES_INVITE')
readonly invite: string = '';
/** Overrides default HTML template for resetting password (use full path) */
@Env('N8N_UM_EMAIL_TEMPLATES_PWRESET')
readonly passwordReset: string = '';
/** Overrides default HTML template for notifying that a workflow was shared (use full path) */
@Env('N8N_UM_EMAIL_TEMPLATES_WORKFLOW_SHARED')
readonly workflowShared: string = '';
/** Overrides default HTML template for notifying that credentials were shared (use full path) */
@Env('N8N_UM_EMAIL_TEMPLATES_CREDENTIALS_SHARED')
readonly credentialsShared: string = '';
}
@Config
export class EmailConfig {
/** How to send emails */
@Env('N8N_EMAIL_MODE')
readonly mode: '' | 'smtp' = 'smtp';
@Nested
readonly smtp: SmtpConfig;
@Nested
readonly template: TemplateConfig;
}

View File

@@ -0,0 +1,79 @@
import 'reflect-metadata';
import { readFileSync } from 'fs';
import { Container, Service } from 'typedi';
// eslint-disable-next-line @typescript-eslint/ban-types
type Class = Function;
type PropertyKey = string | symbol;
interface PropertyMetadata {
type: unknown;
envName?: string;
}
const globalMetadata = new Map<Class, Map<PropertyKey, PropertyMetadata>>();
export const Config: ClassDecorator = (ConfigClass: Class) => {
const factory = function () {
const config = new (ConfigClass as new () => Record<PropertyKey, unknown>)();
const classMetadata = globalMetadata.get(ConfigClass);
if (!classMetadata) {
// eslint-disable-next-line n8n-local-rules/no-plain-errors
throw new Error('Invalid config class: ' + ConfigClass.name);
}
for (const [key, { type, envName }] of classMetadata) {
if (typeof type === 'function' && globalMetadata.has(type)) {
config[key] = Container.get(type);
} else if (envName) {
let value: unknown = process.env[envName];
// Read the value from a file, if "_FILE" environment variable is defined
const filePath = process.env[`${envName}_FILE`];
if (filePath) {
value = readFileSync(filePath, 'utf8');
}
if (type === Number) {
value = Number(value);
if (isNaN(value as number)) {
// TODO: add a warning
value = undefined;
}
} else if (type === Boolean) {
if (value !== 'true' && value !== 'false') {
// TODO: add a warning
value = undefined;
} else {
value = value === 'true';
}
}
if (value !== undefined) {
config[key] = value;
}
}
}
return config;
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Service({ factory })(ConfigClass);
};
export const Nested: PropertyDecorator = (target: object, key: PropertyKey) => {
const ConfigClass = target.constructor;
const classMetadata = globalMetadata.get(ConfigClass) ?? new Map<PropertyKey, PropertyMetadata>();
const type = Reflect.getMetadata('design:type', target, key) as unknown;
classMetadata.set(key, { type });
globalMetadata.set(ConfigClass, classMetadata);
};
export const Env =
(envName: string): PropertyDecorator =>
(target: object, key: PropertyKey) => {
const ConfigClass = target.constructor;
const classMetadata =
globalMetadata.get(ConfigClass) ?? new Map<PropertyKey, PropertyMetadata>();
const type = Reflect.getMetadata('design:type', target, key) as unknown;
classMetadata.set(key, { type, envName });
globalMetadata.set(ConfigClass, classMetadata);
};

View File

@@ -0,0 +1,22 @@
import { Config, Nested } from './decorators';
import { CredentialsConfig } from './configs/credentials';
import { DatabaseConfig } from './configs/database';
import { EmailConfig } from './configs/email';
@Config
class UserManagementConfig {
@Nested
emails: EmailConfig;
}
@Config
export class GlobalConfig {
@Nested
database: DatabaseConfig;
@Nested
credentials: CredentialsConfig;
@Nested
userManagement: UserManagementConfig;
}