refactor(core): Validate all string union config fields (#14527)

This commit is contained in:
Iván Ovejero
2025-04-11 13:58:07 +02:00
committed by GitHub
parent be627f08a4
commit 9397320af9
13 changed files with 88 additions and 33 deletions

View File

@@ -1,5 +1,11 @@
import { z } from 'zod';
import { Config, Env, Nested } from '../decorators'; import { Config, Env, Nested } from '../decorators';
const samesiteSchema = z.enum(['strict', 'lax', 'none']);
type Samesite = z.infer<typeof samesiteSchema>;
@Config @Config
class CookieConfig { class CookieConfig {
/** This sets the `Secure` flag on n8n auth cookie */ /** This sets the `Secure` flag on n8n auth cookie */
@@ -7,8 +13,8 @@ class CookieConfig {
secure: boolean = true; secure: boolean = true;
/** This sets the `Samesite` flag on n8n auth cookie */ /** This sets the `Samesite` flag on n8n auth cookie */
@Env('N8N_SAMESITE_COOKIE') @Env('N8N_SAMESITE_COOKIE', samesiteSchema)
samesite: 'strict' | 'lax' | 'none' = 'lax'; samesite: Samesite = 'lax';
} }
@Config @Config

View File

@@ -1,5 +1,10 @@
import { z } from 'zod';
import { Config, Env, Nested } from '../decorators'; import { Config, Env, Nested } from '../decorators';
const cacheBackendSchema = z.enum(['memory', 'redis', 'auto']);
type CacheBackend = z.infer<typeof cacheBackendSchema>;
@Config @Config
class MemoryConfig { class MemoryConfig {
/** Max size of memory cache in bytes */ /** Max size of memory cache in bytes */
@@ -25,8 +30,8 @@ class RedisConfig {
@Config @Config
export class CacheConfig { export class CacheConfig {
/** Backend to use for caching. */ /** Backend to use for caching. */
@Env('N8N_CACHE_BACKEND') @Env('N8N_CACHE_BACKEND', cacheBackendSchema)
backend: 'memory' | 'redis' | 'auto' = 'auto'; backend: CacheBackend = 'auto';
@Nested @Nested
memory: MemoryConfig; memory: MemoryConfig;

View File

@@ -1,5 +1,10 @@
import { z } from 'zod';
import { Config, Env, Nested } from '../decorators'; import { Config, Env, Nested } from '../decorators';
const dbLoggingOptionsSchema = z.enum(['query', 'error', 'schema', 'warn', 'info', 'log', 'all']);
type DbLoggingOptions = z.infer<typeof dbLoggingOptionsSchema>;
@Config @Config
class LoggingConfig { class LoggingConfig {
/** Whether database logging is enabled. */ /** Whether database logging is enabled. */
@@ -9,8 +14,8 @@ class LoggingConfig {
/** /**
* Database logging level. Requires `DB_LOGGING_MAX_EXECUTION_TIME` to be higher than `0`. * Database logging level. Requires `DB_LOGGING_MAX_EXECUTION_TIME` to be higher than `0`.
*/ */
@Env('DB_LOGGING_OPTIONS') @Env('DB_LOGGING_OPTIONS', dbLoggingOptionsSchema)
options: 'query' | 'error' | 'schema' | 'warn' | 'info' | 'log' | 'all' = 'error'; options: DbLoggingOptions = 'error';
/** /**
* Only queries that exceed this time (ms) will be logged. Set `0` to disable. * Only queries that exceed this time (ms) will be logged. Set `0` to disable.
@@ -131,11 +136,14 @@ export class SqliteConfig {
executeVacuumOnStartup: boolean = false; executeVacuumOnStartup: boolean = false;
} }
const dbTypeSchema = z.enum(['sqlite', 'mariadb', 'mysqldb', 'postgresdb']);
type DbType = z.infer<typeof dbTypeSchema>;
@Config @Config
export class DatabaseConfig { export class DatabaseConfig {
/** Type of database to use */ /** Type of database to use */
@Env('DB_TYPE') @Env('DB_TYPE', dbTypeSchema)
type: 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb' = 'sqlite'; type: DbType = 'sqlite';
/** Prefix for table names */ /** Prefix for table names */
@Env('DB_TABLE_PREFIX') @Env('DB_TABLE_PREFIX')

View File

@@ -1,3 +1,5 @@
import { z } from 'zod';
import { Config, Env, Nested } from '../decorators'; import { Config, Env, Nested } from '../decorators';
@Config @Config
@@ -15,6 +17,9 @@ class LogWriterConfig {
logBaseName: string = 'n8nEventLog'; logBaseName: string = 'n8nEventLog';
} }
const recoveryModeSchema = z.enum(['simple', 'extensive']);
type RecoveryMode = z.infer<typeof recoveryModeSchema>;
@Config @Config
export class EventBusConfig { export class EventBusConfig {
/** How often (in ms) to check for unsent event messages. Can in rare cases cause a message to be sent twice. `0` to disable */ /** How often (in ms) to check for unsent event messages. Can in rare cases cause a message to be sent twice. `0` to disable */
@@ -26,6 +31,6 @@ export class EventBusConfig {
logWriter: LogWriterConfig; logWriter: LogWriterConfig;
/** Whether to recover execution details after a crash or only mark status executions as crashed. */ /** Whether to recover execution details after a crash or only mark status executions as crashed. */
@Env('N8N_EVENTBUS_RECOVERY_MODE') @Env('N8N_EVENTBUS_RECOVERY_MODE', recoveryModeSchema)
crashRecoveryMode: 'simple' | 'extensive' = 'extensive'; crashRecoveryMode: RecoveryMode = 'extensive';
} }

View File

@@ -1,5 +1,11 @@
import { z } from 'zod';
import { Config, Env, Nested } from '../decorators'; import { Config, Env, Nested } from '../decorators';
const protocolSchema = z.enum(['http', 'https']);
export type Protocol = z.infer<typeof protocolSchema>;
@Config @Config
class S3BucketConfig { class S3BucketConfig {
/** Name of the n8n bucket in S3-compatible external storage */ /** Name of the n8n bucket in S3-compatible external storage */
@@ -28,8 +34,8 @@ export class S3Config {
@Env('N8N_EXTERNAL_STORAGE_S3_HOST') @Env('N8N_EXTERNAL_STORAGE_S3_HOST')
host: string = ''; host: string = '';
@Env('N8N_EXTERNAL_STORAGE_S3_PROTOCOL') @Env('N8N_EXTERNAL_STORAGE_S3_PROTOCOL', protocolSchema)
protocol: 'http' | 'https' = 'https'; protocol: Protocol = 'https';
@Nested @Nested
bucket: S3BucketConfig; bucket: S3BucketConfig;

View File

@@ -1,13 +1,18 @@
import { z } from 'zod';
import { Config, Env } from '../decorators'; import { Config, Env } from '../decorators';
const releaseChannelSchema = z.enum(['stable', 'beta', 'nightly', 'dev']);
type ReleaseChannel = z.infer<typeof releaseChannelSchema>;
@Config @Config
export class GenericConfig { export class GenericConfig {
/** Default timezone for the n8n instance. Can be overridden on a per-workflow basis. */ /** Default timezone for the n8n instance. Can be overridden on a per-workflow basis. */
@Env('GENERIC_TIMEZONE') @Env('GENERIC_TIMEZONE')
timezone: string = 'America/New_York'; timezone: string = 'America/New_York';
@Env('N8N_RELEASE_TYPE') @Env('N8N_RELEASE_TYPE', releaseChannelSchema)
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev'; releaseChannel: ReleaseChannel = 'dev';
/** Grace period (in seconds) to wait for components to shut down before process exit. */ /** Grace period (in seconds) to wait for components to shut down before process exit. */
@Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT') @Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT')

View File

@@ -1,4 +1,6 @@
import { CommaSeperatedStringArray } from '../custom-types'; import { z } from 'zod';
import { CommaSeparatedStringArray } from '../custom-types';
import { Config, Env, Nested } from '../decorators'; import { Config, Env, Nested } from '../decorators';
/** Scopes (areas of functionality) to filter logs by. */ /** Scopes (areas of functionality) to filter logs by. */
@@ -41,6 +43,9 @@ class FileLoggingConfig {
location: string = 'logs/n8n.log'; location: string = 'logs/n8n.log';
} }
const logLevelSchema = z.enum(['error', 'warn', 'info', 'debug', 'silent']);
type LogLevel = z.infer<typeof logLevelSchema>;
@Config @Config
export class LoggingConfig { export class LoggingConfig {
/** /**
@@ -49,8 +54,8 @@ export class LoggingConfig {
* *
* @example `N8N_LOG_LEVEL=info` will output `error`, `warn` and `info` logs, but not `debug`. * @example `N8N_LOG_LEVEL=info` will output `error`, `warn` and `info` logs, but not `debug`.
*/ */
@Env('N8N_LOG_LEVEL') @Env('N8N_LOG_LEVEL', logLevelSchema)
level: 'error' | 'warn' | 'info' | 'debug' | 'silent' = 'info'; level: LogLevel = 'info';
/** /**
* Where to output logs to. Options are: `console` or `file` or both in a comma separated list. * Where to output logs to. Options are: `console` or `file` or both in a comma separated list.
@@ -58,7 +63,7 @@ export class LoggingConfig {
* @example `N8N_LOG_OUTPUT=console,file` will output to both console and file. * @example `N8N_LOG_OUTPUT=console,file` will output to both console and file.
*/ */
@Env('N8N_LOG_OUTPUT') @Env('N8N_LOG_OUTPUT')
outputs: CommaSeperatedStringArray<'console' | 'file'> = ['console']; outputs: CommaSeparatedStringArray<'console' | 'file'> = ['console'];
@Nested @Nested
file: FileLoggingConfig; file: FileLoggingConfig;
@@ -85,5 +90,5 @@ export class LoggingConfig {
* `N8N_LOG_SCOPES=license,waiting-executions` * `N8N_LOG_SCOPES=license,waiting-executions`
*/ */
@Env('N8N_LOG_SCOPES') @Env('N8N_LOG_SCOPES')
scopes: CommaSeperatedStringArray<LogScope> = []; scopes: CommaSeparatedStringArray<LogScope> = [];
} }

View File

@@ -1,3 +1,5 @@
import { z } from 'zod';
import { Config, Env, Nested } from '../decorators'; import { Config, Env, Nested } from '../decorators';
@Config @Config
@@ -64,11 +66,14 @@ export class TemplateConfig {
'credentials-shared': string = ''; 'credentials-shared': string = '';
} }
const emailModeSchema = z.enum(['', 'smtp']);
type EmailMode = z.infer<typeof emailModeSchema>;
@Config @Config
class EmailConfig { class EmailConfig {
/** How to send emails */ /** How to send emails */
@Env('N8N_EMAIL_MODE') @Env('N8N_EMAIL_MODE', emailModeSchema)
mode: '' | 'smtp' = 'smtp'; mode: EmailMode = 'smtp';
@Nested @Nested
smtp: SmtpConfig; smtp: SmtpConfig;

View File

@@ -1,5 +1,10 @@
import { z } from 'zod';
import { Config, Env } from '../decorators'; import { Config, Env } from '../decorators';
const callerPolicySchema = z.enum(['any', 'none', 'workflowsFromAList', 'workflowsFromSameOwner']);
type CallerPolicy = z.infer<typeof callerPolicySchema>;
@Config @Config
export class WorkflowsConfig { export class WorkflowsConfig {
/** Default name for workflow */ /** Default name for workflow */
@@ -7,9 +12,8 @@ export class WorkflowsConfig {
defaultName: string = 'My workflow'; defaultName: string = 'My workflow';
/** Default option for which workflows may call the current workflow */ /** Default option for which workflows may call the current workflow */
@Env('N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION') @Env('N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION', callerPolicySchema)
callerPolicyDefaultOption: 'any' | 'none' | 'workflowsFromAList' | 'workflowsFromSameOwner' = callerPolicyDefaultOption: CallerPolicy = 'workflowsFromSameOwner';
'workflowsFromSameOwner';
/** How many workflows to activate simultaneously during startup. */ /** How many workflows to activate simultaneously during startup. */
@Env('N8N_WORKFLOW_ACTIVATION_BATCH_SIZE') @Env('N8N_WORKFLOW_ACTIVATION_BATCH_SIZE')

View File

@@ -6,7 +6,7 @@ abstract class StringArray<T extends string> extends Array<T> {
} }
} }
export class CommaSeperatedStringArray<T extends string> extends StringArray<T> { export class CommaSeparatedStringArray<T extends string> extends StringArray<T> {
constructor(str: string) { constructor(str: string) {
super(str, ','); super(str, ',');
} }

View File

@@ -1,3 +1,5 @@
import { z } from 'zod';
import { AiAssistantConfig } from './configs/aiAssistant.config'; import { AiAssistantConfig } from './configs/aiAssistant.config';
import { AuthConfig } from './configs/auth.config'; import { AuthConfig } from './configs/auth.config';
import { CacheConfig } from './configs/cache.config'; import { CacheConfig } from './configs/cache.config';
@@ -38,6 +40,10 @@ export type { LogScope } from './configs/logging.config';
export { WorkflowsConfig } from './configs/workflows.config'; export { WorkflowsConfig } from './configs/workflows.config';
export * from './custom-types'; export * from './custom-types';
const protocolSchema = z.enum(['http', 'https']);
export type Protocol = z.infer<typeof protocolSchema>;
@Config @Config
export class GlobalConfig { export class GlobalConfig {
@Nested @Nested
@@ -99,8 +105,8 @@ export class GlobalConfig {
listen_address: string = '0.0.0.0'; listen_address: string = '0.0.0.0';
/** HTTP Protocol via which n8n can be reached */ /** HTTP Protocol via which n8n can be reached */
@Env('N8N_PROTOCOL') @Env('N8N_PROTOCOL', protocolSchema)
protocol: 'http' | 'https' = 'http'; protocol: Protocol = 'http';
@Nested @Nested
endpoints: EndpointsConfig; endpoints: EndpointsConfig;

View File

@@ -1,13 +1,13 @@
import { CommaSeperatedStringArray, ColonSeparatedStringArray } from '../src/custom-types'; import { CommaSeparatedStringArray, ColonSeparatedStringArray } from '../src/custom-types';
describe('CommaSeperatedStringArray', () => { describe('CommaSeparatedStringArray', () => {
it('should parse comma-separated string into array', () => { it('should parse comma-separated string into array', () => {
const result = new CommaSeperatedStringArray('a,b,c'); const result = new CommaSeparatedStringArray('a,b,c');
expect(result).toEqual(['a', 'b', 'c']); expect(result).toEqual(['a', 'b', 'c']);
}); });
it('should handle empty strings', () => { it('should handle empty strings', () => {
const result = new CommaSeperatedStringArray('a,b,,,'); const result = new CommaSeparatedStringArray('a,b,,,');
expect(result).toEqual(['a', 'b']); expect(result).toEqual(['a', 'b']);
}); });
}); });

View File

@@ -1,10 +1,10 @@
import { CommaSeperatedStringArray, Config, Env } from '@n8n/config'; import { CommaSeparatedStringArray, Config, Env } from '@n8n/config';
import { UnexpectedError } from 'n8n-workflow'; import { UnexpectedError } from 'n8n-workflow';
const moduleNames = ['insights'] as const; const moduleNames = ['insights'] as const;
type ModuleName = (typeof moduleNames)[number]; type ModuleName = (typeof moduleNames)[number];
class Modules extends CommaSeperatedStringArray<ModuleName> { class Modules extends CommaSeparatedStringArray<ModuleName> {
constructor(str: string) { constructor(str: string) {
super(str); super(str);