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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 941 additions and 556 deletions

View file

@ -0,0 +1,10 @@
const sharedOptions = require('@n8n_io/eslint-config/shared');
/**
* @type {import('@types/eslint').ESLint.ConfigData}
*/
module.exports = {
extends: ['@n8n_io/eslint-config/node'],
...sharedOptions(__dirname),
};

View file

@ -0,0 +1,2 @@
/** @type {import('jest').Config} */
module.exports = require('../../../jest.config');

View file

@ -0,0 +1,26 @@
{
"name": "@n8n/config",
"version": "1.0.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"format": "prettier --write . --ignore-path ../../../.prettierignore",
"lint": "eslint .",
"lintfix": "eslint . --fix",
"watch": "tsc -p tsconfig.build.json --watch",
"test": "jest",
"test:dev": "jest --watch"
},
"main": "dist/index.js",
"module": "src/index.ts",
"types": "dist/index.d.ts",
"files": [
"dist/**/*"
],
"dependencies": {
"reflect-metadata": "0.2.2",
"typedi": "0.10.0"
}
}

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;
}

View file

@ -0,0 +1,145 @@
import fs from 'fs';
import { Container } from 'typedi';
import { mock } from 'jest-mock-extended';
import { GlobalConfig } from '../src/index';
jest.mock('fs');
const mockFs = mock<typeof fs>();
fs.readFileSync = mockFs.readFileSync;
describe('GlobalConfig', () => {
beforeEach(() => {
Container.reset();
});
const originalEnv = process.env;
afterEach(() => {
process.env = originalEnv;
});
const defaultConfig = {
database: {
logging: {
enabled: false,
maxQueryExecutionTime: 0,
options: 'error',
},
mysqldb: {
database: 'n8n',
host: 'localhost',
password: '',
port: 3306,
user: 'root',
},
postgresdb: {
database: 'n8n',
host: 'localhost',
password: '',
poolSize: 2,
port: 5432,
schema: 'public',
ssl: {
ca: '',
cert: '',
enabled: false,
key: '',
rejectUnauthorized: true,
},
user: 'postgres',
},
sqlite: {
database: 'database.sqlite',
enableWAL: false,
executeVacuumOnStartup: false,
poolSize: 0,
},
tablePrefix: '',
type: 'sqlite',
},
credentials: {
defaultName: 'My credentials',
overwrite: {
data: '{}',
endpoint: '',
},
},
userManagement: {
emails: {
mode: 'smtp',
smtp: {
host: '',
port: 465,
secure: true,
sender: '',
startTLS: true,
auth: {
pass: '',
user: '',
privateKey: '',
serviceClient: '',
},
},
template: {
credentialsShared: '',
invite: '',
passwordReset: '',
workflowShared: '',
},
},
},
};
it('should use all default values when no env variables are defined', () => {
process.env = {};
const config = Container.get(GlobalConfig);
expect(config).toEqual(defaultConfig);
expect(mockFs.readFileSync).not.toHaveBeenCalled();
});
it('should use values from env variables when defined', () => {
process.env = {
DB_POSTGRESDB_HOST: 'some-host',
DB_POSTGRESDB_USER: 'n8n',
DB_TABLE_PREFIX: 'test_',
};
const config = Container.get(GlobalConfig);
expect(config).toEqual({
...defaultConfig,
database: {
logging: defaultConfig.database.logging,
mysqldb: defaultConfig.database.mysqldb,
postgresdb: {
...defaultConfig.database.postgresdb,
host: 'some-host',
user: 'n8n',
},
sqlite: defaultConfig.database.sqlite,
tablePrefix: 'test_',
type: 'sqlite',
},
});
expect(mockFs.readFileSync).not.toHaveBeenCalled();
});
it('should use values from env variables when defined and convert them to the correct type', () => {
const passwordFile = '/path/to/postgres/password';
process.env = {
DB_POSTGRESDB_PASSWORD_FILE: passwordFile,
};
mockFs.readFileSync.calledWith(passwordFile, 'utf8').mockReturnValueOnce('password-from-file');
const config = Container.get(GlobalConfig);
expect(config).toEqual({
...defaultConfig,
database: {
...defaultConfig.database,
postgresdb: {
...defaultConfig.database.postgresdb,
password: 'password-from-file',
},
},
});
expect(mockFs.readFileSync).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,11 @@
{
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/build.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"exclude": ["test/**"]
}

View file

@ -0,0 +1,13 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"types": ["node", "jest"],
"baseUrl": "src",
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
},
"include": ["src/**/*.ts", "test/**/*.ts"]
}

View file

@ -68,6 +68,7 @@
"@types/convict": "^6.1.1",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.21",
"@types/flat": "^5.0.5",
"@types/formidable": "^3.4.5",
"@types/json-diff": "^1.0.0",
"@types/jsonwebtoken": "^9.0.6",
@ -91,6 +92,7 @@
},
"dependencies": {
"@n8n/client-oauth2": "workspace:*",
"@n8n/config": "workspace:*",
"@n8n/localtunnel": "2.1.0",
"@n8n/n8n-nodes-langchain": "workspace:*",
"@n8n/permissions": "workspace:*",
@ -123,6 +125,7 @@
"express-prom-bundle": "6.6.0",
"express-rate-limit": "7.2.0",
"fast-glob": "3.2.12",
"flat": "5.0.2",
"flatted": "3.2.7",
"formidable": "3.5.1",
"google-timezones-json": "1.1.0",

View file

@ -1,7 +1,7 @@
import { Service } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import { deepCopy, jsonParse } from 'n8n-workflow';
import config from '@/config';
import type { ICredentialsOverwrite } from '@/Interfaces';
import { CredentialTypes } from '@/CredentialTypes';
import { Logger } from '@/Logger';
@ -13,10 +13,11 @@ export class CredentialsOverwrites {
private resolvedTypes: string[] = [];
constructor(
globalConfig: GlobalConfig,
private readonly credentialTypes: CredentialTypes,
private readonly logger: Logger,
) {
const data = config.getEnv('credentials.overwrite.data');
const data = globalConfig.credentials.overwrite.data;
const overwriteData = jsonParse<ICredentialsOverwrite>(data, {
errorMessage: 'The credentials-overwrite is not valid JSON.',
});

View file

@ -2,6 +2,7 @@ import { Service } from 'typedi';
import { snakeCase } from 'change-case';
import os from 'node:os';
import { get as pslGet } from 'psl';
import { GlobalConfig } from '@n8n/config';
import type {
ExecutionStatus,
INodesGraphResult,
@ -37,6 +38,7 @@ import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
@Service()
export class InternalHooks {
constructor(
private readonly globalConfig: GlobalConfig,
private readonly telemetry: Telemetry,
private readonly nodeTypes: NodeTypes,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
@ -73,7 +75,7 @@ export class InternalHooks {
const info = {
version_cli: N8N_VERSION,
db_type: config.getEnv('database.type'),
db_type: this.globalConfig.database.type,
n8n_version_notifications_enabled: config.getEnv('versionNotifications.enabled'),
n8n_disable_production_main_process: config.getEnv(
'endpoints.disableProductionWebhooksOnMainProcess',
@ -105,7 +107,7 @@ export class InternalHooks {
},
n8n_deployment_type: config.getEnv('deployment.type'),
n8n_binary_data_mode: binaryDataConfig.mode,
smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp',
smtp_set_up: this.globalConfig.userManagement.emails.mode === 'smtp',
ldap_allowed: authenticationMethod === 'ldap',
saml_enabled: authenticationMethod === 'saml',
license_plan_name: this.license.getPlanName(),

View file

@ -5,6 +5,7 @@ import { promisify } from 'util';
import cookieParser from 'cookie-parser';
import express from 'express';
import helmet from 'helmet';
import { GlobalConfig } from '@n8n/config';
import { InstanceSettings } from 'n8n-core';
import type { IN8nUISettings } from 'n8n-workflow';
@ -95,7 +96,9 @@ export class Server extends AbstractServer {
}
this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
const globalConfig = Container.get(GlobalConfig);
this.endpointPresetCredentials = globalConfig.credentials.overwrite.endpoint;
await super.start();
this.logger.debug(`Server ID: ${this.uniqueInstanceId}`);

View file

@ -1,87 +1,52 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Service } from 'typedi';
import { pick } from 'lodash';
import type { Transporter } from 'nodemailer';
import { createTransport } from 'nodemailer';
import type SMTPConnection from 'nodemailer/lib/smtp-connection';
import { Service } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import config from '@/config';
import type { MailData, SendEmailResult } from './Interfaces';
import { Logger } from '@/Logger';
import type { MailData, SendEmailResult } from './Interfaces';
@Service()
export class NodeMailer {
private transport?: Transporter;
readonly sender: string;
constructor(private readonly logger: Logger) {}
private transport: Transporter;
async init(): Promise<void> {
const transportConfig: SMTPConnection.Options = {
host: config.getEnv('userManagement.emails.smtp.host'),
port: config.getEnv('userManagement.emails.smtp.port'),
secure: config.getEnv('userManagement.emails.smtp.secure'),
ignoreTLS: !config.getEnv('userManagement.emails.smtp.startTLS'),
};
constructor(
globalConfig: GlobalConfig,
private readonly logger: Logger,
) {
const smtpConfig = globalConfig.userManagement.emails.smtp;
const transportConfig: SMTPConnection.Options = pick(smtpConfig, ['host', 'port', 'secure']);
transportConfig.ignoreTLS = !smtpConfig.startTLS;
if (
config.getEnv('userManagement.emails.smtp.auth.user') &&
config.getEnv('userManagement.emails.smtp.auth.pass')
) {
transportConfig.auth = {
user: config.getEnv('userManagement.emails.smtp.auth.user'),
pass: config.getEnv('userManagement.emails.smtp.auth.pass'),
};
const { auth } = smtpConfig;
if (auth.user && auth.pass) {
transportConfig.auth = pick(auth, ['user', 'pass']);
}
if (
config.getEnv('userManagement.emails.smtp.auth.serviceClient') &&
config.getEnv('userManagement.emails.smtp.auth.privateKey')
) {
if (auth.serviceClient && auth.privateKey) {
transportConfig.auth = {
type: 'OAuth2',
user: config.getEnv('userManagement.emails.smtp.auth.user'),
serviceClient: config.getEnv('userManagement.emails.smtp.auth.serviceClient'),
privateKey: config
.getEnv('userManagement.emails.smtp.auth.privateKey')
.replace(/\\n/g, '\n'),
user: auth.user,
serviceClient: auth.serviceClient,
privateKey: auth.privateKey.replace(/\\n/g, '\n'),
};
}
this.transport = createTransport(transportConfig);
}
async verifyConnection(): Promise<void> {
if (!this.transport) {
await this.init();
}
const host = config.getEnv('userManagement.emails.smtp.host');
const user = config.getEnv('userManagement.emails.smtp.auth.user');
const pass = config.getEnv('userManagement.emails.smtp.auth.pass');
try {
await this.transport?.verify();
} catch (error) {
const message: string[] = [];
if (!host) message.push('SMTP host not defined (N8N_SMTP_HOST).');
if (!user) message.push('SMTP user not defined (N8N_SMTP_USER).');
if (!pass) message.push('SMTP pass not defined (N8N_SMTP_PASS).');
throw message.length ? new Error(message.join(' '), { cause: error }) : error;
this.sender = smtpConfig.sender;
if (!this.sender && auth.user.includes('@')) {
this.sender = auth.user;
}
}
async sendMail(mailData: MailData): Promise<SendEmailResult> {
if (!this.transport) {
await this.init();
}
let sender = config.getEnv('userManagement.emails.smtp.sender');
const user = config.getEnv('userManagement.emails.smtp.auth.user');
if (!sender && user.includes('@')) {
sender = user;
}
try {
await this.transport?.sendMail({
from: sender,
from: this.sender,
to: mailData.emailRecipients,
subject: mailData.subject,
text: mailData.textOnly,
@ -92,7 +57,10 @@ export class NodeMailer {
);
} catch (error) {
ErrorReporter.error(error);
this.logger.error('Failed to send email', { recipients: mailData.emailRecipients, error });
this.logger.error('Failed to send email', {
recipients: mailData.emailRecipients,
error: error as Error,
});
throw error;
}

View file

@ -3,9 +3,8 @@ import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import Handlebars from 'handlebars';
import { join as pathJoin } from 'path';
import { ApplicationError } from 'n8n-workflow';
import { GlobalConfig } from '@n8n/config';
import config from '@/config';
import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { UserRepository } from '@db/repositories/user.repository';
@ -22,42 +21,25 @@ import { EventRelay } from '@/eventbus/event-relay.service';
type Template = HandlebarsTemplateDelegate<unknown>;
type TemplateName = 'invite' | 'passwordReset' | 'workflowShared' | 'credentialsShared';
const templates: Partial<Record<TemplateName, Template>> = {};
async function getTemplate(
templateName: TemplateName,
defaultFilename = `${templateName}.html`,
): Promise<Template> {
let template = templates[templateName];
if (!template) {
const templateOverride = config.getEnv(`userManagement.emails.templates.${templateName}`);
let markup;
if (templateOverride && existsSync(templateOverride)) {
markup = await readFile(templateOverride, 'utf-8');
} else {
markup = await readFile(pathJoin(__dirname, `templates/${defaultFilename}`), 'utf-8');
}
template = Handlebars.compile(markup);
templates[templateName] = template;
}
return template;
}
@Service()
export class UserManagementMailer {
readonly isEmailSetUp: boolean;
private mailer: NodeMailer | undefined;
readonly templateOverrides: GlobalConfig['userManagement']['emails']['template'];
readonly templatesCache: Partial<Record<TemplateName, Template>> = {};
readonly mailer: NodeMailer | undefined;
constructor(
private readonly userRepository: UserRepository,
globalConfig: GlobalConfig,
private readonly logger: Logger,
private readonly userRepository: UserRepository,
private readonly urlService: UrlService,
) {
this.isEmailSetUp =
config.getEnv('userManagement.emails.mode') === 'smtp' &&
config.getEnv('userManagement.emails.smtp.host') !== '';
const emailsConfig = globalConfig.userManagement.emails;
this.isEmailSetUp = emailsConfig.mode === 'smtp' && emailsConfig.smtp.host !== '';
this.templateOverrides = emailsConfig.template;
// Other implementations can be used in the future.
if (this.isEmailSetUp) {
@ -65,15 +47,11 @@ export class UserManagementMailer {
}
}
async verifyConnection(): Promise<void> {
if (!this.mailer) throw new ApplicationError('No mailer configured.');
return await this.mailer.verifyConnection();
}
async invite(inviteEmailData: InviteEmailData): Promise<SendEmailResult> {
const template = await getTemplate('invite');
const result = await this.mailer?.sendMail({
if (!this.mailer) return { emailSent: false };
const template = await this.getTemplate('invite');
const result = await this.mailer.sendMail({
emailRecipients: inviteEmailData.email,
subject: 'You have been invited to n8n',
body: template(inviteEmailData),
@ -85,8 +63,10 @@ export class UserManagementMailer {
}
async passwordReset(passwordResetData: PasswordResetData): Promise<SendEmailResult> {
const template = await getTemplate('passwordReset', 'passwordReset.html');
const result = await this.mailer?.sendMail({
if (!this.mailer) return { emailSent: false };
const template = await this.getTemplate('passwordReset', 'passwordReset.html');
const result = await this.mailer.sendMail({
emailRecipients: passwordResetData.email,
subject: 'n8n password reset',
body: template(passwordResetData),
@ -105,16 +85,16 @@ export class UserManagementMailer {
sharer: User;
newShareeIds: string[];
workflow: WorkflowEntity;
}) {
if (!this.mailer) return;
}): Promise<SendEmailResult> {
if (!this.mailer) return { emailSent: false };
const recipients = await this.userRepository.getEmailsByIds(newShareeIds);
if (recipients.length === 0) return;
if (recipients.length === 0) return { emailSent: false };
const emailRecipients = recipients.map(({ email }) => email);
const populateTemplate = await getTemplate('workflowShared', 'workflowShared.html');
const populateTemplate = await this.getTemplate('workflowShared', 'workflowShared.html');
const baseUrl = this.urlService.getInstanceBaseUrl();
@ -164,16 +144,16 @@ export class UserManagementMailer {
sharer: User;
newShareeIds: string[];
credentialsName: string;
}) {
if (!this.mailer) return;
}): Promise<SendEmailResult> {
if (!this.mailer) return { emailSent: false };
const recipients = await this.userRepository.getEmailsByIds(newShareeIds);
if (recipients.length === 0) return;
if (recipients.length === 0) return { emailSent: false };
const emailRecipients = recipients.map(({ email }) => email);
const populateTemplate = await getTemplate('credentialsShared', 'credentialsShared.html');
const populateTemplate = await this.getTemplate('credentialsShared', 'credentialsShared.html');
const baseUrl = this.urlService.getInstanceBaseUrl();
@ -214,4 +194,22 @@ export class UserManagementMailer {
throw new InternalServerError(`Please contact your administrator: ${error.message}`);
}
}
async getTemplate(
templateName: TemplateName,
defaultFilename = `${templateName}.html`,
): Promise<Template> {
let template = this.templatesCache[templateName];
if (!template) {
const templateOverride = this.templateOverrides[templateName];
const templatePath =
templateOverride && existsSync(templateOverride)
? templateOverride
: pathJoin(__dirname, `templates/${defaultFilename}`);
const markup = await readFile(templatePath, 'utf-8');
template = Handlebars.compile(markup);
this.templatesCache[templateName] = template;
}
return template;
}
}

View file

@ -1,6 +1,7 @@
import 'reflect-metadata';
import { Container } from 'typedi';
import { Command, Errors } from '@oclif/core';
import { GlobalConfig } from '@n8n/config';
import { ApplicationError, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
import { BinaryDataService, InstanceSettings, ObjectStoreService } from 'n8n-core';
import type { AbstractServer } from '@/AbstractServer';
@ -77,7 +78,8 @@ export abstract class BaseCommand extends Command {
await this.exitWithCrash('There was an error running database migrations', error),
);
const dbType = config.getEnv('database.type');
const globalConfig = Container.get(GlobalConfig);
const { type: dbType } = globalConfig.database;
if (['mysqldb', 'mariadb'].includes(dbType)) {
this.logger.warn(

View file

@ -8,6 +8,7 @@ import { createReadStream, createWriteStream, existsSync } from 'fs';
import { pipeline } from 'stream/promises';
import replaceStream from 'replacestream';
import glob from 'fast-glob';
import { GlobalConfig } from '@n8n/config';
import { jsonParse, randomString } from 'n8n-workflow';
import config from '@/config';
@ -260,9 +261,10 @@ export class Start extends BaseCommand {
});
}
const dbType = config.getEnv('database.type');
const globalConfig = Container.get(GlobalConfig);
const { type: dbType } = globalConfig.database;
if (dbType === 'sqlite') {
const shouldRunVacuum = config.getEnv('database.sqlite.executeVacuumOnStartup');
const shouldRunVacuum = globalConfig.database.sqlite.executeVacuumOnStartup;
if (shouldRunVacuum) {
await Container.get(ExecutionRepository).query('VACUUM;');
}

View file

@ -3,6 +3,7 @@ import { Flags, type Config } from '@oclif/core';
import express from 'express';
import http from 'http';
import type PCancelable from 'p-cancelable';
import { GlobalConfig } from '@n8n/config';
import { WorkflowExecute } from 'n8n-core';
import type { ExecutionStatus, IExecuteResponsePromiseData, INodeTypes, IRun } from 'n8n-workflow';
import { Workflow, sleep, ApplicationError } from 'n8n-workflow';
@ -429,7 +430,9 @@ export class Worker extends BaseCommand {
);
let presetCredentialsLoaded = false;
const endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
const globalConfig = Container.get(GlobalConfig);
const endpointPresetCredentials = globalConfig.credentials.overwrite.endpoint;
if (endpointPresetCredentials !== '') {
// POST endpoint to set preset credentials
app.post(

View file

@ -1,7 +1,12 @@
import { Container } from 'typedi';
import convict from 'convict';
import dotenv from 'dotenv';
import { readFileSync } from 'fs';
import { flatten } from 'flat';
import merge from 'lodash/merge';
import { GlobalConfig } from '@n8n/config';
import { ApplicationError, setGlobalState } from 'n8n-workflow';
import { inTest, inE2ETests } from '@/constants';
if (inE2ETests) {
@ -33,9 +38,32 @@ if (!inE2ETests && !inTest) {
// optional configuration files
const { N8N_CONFIG_FILES } = process.env;
if (N8N_CONFIG_FILES !== undefined) {
const globalConfig = Container.get(GlobalConfig);
const configFiles = N8N_CONFIG_FILES.split(',');
console.debug('Loading config overwrites', configFiles);
config.loadFile(configFiles);
for (const configFile of configFiles) {
if (!configFile) continue;
// NOTE: This is "temporary" code until we have migrated all config to the new package
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const data = JSON.parse(readFileSync(configFile, 'utf8'));
for (const prefix in data) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const innerData = data[prefix];
if (prefix in globalConfig) {
// @ts-ignore
merge(globalConfig[prefix], innerData);
} else {
const flattenedData: Record<string, string> = flatten(innerData);
for (const key in flattenedData) {
config.set(`${prefix}.${key}`, flattenedData[key]);
}
}
}
console.debug('Loaded config overwrites from', configFile);
} catch (error) {
console.error('Error loading config file', configFile, error);
}
}
}
// Overwrite config from files defined in "_FILE" environment variables

View file

@ -21,203 +21,6 @@ convict.addFormat({
});
export const schema = {
database: {
type: {
doc: 'Type of database to use',
format: ['sqlite', 'mariadb', 'mysqldb', 'postgresdb'] as const,
default: 'sqlite',
env: 'DB_TYPE',
},
tablePrefix: {
doc: 'Prefix for table names',
format: '*',
default: '',
env: 'DB_TABLE_PREFIX',
},
logging: {
enabled: {
doc: 'Typeorm logging enabled flag.',
format: Boolean,
default: false,
env: 'DB_LOGGING_ENABLED',
},
options: {
doc: 'Logging level options, default is "error". Possible values: query,error,schema,warn,info,log. To enable all logging, specify "all"',
format: String,
default: 'error',
env: 'DB_LOGGING_OPTIONS',
},
maxQueryExecutionTime: {
doc: 'Maximum number of milliseconds query should be executed before logger logs a warning. Set 0 to disable long running query warning',
format: Number,
default: 0, // 0 disables the slow-query log
env: 'DB_LOGGING_MAX_EXECUTION_TIME',
},
},
postgresdb: {
database: {
doc: 'PostgresDB Database',
format: String,
default: 'n8n',
env: 'DB_POSTGRESDB_DATABASE',
},
host: {
doc: 'PostgresDB Host',
format: String,
default: 'localhost',
env: 'DB_POSTGRESDB_HOST',
},
password: {
doc: 'PostgresDB Password',
format: String,
default: '',
env: 'DB_POSTGRESDB_PASSWORD',
},
port: {
doc: 'PostgresDB Port',
format: Number,
default: 5432,
env: 'DB_POSTGRESDB_PORT',
},
user: {
doc: 'PostgresDB User',
format: String,
default: 'postgres',
env: 'DB_POSTGRESDB_USER',
},
schema: {
doc: 'PostgresDB Schema',
format: String,
default: 'public',
env: 'DB_POSTGRESDB_SCHEMA',
},
poolSize: {
doc: 'PostgresDB Pool Size',
format: Number,
default: 2,
env: 'DB_POSTGRESDB_POOL_SIZE',
},
ssl: {
enabled: {
doc: 'If SSL should be enabled. If `ca`, `cert`, or `key` are defined, this will automatically default to true',
format: Boolean,
default: false,
env: 'DB_POSTGRESDB_SSL_ENABLED',
},
ca: {
doc: 'SSL certificate authority',
format: String,
default: '',
env: 'DB_POSTGRESDB_SSL_CA',
},
cert: {
doc: 'SSL certificate',
format: String,
default: '',
env: 'DB_POSTGRESDB_SSL_CERT',
},
key: {
doc: 'SSL key',
format: String,
default: '',
env: 'DB_POSTGRESDB_SSL_KEY',
},
rejectUnauthorized: {
doc: 'If unauthorized SSL connections should be rejected',
format: Boolean,
default: true,
env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED',
},
},
},
mysqldb: {
database: {
doc: '[DEPRECATED] MySQL Database',
format: String,
default: 'n8n',
env: 'DB_MYSQLDB_DATABASE',
},
host: {
doc: 'MySQL Host',
format: String,
default: 'localhost',
env: 'DB_MYSQLDB_HOST',
},
password: {
doc: 'MySQL Password',
format: String,
default: '',
env: 'DB_MYSQLDB_PASSWORD',
},
port: {
doc: 'MySQL Port',
format: Number,
default: 3306,
env: 'DB_MYSQLDB_PORT',
},
user: {
doc: 'MySQL User',
format: String,
default: 'root',
env: 'DB_MYSQLDB_USER',
},
},
sqlite: {
database: {
doc: 'SQLite Database file name',
format: String,
default: 'database.sqlite',
env: 'DB_SQLITE_DATABASE',
},
enableWAL: {
doc: 'Enable SQLite WAL mode (Always enabled for pool-size > 1)',
format: Boolean,
default: false,
env: 'DB_SQLITE_ENABLE_WAL',
},
poolSize: {
doc: 'SQLite Pool Size (Setting this to 0 disables pooling)',
format: Number,
default: 0,
env: 'DB_SQLITE_POOL_SIZE',
},
executeVacuumOnStartup: {
doc: 'Runs VACUUM operation on startup to rebuild the database. Reduces filesize and optimizes indexes. WARNING: This is a long running blocking operation. Will increase start-up time.',
format: Boolean,
default: false,
env: 'DB_SQLITE_VACUUM_ON_STARTUP',
},
},
},
credentials: {
overwrite: {
data: {
// Allows to set default values for credentials which
// get automatically prefilled and the user does not get
// displayed and can not change.
// Format: { CREDENTIAL_NAME: { PARAMETER: VALUE }}
doc: 'Overwrites for credentials',
format: '*',
default: '{}',
env: 'CREDENTIALS_OVERWRITE_DATA',
},
endpoint: {
doc: 'Fetch credentials from API',
format: String,
default: '',
env: 'CREDENTIALS_OVERWRITE_ENDPOINT',
},
},
defaultName: {
doc: 'Default name for credentials',
format: String,
default: 'My credentials',
env: 'CREDENTIALS_DEFAULT_NAME',
},
},
workflows: {
defaultName: {
doc: 'Default name for workflow',
@ -814,98 +617,6 @@ export const schema = {
format: Boolean,
default: false,
},
emails: {
mode: {
doc: 'How to send emails',
format: ['', 'smtp'] as const,
default: 'smtp',
env: 'N8N_EMAIL_MODE',
},
smtp: {
host: {
doc: 'SMTP server host',
format: String, // e.g. 'smtp.gmail.com'
default: '',
env: 'N8N_SMTP_HOST',
},
port: {
doc: 'SMTP server port',
format: Number,
default: 465,
env: 'N8N_SMTP_PORT',
},
secure: {
doc: 'Whether or not to use SSL for SMTP',
format: Boolean,
default: true,
env: 'N8N_SMTP_SSL',
},
startTLS: {
doc: 'Whether or not to use STARTTLS for SMTP when SSL is disabled',
format: Boolean,
default: true,
env: 'N8N_SMTP_STARTTLS',
},
auth: {
user: {
doc: 'SMTP login username',
format: String, // e.g.'you@gmail.com'
default: '',
env: 'N8N_SMTP_USER',
},
pass: {
doc: 'SMTP login password',
format: String,
default: '',
env: 'N8N_SMTP_PASS',
},
serviceClient: {
doc: 'SMTP OAuth Service Client',
format: String,
default: '',
env: 'N8N_SMTP_OAUTH_SERVICE_CLIENT',
},
privateKey: {
doc: 'SMTP OAuth Private Key',
format: String,
default: '',
env: 'N8N_SMTP_OAUTH_PRIVATE_KEY',
},
},
sender: {
doc: 'How to display sender name',
format: String,
default: '',
env: 'N8N_SMTP_SENDER',
},
},
templates: {
invite: {
doc: 'Overrides default HTML template for inviting new people (use full path)',
format: String,
default: '',
env: 'N8N_UM_EMAIL_TEMPLATES_INVITE',
},
passwordReset: {
doc: 'Overrides default HTML template for resetting password (use full path)',
format: String,
default: '',
env: 'N8N_UM_EMAIL_TEMPLATES_PWRESET',
},
workflowShared: {
doc: 'Overrides default HTML template for notifying that a workflow was shared (use full path)',
format: String,
default: '',
env: 'N8N_UM_EMAIL_TEMPLATES_WORKFLOW_SHARED',
},
credentialsShared: {
doc: 'Overrides default HTML template for notifying that credentials were shared (use full path)',
format: String,
default: '',
env: 'N8N_UM_EMAIL_TEMPLATES_CREDENTIALS_SHARED',
},
},
},
authenticationMethod: {
doc: 'How to authenticate users (e.g. "email", "ldap", "saml")',
format: ['email', 'ldap', 'saml'] as const,

View file

@ -1,5 +1,8 @@
import { deepCopy } from 'n8n-workflow';
import config from '@/config';
import { GlobalConfig } from '@n8n/config';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In } from '@n8n/typeorm';
import { CredentialsService } from './credentials.service';
import { CredentialRequest } from '@/requests';
import { InternalHooks } from '@/InternalHooks';
@ -25,8 +28,6 @@ import * as Db from '@/Db';
import * as utils from '@/utils';
import { listQueryMiddleware } from '@/middlewares';
import { SharedCredentialsRepository } from '@/databases/repositories/sharedCredentials.repository';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In } from '@n8n/typeorm';
import { SharedCredentials } from '@/databases/entities/SharedCredentials';
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
import { z } from 'zod';
@ -35,6 +36,7 @@ import { EventRelay } from '@/eventbus/event-relay.service';
@RestController('/credentials')
export class CredentialsController {
constructor(
private readonly globalConfig: GlobalConfig,
private readonly credentialsService: CredentialsService,
private readonly enterpriseCredentialsService: EnterpriseCredentialsService,
private readonly namingService: NamingService,
@ -65,7 +67,7 @@ export class CredentialsController {
@Get('/new')
async generateUniqueName(req: CredentialRequest.NewName) {
const requestedName = req.query.name ?? config.getEnv('credentials.defaultName');
const requestedName = req.query.name ?? this.globalConfig.credentials.defaultName;
return {
name: await this.namingService.getUniqueCredentialName(requestedName),

View file

@ -8,8 +8,8 @@ import type { PostgresConnectionOptions } from '@n8n/typeorm/driver/postgres/Pos
import type { MysqlConnectionOptions } from '@n8n/typeorm/driver/mysql/MysqlConnectionOptions';
import { InstanceSettings } from 'n8n-core';
import { ApplicationError } from 'n8n-workflow';
import { GlobalConfig } from '@n8n/config';
import config from '@/config';
import { entities } from './entities';
import { subscribers } from './subscribers';
import { mysqlMigrations } from './migrations/mysqldb';
@ -17,19 +17,19 @@ import { postgresMigrations } from './migrations/postgresdb';
import { sqliteMigrations } from './migrations/sqlite';
const getCommonOptions = () => {
const entityPrefix = config.getEnv('database.tablePrefix');
const maxQueryExecutionTime = config.getEnv('database.logging.maxQueryExecutionTime');
const { tablePrefix: entityPrefix, logging: loggingConfig } =
Container.get(GlobalConfig).database;
let loggingOption: LoggerOptions = config.getEnv('database.logging.enabled');
let loggingOption: LoggerOptions = loggingConfig.enabled;
if (loggingOption) {
const optionsString = config.getEnv('database.logging.options').replace(/\s+/g, '');
const optionsString = loggingConfig.options.replace(/\s+/g, '');
if (optionsString === 'all') {
loggingOption = optionsString;
} else {
loggingOption = optionsString.split(',') as LoggerOptions;
}
}
return {
entityPrefix,
entities: Object.values(entities),
@ -37,33 +37,35 @@ const getCommonOptions = () => {
migrationsTableName: `${entityPrefix}migrations`,
migrationsRun: false,
synchronize: false,
maxQueryExecutionTime,
maxQueryExecutionTime: loggingConfig.maxQueryExecutionTime,
logging: loggingOption,
};
};
export const getOptionOverrides = (dbType: 'postgresdb' | 'mysqldb') => ({
database: config.getEnv(`database.${dbType}.database`),
host: config.getEnv(`database.${dbType}.host`),
port: config.getEnv(`database.${dbType}.port`),
username: config.getEnv(`database.${dbType}.user`),
password: config.getEnv(`database.${dbType}.password`),
});
export const getOptionOverrides = (dbType: 'postgresdb' | 'mysqldb') => {
const globalConfig = Container.get(GlobalConfig);
const dbConfig = globalConfig.database[dbType];
return {
database: dbConfig.database,
host: dbConfig.host,
port: dbConfig.port,
username: dbConfig.user,
password: dbConfig.password,
};
};
const getSqliteConnectionOptions = (): SqliteConnectionOptions | SqlitePooledConnectionOptions => {
const poolSize = config.getEnv('database.sqlite.poolSize');
const globalConfig = Container.get(GlobalConfig);
const sqliteConfig = globalConfig.database.sqlite;
const commonOptions = {
...getCommonOptions(),
database: path.resolve(
Container.get(InstanceSettings).n8nFolder,
config.getEnv('database.sqlite.database'),
),
database: path.resolve(Container.get(InstanceSettings).n8nFolder, sqliteConfig.database),
migrations: sqliteMigrations,
};
if (poolSize > 0) {
if (sqliteConfig.poolSize > 0) {
return {
type: 'sqlite-pooled',
poolSize,
poolSize: sqliteConfig.poolSize,
enableWAL: true,
acquireTimeout: 60_000,
destroyTimeout: 5_000,
@ -72,19 +74,19 @@ const getSqliteConnectionOptions = (): SqliteConnectionOptions | SqlitePooledCon
} else {
return {
type: 'sqlite',
enableWAL: config.getEnv('database.sqlite.enableWAL'),
enableWAL: sqliteConfig.enableWAL,
...commonOptions,
};
}
};
const getPostgresConnectionOptions = (): PostgresConnectionOptions => {
const sslCa = config.getEnv('database.postgresdb.ssl.ca');
const sslCert = config.getEnv('database.postgresdb.ssl.cert');
const sslKey = config.getEnv('database.postgresdb.ssl.key');
const sslRejectUnauthorized = config.getEnv('database.postgresdb.ssl.rejectUnauthorized');
const postgresConfig = Container.get(GlobalConfig).database.postgresdb;
const {
ssl: { ca: sslCa, cert: sslCert, key: sslKey, rejectUnauthorized: sslRejectUnauthorized },
} = postgresConfig;
let ssl: TlsOptions | boolean = config.getEnv('database.postgresdb.ssl.enabled');
let ssl: TlsOptions | boolean = postgresConfig.ssl.enabled;
if (sslCa !== '' || sslCert !== '' || sslKey !== '' || !sslRejectUnauthorized) {
ssl = {
ca: sslCa || undefined,
@ -98,8 +100,8 @@ const getPostgresConnectionOptions = (): PostgresConnectionOptions => {
type: 'postgres',
...getCommonOptions(),
...getOptionOverrides('postgresdb'),
schema: config.getEnv('database.postgresdb.schema'),
poolSize: config.getEnv('database.postgresdb.poolSize'),
schema: postgresConfig.schema,
poolSize: postgresConfig.poolSize,
migrations: postgresMigrations,
ssl,
};
@ -114,7 +116,8 @@ const getMysqlConnectionOptions = (dbType: 'mariadb' | 'mysqldb'): MysqlConnecti
});
export function getConnectionOptions(): DataSourceOptions {
const dbType = config.getEnv('database.type');
const globalConfig = Container.get(GlobalConfig);
const { type: dbType } = globalConfig.database;
switch (dbType) {
case 'sqlite':
return getSqliteConnectionOptions();

View file

@ -1,3 +1,5 @@
import { Container } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import type { ColumnOptions } from '@n8n/typeorm';
import {
BeforeInsert,
@ -6,11 +8,10 @@ import {
PrimaryColumn,
UpdateDateColumn,
} from '@n8n/typeorm';
import config from '@/config';
import type { Class } from 'n8n-core';
import { generateNanoId } from '../utils/generators';
const dbType = config.getEnv('database.type');
export const { type: dbType } = Container.get(GlobalConfig).database;
const timestampSyntax = {
sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')",

View file

@ -5,13 +5,12 @@ import type { IBinaryKeyData, INode, IPairedItemData } from 'n8n-workflow';
import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, OneToMany } from '@n8n/typeorm';
import config from '@/config';
import type { TagEntity } from './TagEntity';
import type { SharedWorkflow } from './SharedWorkflow';
import type { WorkflowStatistics } from './WorkflowStatistics';
import type { WorkflowTagMapping } from './WorkflowTagMapping';
import { objectRetriever, sqlite } from '../utils/transformers';
import { WithTimestampsAndStringId, jsonColumnType } from './AbstractEntity';
import { WithTimestampsAndStringId, dbType, jsonColumnType } from './AbstractEntity';
import type { IWorkflowDb } from '@/Interfaces';
@Entity()
@ -78,7 +77,7 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl
statistics: WorkflowStatistics[];
@Column({
type: config.getEnv('database.type') === 'sqlite' ? 'text' : 'json',
type: dbType === 'sqlite' ? 'text' : 'json',
nullable: true,
transformer: sqlite.jsonColumn,
})

View file

@ -1,14 +1,12 @@
import type { MigrationContext, IrreversibleMigration } from '@db/types';
import config from '@/config';
const COLLATION_57 = 'utf8mb4_general_ci';
const COLLATION_80 = 'utf8mb4_0900_ai_ci';
export class MigrateIntegerKeysToString1690000000001 implements IrreversibleMigration {
async up({ queryRunner, tablePrefix }: MigrationContext) {
const databaseType = config.get('database.type');
async up({ queryRunner, tablePrefix, dbType }: MigrationContext) {
let collation: string;
if (databaseType === 'mariadb') {
if (dbType === 'mariadb') {
collation = COLLATION_57;
} else {
const dbVersionQuery = (await queryRunner.query('SELECT @@version')) as

View file

@ -2,8 +2,8 @@ import { statSync } from 'fs';
import path from 'path';
import { Container } from 'typedi';
import { InstanceSettings } from 'n8n-core';
import { GlobalConfig } from '@n8n/config';
import type { MigrationContext, IrreversibleMigration } from '@db/types';
import config from '@/config';
export class MigrateIntegerKeysToString1690000000002 implements IrreversibleMigration {
transaction = false as const;
@ -193,7 +193,7 @@ const migrationsPruningEnabled = process.env.MIGRATIONS_PRUNING_ENABLED === 'tru
function getSqliteDbFileSize(): number {
const filename = path.resolve(
Container.get(InstanceSettings).n8nFolder,
config.getEnv('database.sqlite.database'),
Container.get(GlobalConfig).database.sqlite.database,
);
const { size } = statSync(filename);
return size;

View file

@ -43,6 +43,7 @@ import { ExecutionDataRepository } from './executionData.repository';
import { Logger } from '@/Logger';
import type { ExecutionSummaries } from '@/executions/execution.types';
import { PostgresLiveRowsRetrievalError } from '@/errors/postgres-live-rows-retrieval.error';
import { GlobalConfig } from '@n8n/config';
import { separate } from '@/utils';
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
@ -113,6 +114,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
constructor(
dataSource: DataSource,
private readonly globalConfig: GlobalConfig,
private readonly logger: Logger,
private readonly executionDataRepository: ExecutionDataRepository,
private readonly binaryDataService: BinaryDataService,
@ -482,7 +484,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
status: Not('crashed'),
};
const dbType = config.getEnv('database.type');
const dbType = this.globalConfig.database.type;
if (dbType === 'sqlite') {
// This is needed because of issue in TypeORM <> SQLite:
// https://github.com/typeorm/typeorm/issues/2286
@ -730,7 +732,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
}
async getLiveExecutionRowsOnPostgres() {
const tableName = `${config.getEnv('database.tablePrefix')}execution_entity`;
const tableName = `${this.globalConfig.database.tablePrefix}execution_entity`;
const pgSql = `SELECT n_live_tup as result FROM pg_stat_all_tables WHERE relname = '${tableName}';`;

View file

@ -1,5 +1,5 @@
import config from '@/config';
import { Service } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import { DataSource, Repository, Entity } from '@n8n/typeorm';
@Entity()
@ -7,19 +7,22 @@ export class UsageMetrics {}
@Service()
export class UsageMetricsRepository extends Repository<UsageMetrics> {
constructor(dataSource: DataSource) {
constructor(
dataSource: DataSource,
private readonly globalConfig: GlobalConfig,
) {
super(UsageMetrics, dataSource.manager);
}
toTableName(name: string) {
const tablePrefix = config.getEnv('database.tablePrefix');
const tablePrefix = this.globalConfig.database.tablePrefix;
let tableName =
config.getEnv('database.type') === 'mysqldb'
this.globalConfig.database.type === 'mysqldb'
? `\`${tablePrefix}${name}\``
: `"${tablePrefix}${name}"`;
const pgSchema = config.getEnv('database.postgresdb.schema');
const pgSchema = this.globalConfig.database.postgresdb.schema;
if (pgSchema !== 'public') tableName = [pgSchema, tablePrefix + name].join('.');

View file

@ -1,4 +1,5 @@
import { Service } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import {
DataSource,
Repository,
@ -18,7 +19,10 @@ import { WebhookEntity } from '../entities/WebhookEntity';
@Service()
export class WorkflowRepository extends Repository<WorkflowEntity> {
constructor(dataSource: DataSource) {
constructor(
dataSource: DataSource,
private readonly globalConfig: GlobalConfig,
) {
super(WorkflowEntity, dataSource.manager);
}
@ -73,12 +77,13 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
async updateWorkflowTriggerCount(id: string, triggerCount: number): Promise<UpdateResult> {
const qb = this.createQueryBuilder('workflow');
const dbType = this.globalConfig.database.type;
return await qb
.update()
.set({
triggerCount,
updatedAt: () => {
if (['mysqldb', 'mariadb'].includes(config.getEnv('database.type'))) {
if (['mysqldb', 'mariadb'].includes(dbType)) {
return 'updatedAt';
}
return '"updatedAt"';

View file

@ -1,6 +1,6 @@
import { Service } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import { DataSource, MoreThanOrEqual, QueryFailedError, Repository } from '@n8n/typeorm';
import config from '@/config';
import { StatisticsNames, WorkflowStatistics } from '../entities/WorkflowStatistics';
import type { User } from '@/databases/entities/User';
@ -9,9 +9,10 @@ type StatisticsUpsertResult = StatisticsInsertResult | 'update';
@Service()
export class WorkflowStatisticsRepository extends Repository<WorkflowStatistics> {
private readonly dbType = config.getEnv('database.type');
constructor(dataSource: DataSource) {
constructor(
dataSource: DataSource,
private readonly globalConfig: GlobalConfig,
) {
super(WorkflowStatistics, dataSource.manager);
}
@ -49,9 +50,10 @@ export class WorkflowStatisticsRepository extends Repository<WorkflowStatistics>
eventName: StatisticsNames,
workflowId: string,
): Promise<StatisticsUpsertResult> {
const dbType = this.globalConfig.database.type;
const { tableName } = this.metadata;
try {
if (this.dbType === 'sqlite') {
if (dbType === 'sqlite') {
await this.query(
`INSERT INTO "${tableName}" ("count", "name", "workflowId", "latestEvent")
VALUES (1, "${eventName}", "${workflowId}", CURRENT_TIMESTAMP)
@ -70,7 +72,7 @@ export class WorkflowStatisticsRepository extends Repository<WorkflowStatistics>
});
return counter?.count === 1 ? 'insert' : 'failed';
} else if (this.dbType === 'postgresdb') {
} else if (dbType === 'postgresdb') {
const queryResult = (await this.query(
`INSERT INTO "${tableName}" ("count", "name", "workflowId", "latestEvent")
VALUES (1, '${eventName}', '${workflowId}', CURRENT_TIMESTAMP)

View file

@ -1,10 +1,11 @@
import { Container } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import { readFileSync, rmSync } from 'fs';
import { InstanceSettings } from 'n8n-core';
import type { ObjectLiteral } from '@n8n/typeorm';
import type { QueryRunner } from '@n8n/typeorm/query-runner/QueryRunner';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import config from '@/config';
import { inTest } from '@/constants';
import type { BaseMigration, Migration, MigrationContext, MigrationFn } from '@db/types';
import { createSchemaBuilder } from '@db/dsl';
@ -89,10 +90,11 @@ function parseJson<T>(data: string | T): T {
return typeof data === 'string' ? jsonParse<T>(data) : data;
}
const dbType = config.getEnv('database.type');
const globalConfig = Container.get(GlobalConfig);
const dbType = globalConfig.database.type;
const isMysql = ['mariadb', 'mysqldb'].includes(dbType);
const dbName = config.getEnv(`database.${dbType === 'mariadb' ? 'mysqldb' : dbType}.database`);
const tablePrefix = config.getEnv('database.tablePrefix');
const dbName = globalConfig.database[dbType === 'mariadb' ? 'mysqldb' : dbType].database;
const tablePrefix = globalConfig.database.tablePrefix;
const createContext = (queryRunner: QueryRunner, migration: Migration): MigrationContext => ({
logger: Container.get(Logger),

View file

@ -1,6 +1,7 @@
import { jsonParse } from 'n8n-workflow';
import { Container } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import type { ValueTransformer, FindOperator } from '@n8n/typeorm';
import config from '@/config';
import { jsonParse } from 'n8n-workflow';
export const idStringifier = {
from: (value?: number): string | undefined => value?.toString(),
@ -29,7 +30,7 @@ export const objectRetriever: ValueTransformer = {
*/
const jsonColumn: ValueTransformer = {
to: (value: object): string | object =>
config.getEnv('database.type') === 'sqlite' ? JSON.stringify(value) : value,
Container.get(GlobalConfig).database.type === 'sqlite' ? JSON.stringify(value) : value,
from: (value: string | object): object => (typeof value === 'string' ? jsonParse(value) : value),
};

View file

@ -20,6 +20,7 @@ describe('ExecutionService', () => {
const concurrencyControl = mock<ConcurrencyControlService>();
const executionService = new ExecutionService(
mock(),
mock(),
queue,
activeExecutions,

View file

@ -1,4 +1,5 @@
import { Service } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import { validate as jsonSchemaValidate } from 'jsonschema';
import type {
IWorkflowBase,
@ -80,6 +81,7 @@ export const allowedExecutionsQueryFilterFields = Object.keys(
@Service()
export class ExecutionService {
constructor(
private readonly globalConfig: GlobalConfig,
private readonly logger: Logger,
private readonly queue: Queue,
private readonly activeExecutions: ActiveExecutions,
@ -337,7 +339,7 @@ export class ExecutionService {
async findRangeWithCount(query: ExecutionSummaries.RangeQuery) {
const results = await this.executionRepository.findManyByRangeQuery(query);
if (config.getEnv('database.type') === 'postgresdb') {
if (this.globalConfig.database.type === 'postgresdb') {
const liveRows = await this.executionRepository.getLiveExecutionRowsOnPostgres();
if (liveRows === -1) return { count: -1, estimated: false, results };

View file

@ -4,7 +4,7 @@ import uniq from 'lodash/uniq';
import { createWriteStream } from 'fs';
import { mkdir } from 'fs/promises';
import path from 'path';
import { GlobalConfig } from '@n8n/config';
import type {
ICredentialType,
IN8nUISettings,
@ -41,6 +41,7 @@ export class FrontendService {
private communityPackagesService?: CommunityPackagesService;
constructor(
private readonly globalConfig: GlobalConfig,
private readonly logger: Logger,
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
private readonly credentialTypes: CredentialTypes,
@ -85,7 +86,7 @@ export class FrontendService {
this.settings = {
isDocker: this.isDocker(),
databaseType: config.getEnv('database.type'),
databaseType: this.globalConfig.database.type,
previewMode: process.env.N8N_PREVIEW_MODE === 'true',
endpointForm: config.getEnv('endpoints.form'),
endpointFormTest: config.getEnv('endpoints.formTest'),

View file

@ -1,13 +1,14 @@
import { Service } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import { type IDataObject, type Workflow, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import { Logger } from '@/Logger';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { isWorkflowIdValid } from '@/utils';
import config from '@/config';
@Service()
export class WorkflowStaticDataService {
constructor(
private readonly globalConfig: GlobalConfig,
private readonly logger: Logger,
private readonly workflowRepository: WorkflowRepository,
) {}
@ -50,7 +51,7 @@ export class WorkflowStaticDataService {
.set({
staticData: newStaticData,
updatedAt: () => {
if (['mysqldb', 'mariadb'].includes(config.getEnv('database.type'))) {
if (['mysqldb', 'mariadb'].includes(this.globalConfig.database.type)) {
return 'updatedAt';
}
return '"updatedAt"';

View file

@ -1,11 +1,11 @@
import { Container } from 'typedi';
import type { Scope } from '@sentry/node';
import { GlobalConfig } from '@n8n/config';
import { Credentials } from 'n8n-core';
import { randomString } from 'n8n-workflow';
import type { ListQuery } from '@/requests';
import type { User } from '@db/entities/User';
import config from '@/config';
import { ProjectRepository } from '@db/repositories/project.repository';
import type { Project } from '@db/entities/Project';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
@ -1018,7 +1018,7 @@ describe('PATCH /credentials/:id', () => {
describe('GET /credentials/new', () => {
test('should return default name for new credential or its increment', async () => {
const name = config.getEnv('credentials.defaultName');
const name = Container.get(GlobalConfig).credentials.defaultName;
let tempName = name;
for (let i = 0; i < 4; i++) {

View file

@ -22,6 +22,7 @@ describe('ExecutionService', () => {
mock(),
mock(),
mock(),
mock(),
executionRepository,
Container.get(WorkflowRepository),
mock(),

View file

@ -1,10 +1,10 @@
import { Container } from 'typedi';
import type { DataSourceOptions, Repository } from '@n8n/typeorm';
import { DataSource as Connection } from '@n8n/typeorm';
import { Container } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import type { Class } from 'n8n-core';
import { randomString } from 'n8n-workflow';
import config from '@/config';
import * as Db from '@/Db';
import { getOptionOverrides } from '@db/config';
@ -14,7 +14,8 @@ export const testDbPrefix = 'n8n_test_';
* Initialize one test DB per suite run, with bootstrap connection if needed.
*/
export async function init() {
const dbType = config.getEnv('database.type');
const globalConfig = Container.get(GlobalConfig);
const dbType = globalConfig.database.type;
const testDbName = `${testDbPrefix}${randomString(6, 10).toLowerCase()}_${Date.now()}`;
if (dbType === 'postgresdb') {
@ -24,13 +25,13 @@ export async function init() {
await bootstrapPostgres.query(`CREATE DATABASE ${testDbName}`);
await bootstrapPostgres.destroy();
config.set('database.postgresdb.database', testDbName);
globalConfig.database.postgresdb.database = testDbName;
} else if (dbType === 'mysqldb' || dbType === 'mariadb') {
const bootstrapMysql = await new Connection(getBootstrapDBOptions('mysqldb')).initialize();
await bootstrapMysql.query(`CREATE DATABASE ${testDbName} DEFAULT CHARACTER SET utf8mb4`);
await bootstrapMysql.destroy();
config.set('database.mysqldb.database', testDbName);
globalConfig.database.mysqldb.database = testDbName;
}
await Db.init();
@ -89,12 +90,13 @@ export async function truncate(names: Array<(typeof repositories)[number]>) {
* Generate options for a bootstrap DB connection, to create and drop test databases.
*/
export const getBootstrapDBOptions = (dbType: 'postgresdb' | 'mysqldb'): DataSourceOptions => {
const globalConfig = Container.get(GlobalConfig);
const type = dbType === 'postgresdb' ? 'postgres' : 'mysql';
return {
type,
...getOptionOverrides(dbType),
database: type,
entityPrefix: config.getEnv('database.tablePrefix'),
schema: dbType === 'postgresdb' ? config.getEnv('database.postgresdb.schema') : undefined,
entityPrefix: globalConfig.database.tablePrefix,
schema: dbType === 'postgresdb' ? globalConfig.database.postgresdb.schema : undefined,
};
};

View file

@ -1,10 +1,11 @@
import 'tsconfig-paths/register';
import { Container } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import { DataSource as Connection } from '@n8n/typeorm';
import config from '@/config';
import { getBootstrapDBOptions, testDbPrefix } from './integration/shared/testDb';
export default async () => {
const dbType = config.getEnv('database.type');
const { type: dbType } = Container.get(GlobalConfig).database;
if (dbType !== 'postgresdb' && dbType !== 'mysqldb') return;
const connection = new Connection(getBootstrapDBOptions(dbType));

View file

@ -1,5 +1,5 @@
import { mock } from 'jest-mock-extended';
import config from '@/config';
import type { GlobalConfig } from '@n8n/config';
import { N8N_VERSION } from '@/constants';
import { InternalHooks } from '@/InternalHooks';
import type { License } from '@/License';
@ -16,7 +16,18 @@ jest.mock('node:os', () => ({
describe('InternalHooks', () => {
const telemetry = mock<Telemetry>();
const license = mock<License>();
const globalConfig = mock<GlobalConfig>({
database: {
type: 'sqlite',
},
userManagement: {
emails: {
mode: 'smtp',
},
},
});
const internalHooks = new InternalHooks(
globalConfig,
telemetry,
mock(),
mock(),
@ -26,6 +37,7 @@ describe('InternalHooks', () => {
license,
mock(),
mock(),
mock(),
);
beforeEach(() => jest.clearAllMocks());
@ -36,12 +48,13 @@ describe('InternalHooks', () => {
it('Should forward license plan name and tenant id to identify when provided', async () => {
license.getPlanName.mockReturnValue('Best Plan');
globalConfig.database.type = 'sqlite';
await internalHooks.onServerStarted();
expect(telemetry.identify).toHaveBeenCalledWith({
version_cli: N8N_VERSION,
db_type: config.get('database.type'),
db_type: 'sqlite',
n8n_version_notifications_enabled: true,
n8n_disable_production_main_process: false,
system_info: {

View file

@ -1,41 +1,88 @@
import config from '@/config';
import { NodeMailer } from '@/UserManagement/email/NodeMailer';
import { UserManagementMailer } from '@/UserManagement/email/UserManagementMailer';
import type { GlobalConfig } from '@n8n/config';
import { mock } from 'jest-mock-extended';
import type { InviteEmailData, PasswordResetData } from '@/UserManagement/email/Interfaces';
import { NodeMailer } from '@/UserManagement/email/NodeMailer';
import { UserManagementMailer } from '@/UserManagement/email/UserManagementMailer';
import { mockInstance } from '@test/mocking';
describe('UserManagementMailer', () => {
describe('expect NodeMailer.verifyConnection', () => {
let mockInit: jest.SpyInstance<Promise<void>, []>;
let mockVerifyConnection: jest.SpyInstance<Promise<void>, []>;
const email = 'test@user.com';
const nodeMailer = mockInstance(NodeMailer);
const inviteEmailData = mock<InviteEmailData>({
email,
inviteAcceptUrl: 'https://accept.url',
});
const passwordResetData = mock<PasswordResetData>({
email,
passwordResetUrl: 'https://reset.url',
});
beforeAll(() => {
mockVerifyConnection = jest
.spyOn(NodeMailer.prototype, 'verifyConnection')
.mockImplementation(async () => {});
mockInit = jest.spyOn(NodeMailer.prototype, 'init').mockImplementation(async () => {});
beforeEach(() => {
jest.clearAllMocks();
nodeMailer.sendMail.mockResolvedValue({ emailSent: true });
});
describe('when SMTP is not configured', () => {
const config = mock<GlobalConfig>({
userManagement: {
emails: {
mode: '',
},
},
});
const userManagementMailer = new UserManagementMailer(config, mock(), mock(), mock());
it('should not setup email transport', async () => {
expect(userManagementMailer.isEmailSetUp).toBe(false);
expect(userManagementMailer.mailer).toBeUndefined();
});
afterAll(() => {
mockVerifyConnection.mockRestore();
mockInit.mockRestore();
it('should not send emails', async () => {
const result = await userManagementMailer.invite(inviteEmailData);
expect(result.emailSent).toBe(false);
expect(nodeMailer.sendMail).not.toHaveBeenCalled();
});
});
describe('when SMTP is configured', () => {
const config = mock<GlobalConfig>({
userManagement: {
emails: {
mode: 'smtp',
smtp: {
host: 'email.host',
},
},
},
});
const userManagementMailer = new UserManagementMailer(config, mock(), mock(), mock());
it('should setup email transport', async () => {
expect(userManagementMailer.isEmailSetUp).toBe(true);
expect(userManagementMailer.mailer).toEqual(nodeMailer);
});
test('not be called when SMTP not set up', async () => {
const userManagementMailer = new UserManagementMailer(mock(), mock(), mock());
// NodeMailer.verifyConnection gets called only explicitly
await expect(async () => await userManagementMailer.verifyConnection()).rejects.toThrow();
expect(NodeMailer.prototype.verifyConnection).toHaveBeenCalledTimes(0);
it('should send invitation emails', async () => {
const result = await userManagementMailer.invite(inviteEmailData);
expect(result.emailSent).toBe(true);
expect(nodeMailer.sendMail).toHaveBeenCalledWith({
body: expect.stringContaining(
`<a href="${inviteEmailData.inviteAcceptUrl}" target="_blank">`,
),
emailRecipients: email,
subject: 'You have been invited to n8n',
});
});
test('to be called when SMTP set up', async () => {
// host needs to be set, otherwise smtp is skipped
config.set('userManagement.emails.smtp.host', 'host');
config.set('userManagement.emails.mode', 'smtp');
const userManagementMailer = new UserManagementMailer(mock(), mock(), mock());
// NodeMailer.verifyConnection gets called only explicitly
expect(async () => await userManagementMailer.verifyConnection()).not.toThrow();
it('should send password reset emails', async () => {
const result = await userManagementMailer.passwordReset(passwordResetData);
expect(result.emailSent).toBe(true);
expect(nodeMailer.sendMail).toHaveBeenCalledWith({
body: expect.stringContaining(`<a href="${passwordResetData.passwordResetUrl}">`),
emailRecipients: email,
subject: 'n8n password reset',
});
});
});
});

View file

@ -1,19 +1,19 @@
import Container from 'typedi';
import { GlobalConfig } from '@n8n/config';
import type { SelectQueryBuilder } from '@n8n/typeorm';
import { Not, LessThanOrEqual } from '@n8n/typeorm';
import config from '@/config';
import { ExecutionEntity } from '@db/entities/ExecutionEntity';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { mockEntityManager } from '../../shared/mocking';
import { mockInstance } from '../../shared/mocking';
import { BinaryDataService } from 'n8n-core';
import { nanoid } from 'nanoid';
import { mock } from 'jest-mock-extended';
import { ExecutionEntity } from '@db/entities/ExecutionEntity';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { mockEntityManager } from '../../shared/mocking';
import { mockInstance } from '../../shared/mocking';
describe('ExecutionRepository', () => {
const entityManager = mockEntityManager(ExecutionEntity);
const globalConfig = mockInstance(GlobalConfig);
const binaryDataService = mockInstance(BinaryDataService);
const executionRepository = Container.get(ExecutionRepository);
const mockDate = new Date('2023-12-28 12:34:56.789Z');
@ -26,10 +26,10 @@ describe('ExecutionRepository', () => {
afterAll(() => jest.useRealTimers());
describe('getWaitingExecutions()', () => {
test.each(['sqlite', 'postgres'])(
test.each(['sqlite', 'postgresdb'] as const)(
'on %s, should be called with expected args',
async (dbType) => {
jest.spyOn(config, 'getEnv').mockReturnValueOnce(dbType);
globalConfig.database.type = dbType;
entityManager.find.mockResolvedValueOnce([]);
await executionRepository.getWaitingExecutions();

View file

@ -1,3 +1,5 @@
import { Container } from 'typedi';
import { GlobalConfig } from '@n8n/config';
import type { IRun, WorkflowExecuteMode } from 'n8n-workflow';
import {
QueryFailedError,
@ -19,11 +21,12 @@ import { mockInstance } from '../../shared/mocking';
import type { Project } from '@/databases/entities/Project';
describe('EventsService', () => {
const dbType = config.getEnv('database.type');
const fakeUser = mock<User>({ id: 'abcde-fghij' });
const fakeProject = mock<Project>({ id: '12345-67890', type: 'personal' });
const ownershipService = mockInstance(OwnershipService);
const userService = mockInstance(UserService);
const globalConfig = Container.get(GlobalConfig);
const dbType = globalConfig.database.type;
const entityManager = mock<EntityManager>();
const dataSource = mock<DataSource>({
@ -43,7 +46,7 @@ describe('EventsService', () => {
const eventsService = new EventsService(
mock(),
new WorkflowStatisticsRepository(dataSource),
new WorkflowStatisticsRepository(dataSource, globalConfig),
ownershipService,
);

View file

@ -22,6 +22,7 @@
{ "path": "../workflow/tsconfig.build.json" },
{ "path": "../core/tsconfig.build.json" },
{ "path": "../@n8n/client-oauth2/tsconfig.build.json" },
{ "path": "../@n8n/config/tsconfig.build.json" },
{ "path": "../@n8n/permissions/tsconfig.build.json" }
]
}

View file

@ -217,6 +217,15 @@ importers:
specifier: ^1.7.0
version: 1.7.0
packages/@n8n/config:
dependencies:
reflect-metadata:
specifier: 0.2.2
version: 0.2.2
typedi:
specifier: 0.10.0
version: 0.10.0(patch_hash=sk6omkefrosihg7lmqbzh7vfxe)
packages/@n8n/imap:
dependencies:
iconv-lite:
@ -532,6 +541,9 @@ importers:
'@n8n/client-oauth2':
specifier: workspace:*
version: link:../@n8n/client-oauth2
'@n8n/config':
specifier: workspace:*
version: link:../@n8n/config
'@n8n/localtunnel':
specifier: 2.1.0
version: 2.1.0
@ -628,6 +640,9 @@ importers:
fast-glob:
specifier: 3.2.12
version: 3.2.12
flat:
specifier: 5.0.2
version: 5.0.2
flatted:
specifier: 3.2.7
version: 3.2.7
@ -821,6 +836,9 @@ importers:
'@types/express':
specifier: ^4.17.21
version: 4.17.21
'@types/flat':
specifier: ^5.0.5
version: 5.0.5
'@types/formidable':
specifier: ^3.4.5
version: 3.4.5
@ -5504,6 +5522,9 @@ packages:
'@types/find-cache-dir@3.2.1':
resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==}
'@types/flat@5.0.5':
resolution: {integrity: sha512-nPLljZQKSnac53KDUDzuzdRfGI0TDb5qPrb+SrQyN3MtdQrOnGsKniHN1iYZsJEBIVQve94Y6gNz22sgISZq+Q==}
'@types/formidable@3.4.5':
resolution: {integrity: sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==}
@ -13525,8 +13546,8 @@ packages:
vue-component-type-helpers@2.0.19:
resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==}
vue-component-type-helpers@2.0.24:
resolution: {integrity: sha512-Jr5N8QVYEcbQuMN1LRgvg61758G8HTnzUlQsAFOxx6Y6X8kmhJ7C+jOvWsQruYxi3uHhhS6BghyRlyiwO99DBg==}
vue-component-type-helpers@2.0.26:
resolution: {integrity: sha512-sO9qQ8oC520SW6kqlls0iqDak53gsTVSrYylajgjmkt1c0vcgjsGSy1KzlDrbEx8pm02IEYhlUkU5hCYf8rwtg==}
vue-demi@0.14.5:
resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
@ -17166,17 +17187,6 @@ snapshots:
- langchain
- openai
'@langchain/pinecone@0.0.6(langchain@0.2.2(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@pinecone-database/pinecone@2.1.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.6.7)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2)(fast-xml-parser@4.3.5)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(openai@4.52.1(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.52.1(encoding@0.1.13))':
dependencies:
'@langchain/core': 0.2.9(langchain@0.2.2(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@pinecone-database/pinecone@2.1.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.6.7)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2)(fast-xml-parser@4.3.5)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(openai@4.52.1(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.52.1(encoding@0.1.13))
'@pinecone-database/pinecone': 2.2.1
flat: 5.0.2
uuid: 9.0.1
transitivePeerDependencies:
- langchain
- openai
'@langchain/qdrant@0.0.5(langchain@0.2.2(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@google-ai/generativelanguage@2.5.0(encoding@0.1.13))(@pinecone-database/pinecone@2.2.1)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.6.7)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.3.5)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(openai@4.47.1(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.12)(ws@8.17.1))(openai@4.47.1(encoding@0.1.13))(typescript@5.5.2)':
dependencies:
'@langchain/core': 0.2.9(langchain@0.2.2(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@google-ai/generativelanguage@2.5.0(encoding@0.1.13))(@pinecone-database/pinecone@2.2.1)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.6.7)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.3.5)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(openai@4.47.1(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.12)(ws@8.17.1))(openai@4.47.1(encoding@0.1.13))
@ -17187,7 +17197,6 @@ snapshots:
- openai
- typescript
'@langchain/redis@0.0.5(langchain@0.2.2(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@google-ai/generativelanguage@2.5.0(encoding@0.1.13))(@pinecone-database/pinecone@2.2.1)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.6.7)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.3.5)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(openai@4.47.1(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.12)(ws@8.17.1))(openai@4.47.1(encoding@0.1.13))':
dependencies:
'@langchain/core': 0.2.9(langchain@0.2.2(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@google-ai/generativelanguage@2.5.0(encoding@0.1.13))(@pinecone-database/pinecone@2.2.1)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.6.7)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.3.5)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(openai@4.47.1(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.12)(ws@8.17.1))(openai@4.47.1(encoding@0.1.13))
@ -19308,7 +19317,7 @@ snapshots:
ts-dedent: 2.2.0
type-fest: 2.19.0
vue: 3.4.21(typescript@5.5.2)
vue-component-type-helpers: 2.0.24
vue-component-type-helpers: 2.0.26
transitivePeerDependencies:
- encoding
- prettier
@ -19590,6 +19599,8 @@ snapshots:
'@types/find-cache-dir@3.2.1': {}
'@types/flat@5.0.5': {}
'@types/formidable@3.4.5':
dependencies:
'@types/node': 18.16.16
@ -22508,7 +22519,7 @@ snapshots:
eslint-import-resolver-node@0.3.9:
dependencies:
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
is-core-module: 2.13.1
resolve: 1.22.8
transitivePeerDependencies:
@ -22533,7 +22544,7 @@ snapshots:
eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
optionalDependencies:
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.2)
eslint: 8.57.0
@ -22553,7 +22564,7 @@ snapshots:
array.prototype.findlastindex: 1.2.3
array.prototype.flat: 1.3.2
array.prototype.flatmap: 1.3.2
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
@ -23088,7 +23099,7 @@ snapshots:
follow-redirects@1.15.6(debug@3.2.7):
optionalDependencies:
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
follow-redirects@1.15.6(debug@4.3.4):
optionalDependencies:
@ -23437,7 +23448,7 @@ snapshots:
array-parallel: 0.1.3
array-series: 0.1.5
cross-spawn: 4.0.2
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@ -26243,7 +26254,7 @@ snapshots:
pdf-parse@1.1.1:
dependencies:
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
node-ensure: 0.0.0
transitivePeerDependencies:
- supports-color
@ -27274,7 +27285,7 @@ snapshots:
rhea@1.0.24:
dependencies:
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@ -27677,7 +27688,7 @@ snapshots:
binascii: 0.0.2
bn.js: 5.2.1
browser-request: 0.3.3
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
expand-tilde: 2.0.2
extend: 3.0.2
fast-xml-parser: 4.2.7
@ -28932,7 +28943,7 @@ snapshots:
vue-component-type-helpers@2.0.19: {}
vue-component-type-helpers@2.0.24: {}
vue-component-type-helpers@2.0.26: {}
vue-demi@0.14.5(vue@3.4.21(typescript@5.5.2)):
dependencies: