mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-23 11:44:06 -08:00
refactor(core): Move backend config to a separate package (no-changelog) (#9325)
This commit is contained in:
parent
1d5b9836ca
commit
c7d4b471c4
10
packages/@n8n/config/.eslintrc.js
Normal file
10
packages/@n8n/config/.eslintrc.js
Normal 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),
|
||||
};
|
2
packages/@n8n/config/jest.config.js
Normal file
2
packages/@n8n/config/jest.config.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
/** @type {import('jest').Config} */
|
||||
module.exports = require('../../../jest.config');
|
26
packages/@n8n/config/package.json
Normal file
26
packages/@n8n/config/package.json
Normal 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"
|
||||
}
|
||||
}
|
25
packages/@n8n/config/src/configs/credentials.ts
Normal file
25
packages/@n8n/config/src/configs/credentials.ts
Normal 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;
|
||||
}
|
151
packages/@n8n/config/src/configs/database.ts
Normal file
151
packages/@n8n/config/src/configs/database.ts
Normal 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;
|
||||
}
|
78
packages/@n8n/config/src/configs/email.ts
Normal file
78
packages/@n8n/config/src/configs/email.ts
Normal 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;
|
||||
}
|
79
packages/@n8n/config/src/decorators.ts
Normal file
79
packages/@n8n/config/src/decorators.ts
Normal 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);
|
||||
};
|
22
packages/@n8n/config/src/index.ts
Normal file
22
packages/@n8n/config/src/index.ts
Normal 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;
|
||||
}
|
145
packages/@n8n/config/test/config.test.ts
Normal file
145
packages/@n8n/config/test/config.test.ts
Normal 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();
|
||||
});
|
||||
});
|
11
packages/@n8n/config/tsconfig.build.json
Normal file
11
packages/@n8n/config/tsconfig.build.json
Normal 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/**"]
|
||||
}
|
13
packages/@n8n/config/tsconfig.json
Normal file
13
packages/@n8n/config/tsconfig.json
Normal 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"]
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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.',
|
||||
});
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;');
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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')",
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}';`;
|
||||
|
||||
|
|
|
@ -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('.');
|
||||
|
||||
|
|
|
@ -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"';
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ describe('ExecutionService', () => {
|
|||
const concurrencyControl = mock<ConcurrencyControlService>();
|
||||
|
||||
const executionService = new ExecutionService(
|
||||
mock(),
|
||||
mock(),
|
||||
queue,
|
||||
activeExecutions,
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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"';
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('ExecutionService', () => {
|
|||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
executionRepository,
|
||||
Container.get(WorkflowRepository),
|
||||
mock(),
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue