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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
import { z } from 'zod';
import { Config, Env, Nested } from '../decorators';
@Config
@@ -15,6 +17,9 @@ class LogWriterConfig {
logBaseName: string = 'n8nEventLog';
}
const recoveryModeSchema = z.enum(['simple', 'extensive']);
type RecoveryMode = z.infer<typeof recoveryModeSchema>;
@Config
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 */
@@ -26,6 +31,6 @@ export class EventBusConfig {
logWriter: LogWriterConfig;
/** Whether to recover execution details after a crash or only mark status executions as crashed. */
@Env('N8N_EVENTBUS_RECOVERY_MODE')
crashRecoveryMode: 'simple' | 'extensive' = 'extensive';
@Env('N8N_EVENTBUS_RECOVERY_MODE', recoveryModeSchema)
crashRecoveryMode: RecoveryMode = 'extensive';
}

View File

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

View File

@@ -1,13 +1,18 @@
import { z } from 'zod';
import { Config, Env } from '../decorators';
const releaseChannelSchema = z.enum(['stable', 'beta', 'nightly', 'dev']);
type ReleaseChannel = z.infer<typeof releaseChannelSchema>;
@Config
export class GenericConfig {
/** Default timezone for the n8n instance. Can be overridden on a per-workflow basis. */
@Env('GENERIC_TIMEZONE')
timezone: string = 'America/New_York';
@Env('N8N_RELEASE_TYPE')
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev';
@Env('N8N_RELEASE_TYPE', releaseChannelSchema)
releaseChannel: ReleaseChannel = 'dev';
/** Grace period (in seconds) to wait for components to shut down before process exit. */
@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';
/** Scopes (areas of functionality) to filter logs by. */
@@ -41,6 +43,9 @@ class FileLoggingConfig {
location: string = 'logs/n8n.log';
}
const logLevelSchema = z.enum(['error', 'warn', 'info', 'debug', 'silent']);
type LogLevel = z.infer<typeof logLevelSchema>;
@Config
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`.
*/
@Env('N8N_LOG_LEVEL')
level: 'error' | 'warn' | 'info' | 'debug' | 'silent' = 'info';
@Env('N8N_LOG_LEVEL', logLevelSchema)
level: LogLevel = 'info';
/**
* 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.
*/
@Env('N8N_LOG_OUTPUT')
outputs: CommaSeperatedStringArray<'console' | 'file'> = ['console'];
outputs: CommaSeparatedStringArray<'console' | 'file'> = ['console'];
@Nested
file: FileLoggingConfig;
@@ -85,5 +90,5 @@ export class LoggingConfig {
* `N8N_LOG_SCOPES=license,waiting-executions`
*/
@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';
@Config
@@ -64,11 +66,14 @@ export class TemplateConfig {
'credentials-shared': string = '';
}
const emailModeSchema = z.enum(['', 'smtp']);
type EmailMode = z.infer<typeof emailModeSchema>;
@Config
class EmailConfig {
/** How to send emails */
@Env('N8N_EMAIL_MODE')
mode: '' | 'smtp' = 'smtp';
@Env('N8N_EMAIL_MODE', emailModeSchema)
mode: EmailMode = 'smtp';
@Nested
smtp: SmtpConfig;

View File

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

View File

@@ -1,3 +1,5 @@
import { z } from 'zod';
import { AiAssistantConfig } from './configs/aiAssistant.config';
import { AuthConfig } from './configs/auth.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 * from './custom-types';
const protocolSchema = z.enum(['http', 'https']);
export type Protocol = z.infer<typeof protocolSchema>;
@Config
export class GlobalConfig {
@Nested
@@ -99,8 +105,8 @@ export class GlobalConfig {
listen_address: string = '0.0.0.0';
/** HTTP Protocol via which n8n can be reached */
@Env('N8N_PROTOCOL')
protocol: 'http' | 'https' = 'http';
@Env('N8N_PROTOCOL', protocolSchema)
protocol: Protocol = 'http';
@Nested
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', () => {
const result = new CommaSeperatedStringArray('a,b,c');
const result = new CommaSeparatedStringArray('a,b,c');
expect(result).toEqual(['a', 'b', 'c']);
});
it('should handle empty strings', () => {
const result = new CommaSeperatedStringArray('a,b,,,');
const result = new CommaSeparatedStringArray('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';
const moduleNames = ['insights'] as const;
type ModuleName = (typeof moduleNames)[number];
class Modules extends CommaSeperatedStringArray<ModuleName> {
class Modules extends CommaSeparatedStringArray<ModuleName> {
constructor(str: string) {
super(str);