refactor(core): Introduce @n8n/decorators package (#14784)

This commit is contained in:
Iván Ovejero
2025-04-23 16:39:49 +02:00
committed by GitHub
parent 454e5c77ad
commit e6381e09e3
95 changed files with 447 additions and 228 deletions

View File

@@ -37,6 +37,7 @@ component_management:
- packages/@n8n/api-types/** - packages/@n8n/api-types/**
- packages/@n8n/config/** - packages/@n8n/config/**
- packages/@n8n/client-oauth2/** - packages/@n8n/client-oauth2/**
- packages/@n8n/decorators/**
- packages/@n8n/constants/** - packages/@n8n/constants/**
- packages/@n8n/di/** - packages/@n8n/di/**
- packages/@n8n/imap/** - packages/@n8n/imap/**

View File

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

View File

@@ -0,0 +1,7 @@
/** @type {import('jest').Config} */
module.exports = {
...require('../../../jest.config'),
transform: {
'^.+\\.ts$': ['ts-jest', { isolatedModules: false }],
},
};

View File

@@ -0,0 +1,35 @@
{
"name": "@n8n/decorators",
"version": "0.1.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"format": "biome format --write .",
"format:check": "biome ci .",
"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/**/*"
],
"devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@types/express": "catalog:",
"@types/lodash": "catalog:"
},
"dependencies": {
"@n8n/constants": "workspace:^",
"@n8n/di": "workspace:^",
"@n8n/permissions": "workspace:^",
"lodash": "catalog:",
"n8n-workflow": "workspace:^"
}
}

View File

@@ -1,8 +1,7 @@
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { ExecutionLifecycleHooks } from 'n8n-core';
import type { BaseN8nModule } from '../module'; import type { BaseN8nModule, ExecutionLifecycleHooks } from '../module';
import { ModuleRegistry, N8nModule } from '../module'; import { ModuleRegistry, N8nModule } from '../module';
let moduleRegistry: ModuleRegistry; let moduleRegistry: ModuleRegistry;

View File

@@ -1,16 +1,15 @@
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import { OnShutdown } from '@/decorators/on-shutdown'; import { OnShutdown } from '../on-shutdown';
import { ShutdownService } from '@/shutdown/shutdown.service'; import { ShutdownRegistryMetadata } from '../shutdown-registry-metadata';
describe('OnShutdown', () => { describe('OnShutdown', () => {
let shutdownService: ShutdownService; let shutdownRegistryMetadata: ShutdownRegistryMetadata;
beforeEach(() => { beforeEach(() => {
shutdownService = new ShutdownService(mock(), mock()); shutdownRegistryMetadata = new ShutdownRegistryMetadata();
Container.set(ShutdownService, shutdownService); Container.set(ShutdownRegistryMetadata, shutdownRegistryMetadata);
jest.spyOn(shutdownService, 'register'); jest.spyOn(shutdownRegistryMetadata, 'register');
}); });
it('should register a methods that is decorated with OnShutdown', () => { it('should register a methods that is decorated with OnShutdown', () => {
@@ -20,8 +19,8 @@ describe('OnShutdown', () => {
async onShutdown() {} async onShutdown() {}
} }
expect(shutdownService.register).toHaveBeenCalledTimes(1); expect(shutdownRegistryMetadata.register).toHaveBeenCalledTimes(1);
expect(shutdownService.register).toHaveBeenCalledWith(100, { expect(shutdownRegistryMetadata.register).toHaveBeenCalledWith(100, {
methodName: 'onShutdown', methodName: 'onShutdown',
serviceClass: TestClass, serviceClass: TestClass,
}); });
@@ -37,12 +36,12 @@ describe('OnShutdown', () => {
async two() {} async two() {}
} }
expect(shutdownService.register).toHaveBeenCalledTimes(2); expect(shutdownRegistryMetadata.register).toHaveBeenCalledTimes(2);
expect(shutdownService.register).toHaveBeenCalledWith(100, { expect(shutdownRegistryMetadata.register).toHaveBeenCalledWith(100, {
methodName: 'one', methodName: 'one',
serviceClass: TestClass, serviceClass: TestClass,
}); });
expect(shutdownService.register).toHaveBeenCalledWith(100, { expect(shutdownRegistryMetadata.register).toHaveBeenCalledWith(100, {
methodName: 'two', methodName: 'two',
serviceClass: TestClass, serviceClass: TestClass,
}); });
@@ -57,9 +56,9 @@ describe('OnShutdown', () => {
} }
} }
expect(shutdownService.register).toHaveBeenCalledTimes(1); expect(shutdownRegistryMetadata.register).toHaveBeenCalledTimes(1);
// @ts-expect-error We are checking internal parts of the shutdown service // @ts-expect-error We are checking internal parts of the shutdown service
expect(shutdownService.handlersByPriority[10].length).toEqual(1); expect(shutdownRegistryMetadata.handlersByPriority[10].length).toEqual(1);
}); });
it('should throw an error if the decorated member is not a function', () => { it('should throw an error if the decorated member is not a function', () => {

View File

@@ -1,10 +1,15 @@
import { getRouteMetadata } from './controller.registry'; import { Container } from '@n8n/di';
import { ControllerRegistryMetadata } from './controller-registry-metadata';
import type { Arg, Controller } from './types'; import type { Arg, Controller } from './types';
const ArgDecorator = const ArgDecorator =
(arg: Arg): ParameterDecorator => (arg: Arg): ParameterDecorator =>
(target, handlerName, parameterIndex) => { (target, handlerName, parameterIndex) => {
const routeMetadata = getRouteMetadata(target.constructor as Controller, String(handlerName)); const routeMetadata = Container.get(ControllerRegistryMetadata).getRouteMetadata(
target.constructor as Controller,
String(handlerName),
);
routeMetadata.args[parameterIndex] = arg; routeMetadata.args[parameterIndex] = arg;
}; };

View File

@@ -0,0 +1,36 @@
import { Service } from '@n8n/di';
import type { Controller, ControllerMetadata, HandlerName, RouteMetadata } from './types';
@Service()
export class ControllerRegistryMetadata {
private registry = new Map<Controller, ControllerMetadata>();
getControllerMetadata(controllerClass: Controller) {
let metadata = this.registry.get(controllerClass);
if (!metadata) {
metadata = {
basePath: '/',
middlewares: [],
routes: new Map(),
};
this.registry.set(controllerClass, metadata);
}
return metadata;
}
getRouteMetadata(controllerClass: Controller, handlerName: HandlerName) {
const metadata = this.getControllerMetadata(controllerClass);
let route = metadata.routes.get(handlerName);
if (!route) {
route = {} as RouteMetadata;
route.args = [];
metadata.routes.set(handlerName, route);
}
return route;
}
get controllerClasses() {
return this.registry.keys();
}
}

View File

@@ -19,7 +19,7 @@ export const Debounce =
(waitMs: number): MethodDecorator => (waitMs: number): MethodDecorator =>
<T>( <T>(
_: object, _: object,
methodName: string, methodName: string | symbol,
originalDescriptor: PropertyDescriptor, originalDescriptor: PropertyDescriptor,
): TypedPropertyDescriptor<T> => ({ ): TypedPropertyDescriptor<T> => ({
configurable: true, configurable: true,

View File

@@ -0,0 +1,20 @@
export { Body, Query, Param } from './args';
export { RestController } from './rest-controller';
export { Get, Post, Put, Patch, Delete } from './route';
export { Middleware } from './middleware';
export { ControllerRegistryMetadata } from './controller-registry-metadata';
export { Licensed } from './licensed';
export { GlobalScope, ProjectScope } from './scoped';
export {
HIGHEST_SHUTDOWN_PRIORITY,
DEFAULT_SHUTDOWN_PRIORITY,
LOWEST_SHUTDOWN_PRIORITY,
} from './shutdown/constants';
export { ShutdownRegistryMetadata } from './shutdown-registry-metadata';
export { ModuleRegistry } from './module';
export { OnShutdown } from './on-shutdown';
export { Redactable } from './redactable';
export { BaseN8nModule, N8nModule } from './module';
export { Debounce } from './debounce';
export type { AccessScope, Controller, RateLimit } from './types';
export type { ShutdownHandler } from './types';

View File

@@ -1,11 +1,15 @@
import type { BooleanLicenseFeature } from '@n8n/constants'; import type { BooleanLicenseFeature } from '@n8n/constants';
import { Container } from '@n8n/di';
import { getRouteMetadata } from './controller.registry'; import { ControllerRegistryMetadata } from './controller-registry-metadata';
import type { Controller } from './types'; import type { Controller } from './types';
export const Licensed = export const Licensed =
(licenseFeature: BooleanLicenseFeature): MethodDecorator => (licenseFeature: BooleanLicenseFeature): MethodDecorator =>
(target, handlerName) => { (target, handlerName) => {
const routeMetadata = getRouteMetadata(target.constructor as Controller, String(handlerName)); const routeMetadata = Container.get(ControllerRegistryMetadata).getRouteMetadata(
target.constructor as Controller,
String(handlerName),
);
routeMetadata.licenseFeature = licenseFeature; routeMetadata.licenseFeature = licenseFeature;
}; };

View File

@@ -0,0 +1,11 @@
import { Container } from '@n8n/di';
import { ControllerRegistryMetadata } from './controller-registry-metadata';
import type { Controller } from './types';
export const Middleware = (): MethodDecorator => (target, handlerName) => {
const metadata = Container.get(ControllerRegistryMetadata).getControllerMetadata(
target.constructor as Controller,
);
metadata.middlewares.push(String(handlerName));
};

View File

@@ -1,7 +1,15 @@
import { Container, Service, type Constructable } from '@n8n/di'; import { Container, Service, type Constructable } from '@n8n/di';
import type { ExecutionLifecycleHooks } from 'n8n-core'; import type EventEmitter from 'node:events';
import type { MultiMainSetup } from '@/scaling/multi-main-setup.ee'; /**
* @TODO Temporary dummy type until `MultiMainSetup` registers listeners via decorators.
*/
type MultiMainSetup = EventEmitter;
/**
* @TODO Temporary dummy type until `ExecutionLifecycleHooks` registers hooks via decorators.
*/
export type ExecutionLifecycleHooks = object;
export interface BaseN8nModule { export interface BaseN8nModule {
initialize?(): void; initialize?(): void;

View File

@@ -1,8 +1,9 @@
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { UnexpectedError } from 'n8n-workflow'; import { UnexpectedError } from 'n8n-workflow';
import { DEFAULT_SHUTDOWN_PRIORITY } from '@/constants'; import { DEFAULT_SHUTDOWN_PRIORITY } from './shutdown/constants';
import { type ServiceClass, ShutdownService } from '@/shutdown/shutdown.service'; import { ShutdownRegistryMetadata } from './shutdown-registry-metadata';
import type { ServiceClass } from './types';
/** /**
* Decorator that registers a method as a shutdown hook. The method will * Decorator that registers a method as a shutdown hook. The method will
@@ -30,7 +31,7 @@ export const OnShutdown =
const methodName = String(propertyKey); const methodName = String(propertyKey);
// TODO: assert that serviceClass is decorated with @Service // TODO: assert that serviceClass is decorated with @Service
if (typeof descriptor?.value === 'function') { if (typeof descriptor?.value === 'function') {
Container.get(ShutdownService).register(priority, { serviceClass, methodName }); Container.get(ShutdownRegistryMetadata).register(priority, { serviceClass, methodName });
} else { } else {
const name = `${serviceClass.name}.${methodName}()`; const name = `${serviceClass.name}.${methodName}()`;
throw new UnexpectedError( throw new UnexpectedError(

View File

@@ -1,5 +1,20 @@
import { RedactableError } from '@/errors/redactable.error'; import { UnexpectedError } from 'n8n-workflow';
import type { UserLike } from '@/events/maps/relay.event-map';
type UserLike = {
id: string;
email?: string;
firstName?: string;
lastName?: string;
role: string;
};
export class RedactableError extends UnexpectedError {
constructor(fieldName: string, args: string) {
super(
`Failed to find "${fieldName}" property in argument "${args.toString()}". Please set the decorator \`@Redactable()\` only on \`LogStreamingEventRelay\` methods where the argument contains a "${fieldName}" property.`,
);
}
}
function toRedactable(userLike: UserLike) { function toRedactable(userLike: UserLike) {
return { return {
@@ -28,6 +43,7 @@ type FieldName = 'user' | 'inviter' | 'invitee';
export const Redactable = export const Redactable =
(fieldName: FieldName = 'user'): MethodDecorator => (fieldName: FieldName = 'user'): MethodDecorator =>
(_target, _propertyName, propertyDescriptor: PropertyDescriptor) => { (_target, _propertyName, propertyDescriptor: PropertyDescriptor) => {
// eslint-disable-next-line @typescript-eslint/ban-types
const originalMethod = propertyDescriptor.value as Function; const originalMethod = propertyDescriptor.value as Function;
type MethodArgs = Array<{ [fieldName: string]: UserLike }>; type MethodArgs = Array<{ [fieldName: string]: UserLike }>;

View File

@@ -1,12 +1,14 @@
import { Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import { getControllerMetadata } from './controller.registry'; import { ControllerRegistryMetadata } from './controller-registry-metadata';
import type { Controller } from './types'; import type { Controller } from './types';
export const RestController = export const RestController =
(basePath: `/${string}` = '/'): ClassDecorator => (basePath: `/${string}` = '/'): ClassDecorator =>
(target) => { (target) => {
const metadata = getControllerMetadata(target as unknown as Controller); const metadata = Container.get(ControllerRegistryMetadata).getControllerMetadata(
target as unknown as Controller,
);
metadata.basePath = basePath; metadata.basePath = basePath;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Service()(target); return Service()(target);

View File

@@ -1,6 +1,7 @@
import { Container } from '@n8n/di';
import type { RequestHandler } from 'express'; import type { RequestHandler } from 'express';
import { getRouteMetadata } from './controller.registry'; import { ControllerRegistryMetadata } from './controller-registry-metadata';
import type { Controller, Method, RateLimit } from './types'; import type { Controller, Method, RateLimit } from './types';
interface RouteOptions { interface RouteOptions {
@@ -16,7 +17,10 @@ const RouteFactory =
(method: Method) => (method: Method) =>
(path: `/${string}`, options: RouteOptions = {}): MethodDecorator => (path: `/${string}`, options: RouteOptions = {}): MethodDecorator =>
(target, handlerName) => { (target, handlerName) => {
const routeMetadata = getRouteMetadata(target.constructor as Controller, String(handlerName)); const routeMetadata = Container.get(ControllerRegistryMetadata).getRouteMetadata(
target.constructor as Controller,
String(handlerName),
);
routeMetadata.method = method; routeMetadata.method = method;
routeMetadata.path = path; routeMetadata.path = path;
routeMetadata.middlewares = options.middlewares ?? []; routeMetadata.middlewares = options.middlewares ?? [];

View File

@@ -1,12 +1,16 @@
import { Container } from '@n8n/di';
import type { Scope } from '@n8n/permissions'; import type { Scope } from '@n8n/permissions';
import { getRouteMetadata } from './controller.registry'; import { ControllerRegistryMetadata } from './controller-registry-metadata';
import type { Controller } from './types'; import type { Controller } from './types';
const Scoped = const Scoped =
(scope: Scope, { globalOnly } = { globalOnly: false }): MethodDecorator => (scope: Scope, { globalOnly } = { globalOnly: false }): MethodDecorator =>
(target, handlerName) => { (target, handlerName) => {
const routeMetadata = getRouteMetadata(target.constructor as Controller, String(handlerName)); const routeMetadata = Container.get(ControllerRegistryMetadata).getRouteMetadata(
target.constructor as Controller,
String(handlerName),
);
routeMetadata.accessScope = { scope, globalOnly }; routeMetadata.accessScope = { scope, globalOnly };
}; };

View File

@@ -0,0 +1,31 @@
import { Service } from '@n8n/di';
import { UserError } from 'n8n-workflow';
import { HIGHEST_SHUTDOWN_PRIORITY, LOWEST_SHUTDOWN_PRIORITY } from './shutdown/constants';
import type { ShutdownHandler } from './types';
@Service()
export class ShutdownRegistryMetadata {
private handlersByPriority: ShutdownHandler[][] = [];
register(priority: number, handler: ShutdownHandler) {
if (priority < LOWEST_SHUTDOWN_PRIORITY || priority > HIGHEST_SHUTDOWN_PRIORITY) {
throw new UserError(
`Invalid shutdown priority. Please set it between ${LOWEST_SHUTDOWN_PRIORITY} and ${HIGHEST_SHUTDOWN_PRIORITY}.`,
{ extra: { priority } },
);
}
if (!this.handlersByPriority[priority]) this.handlersByPriority[priority] = [];
this.handlersByPriority[priority].push(handler);
}
getHandlersByPriority(): ShutdownHandler[][] {
return this.handlersByPriority;
}
clear() {
this.handlersByPriority = [];
}
}

View File

@@ -0,0 +1,3 @@
export const LOWEST_SHUTDOWN_PRIORITY = 0;
export const DEFAULT_SHUTDOWN_PRIORITY = 100;
export const HIGHEST_SHUTDOWN_PRIORITY = 200;

View File

@@ -47,3 +47,14 @@ export interface ControllerMetadata {
export type Controller = Constructable<object> & export type Controller = Constructable<object> &
Record<HandlerName, (...args: unknown[]) => Promise<unknown>>; Record<HandlerName, (...args: unknown[]) => Promise<unknown>>;
type RouteHandlerFn = () => Promise<void> | void;
type Class<T = object, A extends unknown[] = unknown[]> = new (...args: A) => T;
export type ServiceClass = Class<Record<string, RouteHandlerFn>>;
export interface ShutdownHandler {
serviceClass: ServiceClass;
methodName: string;
}

View File

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

View File

@@ -0,0 +1,12 @@
{
"extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": {
"rootDir": ".",
"types": ["node", "jest"],
"baseUrl": "src",
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*.ts"]
}

View File

@@ -93,6 +93,7 @@
"@n8n/client-oauth2": "workspace:*", "@n8n/client-oauth2": "workspace:*",
"@n8n/config": "workspace:*", "@n8n/config": "workspace:*",
"@n8n/constants": "workspace:^", "@n8n/constants": "workspace:^",
"@n8n/decorators": "workspace:*",
"@n8n/di": "workspace:*", "@n8n/di": "workspace:*",
"@n8n/localtunnel": "3.0.0", "@n8n/localtunnel": "3.0.0",
"@n8n/n8n-nodes-langchain": "workspace:*", "@n8n/n8n-nodes-langchain": "workspace:*",

View File

@@ -3,27 +3,30 @@ jest.mock('@/constants', () => ({
})); }));
import type { GlobalConfig } from '@n8n/config'; import type { GlobalConfig } from '@n8n/config';
import { ControllerRegistryMetadata } from '@n8n/decorators';
import { Param } from '@n8n/decorators';
import { Get, Licensed, RestController } from '@n8n/decorators';
import { Container } from '@n8n/di';
import express from 'express'; import express from 'express';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { agent as testAgent } from 'supertest'; import { agent as testAgent } from 'supertest';
import type { AuthService } from '@/auth/auth.service'; import type { AuthService } from '@/auth/auth.service';
import { ControllerRegistry, Get, Licensed, RestController } from '@/decorators'; import { ControllerRegistry } from '@/controller.registry';
import type { License } from '@/license'; import type { License } from '@/license';
import type { SuperAgentTest } from '@test-integration/types'; import type { SuperAgentTest } from '@test-integration/types';
import { Param } from '../args';
describe('ControllerRegistry', () => { describe('ControllerRegistry', () => {
const license = mock<License>(); const license = mock<License>();
const authService = mock<AuthService>(); const authService = mock<AuthService>();
const globalConfig = mock<GlobalConfig>({ endpoints: { rest: 'rest' } }); const globalConfig = mock<GlobalConfig>({ endpoints: { rest: 'rest' } });
const metadata = Container.get(ControllerRegistryMetadata);
let agent: SuperAgentTest; let agent: SuperAgentTest;
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
const app = express(); const app = express();
new ControllerRegistry(license, authService, globalConfig).activate(app); new ControllerRegistry(license, authService, globalConfig, metadata).activate(app);
agent = testAgent(app); agent = testAgent(app);
}); });

View File

@@ -1,4 +1,5 @@
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import { OnShutdown } from '@n8n/decorators';
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import compression from 'compression'; import compression from 'compression';
import express from 'express'; import express from 'express';
@@ -11,7 +12,6 @@ import { Logger } from 'n8n-core';
import config from '@/config'; import config from '@/config';
import { N8N_VERSION, TEMPLATES_DIR, inDevelopment, inTest } from '@/constants'; import { N8N_VERSION, TEMPLATES_DIR, inDevelopment, inTest } from '@/constants';
import * as Db from '@/db'; import * as Db from '@/db';
import { OnShutdown } from '@/decorators/on-shutdown';
import { ExternalHooks } from '@/external-hooks'; import { ExternalHooks } from '@/external-hooks';
import { rawBodyReader, bodyParser, corsMiddleware } from '@/middlewares'; import { rawBodyReader, bodyParser, corsMiddleware } from '@/middlewares';
import { send, sendErrorResponse } from '@/response-helper'; import { send, sendErrorResponse } from '@/response-helper';

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { WorkflowsConfig } from '@n8n/config'; import { WorkflowsConfig } from '@n8n/config';
import { OnShutdown } from '@n8n/decorators';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import { chunk } from 'lodash'; import { chunk } from 'lodash';
import { import {
@@ -42,7 +43,6 @@ import {
} from '@/constants'; } from '@/constants';
import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { OnShutdown } from '@/decorators/on-shutdown';
import { executeErrorWorkflow } from '@/execution-lifecycle/execute-error-workflow'; import { executeErrorWorkflow } from '@/execution-lifecycle/execute-error-workflow';
import { ExecutionService } from '@/executions/execution.service'; import { ExecutionService } from '@/executions/execution.service';
import { ExternalHooks } from '@/external-hooks'; import { ExternalHooks } from '@/external-hooks';

View File

@@ -1,6 +1,7 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import { LICENSE_FEATURES } from '@n8n/constants'; import { LICENSE_FEATURES } from '@n8n/constants';
import { ModuleRegistry } from '@n8n/decorators';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { Command, Errors } from '@oclif/core'; import { Command, Errors } from '@oclif/core';
import { import {
@@ -19,7 +20,6 @@ import config from '@/config';
import { N8N_VERSION, N8N_RELEASE_DATE, inDevelopment, inTest } from '@/constants'; import { N8N_VERSION, N8N_RELEASE_DATE, inDevelopment, inTest } from '@/constants';
import * as CrashJournal from '@/crash-journal'; import * as CrashJournal from '@/crash-journal';
import * as Db from '@/db'; import * as Db from '@/db';
import { ModuleRegistry } from '@/decorators/module';
import { getDataDeduplicationService } from '@/deduplication'; import { getDataDeduplicationService } from '@/deduplication';
import { DeprecationService } from '@/deprecation/deprecation.service'; import { DeprecationService } from '@/deprecation/deprecation.service';
import { TestRunnerService } from '@/evaluation.ee/test-runner/test-runner.service.ee'; import { TestRunnerService } from '@/evaluation.ee/test-runner/test-runner.service.ee';

View File

@@ -1,5 +1,7 @@
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import type { BooleanLicenseFeature } from '@n8n/constants'; import type { BooleanLicenseFeature } from '@n8n/constants';
import { ControllerRegistryMetadata } from '@n8n/decorators';
import type { AccessScope, Controller, RateLimit } from '@n8n/decorators';
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import { Router } from 'express'; import { Router } from 'express';
import type { Application, Request, Response, RequestHandler } from 'express'; import type { Application, Request, Response, RequestHandler } from 'express';
@@ -15,57 +17,23 @@ import { userHasScopes } from '@/permissions.ee/check-access';
import type { AuthenticatedRequest } from '@/requests'; import type { AuthenticatedRequest } from '@/requests';
import { send } from '@/response-helper'; // TODO: move `ResponseHelper.send` to this file import { send } from '@/response-helper'; // TODO: move `ResponseHelper.send` to this file
import type {
AccessScope,
Controller,
ControllerMetadata,
HandlerName,
RateLimit,
RouteMetadata,
} from './types';
const registry = new Map<Controller, ControllerMetadata>();
export const getControllerMetadata = (controllerClass: Controller) => {
let metadata = registry.get(controllerClass);
if (!metadata) {
metadata = {
basePath: '/',
middlewares: [],
routes: new Map(),
};
registry.set(controllerClass, metadata);
}
return metadata;
};
export const getRouteMetadata = (controllerClass: Controller, handlerName: HandlerName) => {
const metadata = getControllerMetadata(controllerClass);
let route = metadata.routes.get(handlerName);
if (!route) {
route = {} as RouteMetadata;
route.args = [];
metadata.routes.set(handlerName, route);
}
return route;
};
@Service() @Service()
export class ControllerRegistry { export class ControllerRegistry {
constructor( constructor(
private readonly license: License, private readonly license: License,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly globalConfig: GlobalConfig, private readonly globalConfig: GlobalConfig,
private readonly metadata: ControllerRegistryMetadata,
) {} ) {}
activate(app: Application) { activate(app: Application) {
for (const controllerClass of registry.keys()) { for (const controllerClass of this.metadata.controllerClasses) {
this.activateController(app, controllerClass); this.activateController(app, controllerClass);
} }
} }
private activateController(app: Application, controllerClass: Controller) { private activateController(app: Application, controllerClass: Controller) {
const metadata = registry.get(controllerClass)!; const metadata = this.metadata.getControllerMetadata(controllerClass);
const router = Router({ mergeParams: true }); const router = Router({ mergeParams: true });
const prefix = `/${this.globalConfig.endpoints.rest}/${metadata.basePath}` const prefix = `/${this.globalConfig.endpoints.rest}/${metadata.basePath}`

View File

@@ -1,4 +1,5 @@
import { Get, RestController } from '@/decorators'; import { Get, RestController } from '@n8n/decorators';
import { ActiveWorkflowRequest } from '@/requests'; import { ActiveWorkflowRequest } from '@/requests';
import { ActiveWorkflowsService } from '@/services/active-workflows.service'; import { ActiveWorkflowsService } from '@/services/active-workflows.service';

View File

@@ -5,6 +5,7 @@ import {
AiAskRequestDto, AiAskRequestDto,
AiFreeCreditsRequestDto, AiFreeCreditsRequestDto,
} from '@n8n/api-types'; } from '@n8n/api-types';
import { Body, Post, RestController } from '@n8n/decorators';
import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk'; import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk';
import { Response } from 'express'; import { Response } from 'express';
import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow'; import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
@@ -13,7 +14,6 @@ import { WritableStream } from 'node:stream/web';
import { FREE_AI_CREDITS_CREDENTIAL_NAME } from '@/constants'; import { FREE_AI_CREDITS_CREDENTIAL_NAME } from '@/constants';
import { CredentialsService } from '@/credentials/credentials.service'; import { CredentialsService } from '@/credentials/credentials.service';
import { Body, Post, RestController } from '@/decorators';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import { AiService } from '@/services/ai.service'; import { AiService } from '@/services/ai.service';

View File

@@ -1,4 +1,5 @@
import { Delete, Get, Patch, Post, RestController, GlobalScope } from '@/decorators'; import { Delete, Get, Patch, Post, RestController, GlobalScope } from '@n8n/decorators';
import { AnnotationTagsRequest } from '@/requests'; import { AnnotationTagsRequest } from '@/requests';
import { AnnotationTagService } from '@/services/annotation-tag.service.ee'; import { AnnotationTagService } from '@/services/annotation-tag.service.ee';

View File

@@ -1,7 +1,7 @@
import { CreateApiKeyRequestDto, UpdateApiKeyRequestDto } from '@n8n/api-types'; import { CreateApiKeyRequestDto, UpdateApiKeyRequestDto } from '@n8n/api-types';
import { Body, Delete, Get, Param, Patch, Post, RestController } from '@n8n/decorators';
import type { RequestHandler } from 'express'; import type { RequestHandler } from 'express';
import { Body, Delete, Get, Param, Patch, Post, RestController } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { isApiEnabled } from '@/public-api'; import { isApiEnabled } from '@/public-api';

View File

@@ -1,4 +1,5 @@
import { LoginRequestDto, ResolveSignupTokenQueryDto } from '@n8n/api-types'; import { LoginRequestDto, ResolveSignupTokenQueryDto } from '@n8n/api-types';
import { Body, Get, Post, Query, RestController } from '@n8n/decorators';
import { isEmail } from 'class-validator'; import { isEmail } from 'class-validator';
import { Response } from 'express'; import { Response } from 'express';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
@@ -8,7 +9,6 @@ import { AuthService } from '@/auth/auth.service';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import type { User } from '@/databases/entities/user'; import type { User } from '@/databases/entities/user';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { Body, Get, Post, Query, RestController } from '@/decorators';
import { AuthError } from '@/errors/response-errors/auth.error'; import { AuthError } from '@/errors/response-errors/auth.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error';

View File

@@ -1,9 +1,9 @@
import { BinaryDataQueryDto, BinaryDataSignedQueryDto, ViewableMimeTypes } from '@n8n/api-types'; import { BinaryDataQueryDto, BinaryDataSignedQueryDto, ViewableMimeTypes } from '@n8n/api-types';
import { Get, Query, RestController } from '@n8n/decorators';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { JsonWebTokenError } from 'jsonwebtoken'; import { JsonWebTokenError } from 'jsonwebtoken';
import { BinaryDataService, FileNotFoundError, isValidNonDefaultMode } from 'n8n-core'; import { BinaryDataService, FileNotFoundError, isValidNonDefaultMode } from 'n8n-core';
import { Get, Query, RestController } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@RestController('/binary-data') @RestController('/binary-data')

View File

@@ -1,10 +1,11 @@
import { Delete, Get, Patch, Post, RestController, GlobalScope } from '@n8n/decorators';
import { import {
RESPONSE_ERROR_MESSAGES, RESPONSE_ERROR_MESSAGES,
STARTER_TEMPLATE_NAME, STARTER_TEMPLATE_NAME,
UNKNOWN_FAILURE_REASON, UNKNOWN_FAILURE_REASON,
} from '@/constants'; } from '@/constants';
import type { InstalledPackages } from '@/databases/entities/installed-packages'; import type { InstalledPackages } from '@/databases/entities/installed-packages';
import { Delete, Get, Patch, Post, RestController, GlobalScope } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';

View File

@@ -1,6 +1,6 @@
import { Get, RestController } from '@n8n/decorators';
import express from 'express'; import express from 'express';
import { Get, RestController } from '@/decorators';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import { CtaService } from '@/services/cta.service'; import { CtaService } from '@/services/cta.service';

View File

@@ -1,8 +1,8 @@
import { Get, RestController } from '@n8n/decorators';
import { InstanceSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { ActiveWorkflowManager } from '@/active-workflow-manager';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { Get, RestController } from '@/decorators';
import { OrchestrationService } from '@/services/orchestration.service'; import { OrchestrationService } from '@/services/orchestration.service';
@RestController('/debug') @RestController('/debug')

View File

@@ -4,9 +4,9 @@ import {
ResourceMapperFieldsRequestDto, ResourceMapperFieldsRequestDto,
ActionResultRequestDto, ActionResultRequestDto,
} from '@n8n/api-types'; } from '@n8n/api-types';
import { Post, RestController, Body } from '@n8n/decorators';
import type { INodePropertyOptions, NodeParameterValueType } from 'n8n-workflow'; import type { INodePropertyOptions, NodeParameterValueType } from 'n8n-workflow';
import { Post, RestController, Body } from '@/decorators';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service'; import { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service';
import { getBase } from '@/workflow-execute-additional-data'; import { getBase } from '@/workflow-execute-additional-data';

View File

@@ -1,6 +1,7 @@
import type { PushMessage } from '@n8n/api-types'; import type { PushMessage } from '@n8n/api-types';
import type { BooleanLicenseFeature, NumericLicenseFeature } from '@n8n/constants'; import type { BooleanLicenseFeature, NumericLicenseFeature } from '@n8n/constants';
import { LICENSE_FEATURES, LICENSE_QUOTAS, UNLIMITED_LICENSE_QUOTA } from '@n8n/constants'; import { LICENSE_FEATURES, LICENSE_QUOTAS, UNLIMITED_LICENSE_QUOTA } from '@n8n/constants';
import { Patch, Post, RestController } from '@n8n/decorators';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { Request } from 'express'; import { Request } from 'express';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
@@ -12,7 +13,6 @@ import { inE2ETests } from '@/constants';
import { AuthUserRepository } from '@/databases/repositories/auth-user.repository'; import { AuthUserRepository } from '@/databases/repositories/auth-user.repository';
import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { Patch, Post, RestController } from '@/decorators';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import type { FeatureReturnType } from '@/license'; import type { FeatureReturnType } from '@/license';
import { License } from '@/license'; import { License } from '@/license';

View File

@@ -4,10 +4,19 @@ import {
ListFolderQueryDto, ListFolderQueryDto,
UpdateFolderDto, UpdateFolderDto,
} from '@n8n/api-types'; } from '@n8n/api-types';
import {
Post,
RestController,
ProjectScope,
Body,
Get,
Patch,
Delete,
Query,
} from '@n8n/decorators';
import { Response } from 'express'; import { Response } from 'express';
import { UserError } from 'n8n-workflow'; import { UserError } from 'n8n-workflow';
import { Post, RestController, ProjectScope, Body, Get, Patch, Delete, Query } from '@/decorators';
import { FolderNotFoundError } from '@/errors/folder-not-found.error'; import { FolderNotFoundError } from '@/errors/folder-not-found.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';

View File

@@ -1,4 +1,5 @@
import { AcceptInvitationRequestDto, InviteUsersRequestDto } from '@n8n/api-types'; import { AcceptInvitationRequestDto, InviteUsersRequestDto } from '@n8n/api-types';
import { Post, GlobalScope, RestController, Body, Param } from '@n8n/decorators';
import { Response } from 'express'; import { Response } from 'express';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
@@ -7,7 +8,6 @@ import config from '@/config';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import type { User } from '@/databases/entities/user'; import type { User } from '@/databases/entities/user';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { Post, GlobalScope, RestController, Body, Param } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';

View File

@@ -4,6 +4,7 @@ import {
SettingsUpdateRequestDto, SettingsUpdateRequestDto,
UserUpdateRequestDto, UserUpdateRequestDto,
} from '@n8n/api-types'; } from '@n8n/api-types';
import { Body, Patch, Post, RestController } from '@n8n/decorators';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { Response } from 'express'; import { Response } from 'express';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
@@ -11,7 +12,6 @@ import { Logger } from 'n8n-core';
import { AuthService } from '@/auth/auth.service'; import { AuthService } from '@/auth/auth.service';
import type { User } from '@/databases/entities/user'; import type { User } from '@/databases/entities/user';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { Body, Patch, Post, RestController } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InvalidMfaCodeError } from '@/errors/response-errors/invalid-mfa-code.error'; import { InvalidMfaCodeError } from '@/errors/response-errors/invalid-mfa-code.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';

View File

@@ -1,4 +1,5 @@
import { Get, Post, RestController } from '@/decorators'; import { Get, Post, RestController } from '@n8n/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ExternalHooks } from '@/external-hooks'; import { ExternalHooks } from '@/external-hooks';
import { MfaService } from '@/mfa/mfa.service'; import { MfaService } from '@/mfa/mfa.service';

View File

@@ -1,10 +1,10 @@
import { Post, RestController } from '@n8n/decorators';
import { Request } from 'express'; import { Request } from 'express';
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
import get from 'lodash/get'; import get from 'lodash/get';
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow'; import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import { Post, RestController } from '@/decorators';
import { NodeTypes } from '@/node-types'; import { NodeTypes } from '@/node-types';
@RestController('/node-types') @RestController('/node-types')

View File

@@ -1,3 +1,4 @@
import { Get, RestController } from '@n8n/decorators';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import axios from 'axios'; import axios from 'axios';
import { createHmac } from 'crypto'; import { createHmac } from 'crypto';
@@ -6,7 +7,6 @@ import { ensureError, jsonStringify } from 'n8n-workflow';
import type { RequestOptions } from 'oauth-1.0a'; import type { RequestOptions } from 'oauth-1.0a';
import clientOAuth1 from 'oauth-1.0a'; import clientOAuth1 from 'oauth-1.0a';
import { Get, RestController } from '@/decorators';
import { OAuthRequest } from '@/requests'; import { OAuthRequest } from '@/requests';
import { AbstractOAuthController, skipAuthOnOAuthCallback } from './abstract-oauth.controller'; import { AbstractOAuthController, skipAuthOnOAuthCallback } from './abstract-oauth.controller';

View File

@@ -1,5 +1,6 @@
import type { ClientOAuth2Options, OAuth2CredentialData } from '@n8n/client-oauth2'; import type { ClientOAuth2Options, OAuth2CredentialData } from '@n8n/client-oauth2';
import { ClientOAuth2 } from '@n8n/client-oauth2'; import { ClientOAuth2 } from '@n8n/client-oauth2';
import { Get, RestController } from '@n8n/decorators';
import { Response } from 'express'; import { Response } from 'express';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import set from 'lodash/set'; import set from 'lodash/set';
@@ -9,7 +10,6 @@ import pkceChallenge from 'pkce-challenge';
import * as qs from 'querystring'; import * as qs from 'querystring';
import { GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE as GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE } from '@/constants'; import { GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE as GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE } from '@/constants';
import { Get, RestController } from '@/decorators';
import { OAuthRequest } from '@/requests'; import { OAuthRequest } from '@/requests';
import { AbstractOAuthController, skipAuthOnOAuthCallback } from './abstract-oauth.controller'; import { AbstractOAuthController, skipAuthOnOAuthCallback } from './abstract-oauth.controller';

View File

@@ -1,4 +1,5 @@
import { Post, RestController, GlobalScope } from '@/decorators'; import { Post, RestController, GlobalScope } from '@n8n/decorators';
import { License } from '@/license'; import { License } from '@/license';
import { Publisher } from '@/scaling/pubsub/publisher.service'; import { Publisher } from '@/scaling/pubsub/publisher.service';

View File

@@ -1,4 +1,5 @@
import { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types'; import { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types';
import { Body, GlobalScope, Post, RestController } from '@n8n/decorators';
import { Response } from 'express'; import { Response } from 'express';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
@@ -6,7 +7,6 @@ import { AuthService } from '@/auth/auth.service';
import config from '@/config'; import config from '@/config';
import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { Body, GlobalScope, Post, RestController } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { validateEntity } from '@/generic-helpers'; import { validateEntity } from '@/generic-helpers';

View File

@@ -3,13 +3,13 @@ import {
ForgotPasswordRequestDto, ForgotPasswordRequestDto,
ResolvePasswordTokenQueryDto, ResolvePasswordTokenQueryDto,
} from '@n8n/api-types'; } from '@n8n/api-types';
import { Body, Get, Post, Query, RestController } from '@n8n/decorators';
import { Response } from 'express'; import { Response } from 'express';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
import { AuthService } from '@/auth/auth.service'; import { AuthService } from '@/auth/auth.service';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { Body, Get, Post, Query, RestController } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';

View File

@@ -1,12 +1,4 @@
import { CreateProjectDto, DeleteProjectDto, UpdateProjectDto } from '@n8n/api-types'; import { CreateProjectDto, DeleteProjectDto, UpdateProjectDto } from '@n8n/api-types';
import { combineScopes } from '@n8n/permissions';
import type { Scope } from '@n8n/permissions';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In, Not } from '@n8n/typeorm';
import { Response } from 'express';
import type { Project } from '@/databases/entities/project';
import { ProjectRepository } from '@/databases/repositories/project.repository';
import { import {
Get, Get,
Post, Post,
@@ -19,7 +11,15 @@ import {
Body, Body,
Param, Param,
Query, Query,
} from '@/decorators'; } from '@n8n/decorators';
import { combineScopes } from '@n8n/permissions';
import type { Scope } from '@n8n/permissions';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In, Not } from '@n8n/typeorm';
import { Response } from 'express';
import type { Project } from '@/databases/entities/project';
import { ProjectRepository } from '@/databases/repositories/project.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';

View File

@@ -1,4 +1,5 @@
import { Get, RestController } from '@/decorators'; import { Get, RestController } from '@n8n/decorators';
import { type AllRoleTypes, RoleService } from '@/services/role.service'; import { type AllRoleTypes, RoleService } from '@/services/role.service';
@RestController('/roles') @RestController('/roles')

View File

@@ -1,6 +1,4 @@
import { CreateOrUpdateTagRequestDto, RetrieveTagQueryDto } from '@n8n/api-types'; import { CreateOrUpdateTagRequestDto, RetrieveTagQueryDto } from '@n8n/api-types';
import { Response } from 'express';
import { import {
Delete, Delete,
Get, Get,
@@ -11,7 +9,9 @@ import {
Body, Body,
Param, Param,
Query, Query,
} from '@/decorators'; } from '@n8n/decorators';
import { Response } from 'express';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import { TagService } from '@/services/tag.service'; import { TagService } from '@/services/tag.service';

View File

@@ -1,3 +1,4 @@
import { Get, RestController } from '@n8n/decorators';
import type { Request } from 'express'; import type { Request } from 'express';
import { access } from 'fs/promises'; import { access } from 'fs/promises';
import { join } from 'path'; import { join } from 'path';
@@ -5,7 +6,6 @@ import { join } from 'path';
import config from '@/config'; import config from '@/config';
import { NODES_BASE_DIR } from '@/constants'; import { NODES_BASE_DIR } from '@/constants';
import { CredentialTypes } from '@/credential-types'; import { CredentialTypes } from '@/credential-types';
import { Get, RestController } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';

View File

@@ -1,6 +1,6 @@
import { Patch, RestController } from '@n8n/decorators';
import type { NpsSurveyState } from 'n8n-workflow'; import type { NpsSurveyState } from 'n8n-workflow';
import { Patch, RestController } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NpsSurveyRequest } from '@/requests'; import { NpsSurveyRequest } from '@/requests';
import { UserService } from '@/services/user.service'; import { UserService } from '@/services/user.service';

View File

@@ -1,4 +1,14 @@
import { RoleChangeRequestDto, SettingsUpdateRequestDto } from '@n8n/api-types'; import { RoleChangeRequestDto, SettingsUpdateRequestDto } from '@n8n/api-types';
import {
GlobalScope,
Delete,
Get,
RestController,
Patch,
Licensed,
Body,
Param,
} from '@n8n/decorators';
import { Response } from 'express'; import { Response } from 'express';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
@@ -11,16 +21,6 @@ import { ProjectRepository } from '@/databases/repositories/project.repository';
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import {
GlobalScope,
Delete,
Get,
RestController,
Patch,
Licensed,
Body,
Param,
} from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';

View File

@@ -1,3 +1,4 @@
import { Get, Middleware, RestController } from '@n8n/decorators';
import { Response, NextFunction } from 'express'; import { Response, NextFunction } from 'express';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
@@ -5,7 +6,6 @@ import type { WorkflowStatistics } from '@/databases/entities/workflow-statistic
import { StatisticsNames } from '@/databases/entities/workflow-statistics'; import { StatisticsNames } from '@/databases/entities/workflow-statistics';
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
import { WorkflowStatisticsRepository } from '@/databases/repositories/workflow-statistics.repository'; import { WorkflowStatisticsRepository } from '@/databases/repositories/workflow-statistics.repository';
import { Get, Middleware, RestController } from '@/decorators';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import type { IWorkflowStatisticsDataLoaded } from '@/interfaces'; import type { IWorkflowStatisticsDataLoaded } from '@/interfaces';

View File

@@ -5,17 +5,6 @@ import {
GenerateCredentialNameRequestQuery, GenerateCredentialNameRequestQuery,
} from '@n8n/api-types'; } from '@n8n/api-types';
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In } from '@n8n/typeorm';
import { Logger } from 'n8n-core';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow';
import { z } from 'zod';
import { SharedCredentials } from '@/databases/entities/shared-credentials';
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
import * as Db from '@/db';
import { import {
Delete, Delete,
Get, Get,
@@ -25,8 +14,21 @@ import {
Put, Put,
RestController, RestController,
ProjectScope, ProjectScope,
} from '@/decorators'; Body,
import { Body, Param, Query } from '@/decorators/args'; Param,
Query,
} from '@n8n/decorators';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In } from '@n8n/typeorm';
import { Logger } from 'n8n-core';
import { deepCopy } from 'n8n-workflow';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import { z } from 'zod';
import { SharedCredentials } from '@/databases/entities/shared-credentials';
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
import * as Db from '@/db';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';

View File

@@ -1,7 +0,0 @@
export { Body, Query, Param } from './args';
export { RestController } from './rest-controller';
export { Get, Post, Put, Patch, Delete } from './route';
export { Middleware } from './middleware';
export { ControllerRegistry } from './controller.registry';
export { Licensed } from './licensed';
export { GlobalScope, ProjectScope } from './scoped';

View File

@@ -1,7 +0,0 @@
import { getControllerMetadata } from './controller.registry';
import type { Controller } from './types';
export const Middleware = (): MethodDecorator => (target, handlerName) => {
const metadata = getControllerMetadata(target.constructor as Controller);
metadata.middlewares.push(String(handlerName));
};

View File

@@ -1,9 +1,9 @@
import { PullWorkFolderRequestDto, PushWorkFolderRequestDto } from '@n8n/api-types'; import { PullWorkFolderRequestDto, PushWorkFolderRequestDto } from '@n8n/api-types';
import type { SourceControlledFile } from '@n8n/api-types'; import type { SourceControlledFile } from '@n8n/api-types';
import { Get, Post, Patch, RestController, GlobalScope, Body } from '@n8n/decorators';
import express from 'express'; import express from 'express';
import type { PullResult } from 'simple-git'; import type { PullResult } from 'simple-git';
import { Get, Post, Patch, RestController, GlobalScope, Body } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';

View File

@@ -1,5 +1,4 @@
import { VariableListRequestDto } from '@n8n/api-types'; import { VariableListRequestDto } from '@n8n/api-types';
import { import {
Delete, Delete,
Get, Get,
@@ -9,7 +8,8 @@ import {
Post, Post,
Query, Query,
RestController, RestController,
} from '@/decorators'; } from '@n8n/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error'; import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error';

View File

@@ -1,7 +1,7 @@
import { Get, Post, Patch, RestController, Delete } from '@n8n/decorators';
import express from 'express'; import express from 'express';
import assert from 'node:assert'; import assert from 'node:assert';
import { Get, Post, Patch, RestController, Delete } from '@/decorators';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { import {

View File

@@ -1,9 +1,9 @@
import { Delete, Get, Post, RestController } from '@n8n/decorators';
import express from 'express'; import express from 'express';
import { InstanceSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import { TestCaseExecutionRepository } from '@/databases/repositories/test-case-execution.repository.ee'; import { TestCaseExecutionRepository } from '@/databases/repositories/test-case-execution.repository.ee';
import { TestRunRepository } from '@/databases/repositories/test-run.repository.ee'; import { TestRunRepository } from '@/databases/repositories/test-run.repository.ee';
import { Delete, Get, Post, RestController } from '@/decorators';
import { ConflictError } from '@/errors/response-errors/conflict.error'; import { ConflictError } from '@/errors/response-errors/conflict.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { NotImplementedError } from '@/errors/response-errors/not-implemented.error'; import { NotImplementedError } from '@/errors/response-errors/not-implemented.error';

View File

@@ -1,3 +1,4 @@
import { RestController, Get, Post, Delete, GlobalScope, Licensed } from '@n8n/decorators';
import express from 'express'; import express from 'express';
import type { import type {
MessageEventBusDestinationWebhookOptions, MessageEventBusDestinationWebhookOptions,
@@ -5,7 +6,6 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { MessageEventBusDestinationTypeNames } from 'n8n-workflow'; import { MessageEventBusDestinationTypeNames } from 'n8n-workflow';
import { RestController, Get, Post, Delete, GlobalScope, Licensed } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';

View File

@@ -1,4 +1,5 @@
import { Get, RestController } from '@/decorators'; import { Get, RestController } from '@n8n/decorators';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import { EventService } from './event.service'; import { EventService } from './event.service';

View File

@@ -1,7 +1,7 @@
import { Redactable } from '@n8n/decorators';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import type { IWorkflowBase } from 'n8n-workflow'; import type { IWorkflowBase } from 'n8n-workflow';
import { Redactable } from '@/decorators/redactable';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import type { RelayEventMap } from '@/events/maps/relay.event-map'; import type { RelayEventMap } from '@/events/maps/relay.event-map';

View File

@@ -1,3 +1,4 @@
import { ModuleRegistry } from '@n8n/decorators';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { stringify } from 'flatted'; import { stringify } from 'flatted';
import { ErrorReporter, Logger, InstanceSettings, ExecutionLifecycleHooks } from 'n8n-core'; import { ErrorReporter, Logger, InstanceSettings, ExecutionLifecycleHooks } from 'n8n-core';
@@ -8,7 +9,6 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { ExecutionRepository } from '@/databases/repositories/execution.repository';
import { ModuleRegistry } from '@/decorators/module';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { ExternalHooks } from '@/external-hooks'; import { ExternalHooks } from '@/external-hooks';
import { Push } from '@/push'; import { Push } from '@/push';

View File

@@ -1,7 +1,7 @@
import { Get, Patch, Post, RestController } from '@n8n/decorators';
import type { Scope } from '@n8n/permissions'; import type { Scope } from '@n8n/permissions';
import type { User } from '@/databases/entities/user'; import type { User } from '@/databases/entities/user';
import { Get, Patch, Post, RestController } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { License } from '@/license'; import { License } from '@/license';

View File

@@ -1,9 +1,9 @@
import { OnShutdown } from '@n8n/decorators';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import { Cipher, Logger } from 'n8n-core'; import { Cipher, Logger } from 'n8n-core';
import { jsonParse, type IDataObject, ensureError, UnexpectedError } from 'n8n-workflow'; import { jsonParse, type IDataObject, ensureError, UnexpectedError } from 'n8n-workflow';
import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { OnShutdown } from '@/decorators/on-shutdown';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { License } from '@/license'; import { License } from '@/license';
import { Publisher } from '@/scaling/pubsub/publisher.service'; import { Publisher } from '@/scaling/pubsub/publisher.service';

View File

@@ -1,6 +1,6 @@
import { Get, Post, RestController, GlobalScope, Middleware } from '@n8n/decorators';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Get, Post, RestController, GlobalScope, Middleware } from '@/decorators';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { ExternalSecretsProviders } from './external-secrets-providers.ee'; import { ExternalSecretsProviders } from './external-secrets-providers.ee';

View File

@@ -1,6 +1,6 @@
import { Get, Post, Put, RestController, GlobalScope } from '@n8n/decorators';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import { Get, Post, Put, RestController, GlobalScope } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';

View File

@@ -6,6 +6,7 @@ import {
type BooleanLicenseFeature, type BooleanLicenseFeature,
type NumericLicenseFeature, type NumericLicenseFeature,
} from '@n8n/constants'; } from '@n8n/constants';
import { OnShutdown } from '@n8n/decorators';
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import type { TEntitlement, TFeatures, TLicenseBlock } from '@n8n_io/license-sdk'; import type { TEntitlement, TFeatures, TLicenseBlock } from '@n8n_io/license-sdk';
import { LicenseManager } from '@n8n_io/license-sdk'; import { LicenseManager } from '@n8n_io/license-sdk';
@@ -13,7 +14,6 @@ import { InstanceSettings, Logger } from 'n8n-core';
import config from '@/config'; import config from '@/config';
import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { OnShutdown } from '@/decorators/on-shutdown';
import { LicenseMetricsService } from '@/metrics/license-metrics.service'; import { LicenseMetricsService } from '@/metrics/license-metrics.service';
import { N8N_VERSION, SETTINGS_LICENSE_CERT_KEY, Time } from './constants'; import { N8N_VERSION, SETTINGS_LICENSE_CERT_KEY, Time } from './constants';

View File

@@ -1,8 +1,8 @@
import { CommunityRegisteredRequestDto } from '@n8n/api-types'; import { CommunityRegisteredRequestDto } from '@n8n/api-types';
import { Get, Post, RestController, GlobalScope, Body } from '@n8n/decorators';
import type { AxiosError } from 'axios'; import type { AxiosError } from 'axios';
import { InstanceSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import { Get, Post, RestController, GlobalScope, Body } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { AuthenticatedRequest, LicenseRequest } from '@/requests'; import { AuthenticatedRequest, LicenseRequest } from '@/requests';
import { UrlService } from '@/services/url.service'; import { UrlService } from '@/services/url.service';

View File

@@ -1,7 +1,7 @@
import { ListInsightsWorkflowQueryDto } from '@n8n/api-types'; import { ListInsightsWorkflowQueryDto } from '@n8n/api-types';
import type { InsightsSummary, InsightsByTime, InsightsByWorkflow } from '@n8n/api-types'; import type { InsightsSummary, InsightsByTime, InsightsByWorkflow } from '@n8n/api-types';
import { Get, GlobalScope, Licensed, Query, RestController } from '@n8n/decorators';
import { Get, GlobalScope, Licensed, Query, RestController } from '@/decorators';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import { InsightsService } from './insights.service'; import { InsightsService } from './insights.service';

View File

@@ -1,8 +1,8 @@
import type { BaseN8nModule } from '@n8n/decorators';
import { N8nModule } from '@n8n/decorators';
import type { ExecutionLifecycleHooks } from 'n8n-core'; import type { ExecutionLifecycleHooks } from 'n8n-core';
import { InstanceSettings, Logger } from 'n8n-core'; import { InstanceSettings, Logger } from 'n8n-core';
import type { BaseN8nModule } from '@/decorators/module';
import { N8nModule } from '@/decorators/module';
import type { MultiMainSetup } from '@/scaling/multi-main-setup.ee'; import type { MultiMainSetup } from '@/scaling/multi-main-setup.ee';
import { InsightsService } from './insights.service'; import { InsightsService } from './insights.service';

View File

@@ -1,11 +1,11 @@
import type { InsightsSummary } from '@n8n/api-types'; import type { InsightsSummary } from '@n8n/api-types';
import type { InsightsDateRange } from '@n8n/api-types/src/schemas/insights.schema'; import type { InsightsDateRange } from '@n8n/api-types/src/schemas/insights.schema';
import { OnShutdown } from '@n8n/decorators';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
import type { ExecutionLifecycleHooks } from 'n8n-core'; import type { ExecutionLifecycleHooks } from 'n8n-core';
import type { IRun } from 'n8n-workflow'; import type { IRun } from 'n8n-workflow';
import { OnShutdown } from '@/decorators/on-shutdown';
import { License } from '@/license'; import { License } from '@/license';
import type { PeriodUnit, TypeUnit } from './database/entities/insights-shared'; import type { PeriodUnit, TypeUnit } from './database/entities/insights-shared';

View File

@@ -1,4 +1,5 @@
import type { PushMessage } from '@n8n/api-types'; import type { PushMessage } from '@n8n/api-types';
import { OnShutdown } from '@n8n/decorators';
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import type { Application } from 'express'; import type { Application } from 'express';
import { ServerResponse } from 'http'; import { ServerResponse } from 'http';
@@ -11,7 +12,6 @@ import { Server as WSServer } from 'ws';
import { AuthService } from '@/auth/auth.service'; import { AuthService } from '@/auth/auth.service';
import { inProduction, TRIMMED_TASK_DATA_CONNECTIONS } from '@/constants'; import { inProduction, TRIMMED_TASK_DATA_CONNECTIONS } from '@/constants';
import type { User } from '@/databases/entities/user'; import type { User } from '@/databases/entities/user';
import { OnShutdown } from '@/decorators/on-shutdown';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { Publisher } from '@/scaling/pubsub/publisher.service'; import { Publisher } from '@/scaling/pubsub/publisher.service';
import { TypedEmitter } from '@/typed-emitter'; import { TypedEmitter } from '@/typed-emitter';

View File

@@ -1,4 +1,5 @@
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import { OnShutdown } from '@n8n/decorators';
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import { ErrorReporter, InstanceSettings, isObjectLiteral, Logger } from 'n8n-core'; import { ErrorReporter, InstanceSettings, isObjectLiteral, Logger } from 'n8n-core';
import { import {
@@ -16,7 +17,6 @@ import { ActiveExecutions } from '@/active-executions';
import config from '@/config'; import config from '@/config';
import { HIGHEST_SHUTDOWN_PRIORITY, Time } from '@/constants'; import { HIGHEST_SHUTDOWN_PRIORITY, Time } from '@/constants';
import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { ExecutionRepository } from '@/databases/repositories/execution.repository';
import { OnShutdown } from '@/decorators/on-shutdown';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { OrchestrationService } from '@/services/orchestration.service'; import { OrchestrationService } from '@/services/orchestration.service';
import { assertNever } from '@/utils'; import { assertNever } from '@/utils';

View File

@@ -20,8 +20,8 @@ import {
N8N_VERSION, N8N_VERSION,
Time, Time,
} from '@/constants'; } from '@/constants';
import { ControllerRegistry } from '@/controller.registry';
import { CredentialsOverwrites } from '@/credentials-overwrites'; import { CredentialsOverwrites } from '@/credentials-overwrites';
import { ControllerRegistry } from '@/decorators';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay'; import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay';

View File

@@ -1,4 +1,5 @@
import { ExecutionsConfig } from '@n8n/config'; import { ExecutionsConfig } from '@n8n/config';
import { OnShutdown } from '@n8n/decorators';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import { BinaryDataService, InstanceSettings, Logger } from 'n8n-core'; import { BinaryDataService, InstanceSettings, Logger } from 'n8n-core';
import { ensureError } from 'n8n-workflow'; import { ensureError } from 'n8n-workflow';
@@ -7,7 +8,6 @@ import { strict } from 'node:assert';
import { Time } from '@/constants'; import { Time } from '@/constants';
import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { ExecutionRepository } from '@/databases/repositories/execution.repository';
import { connectionState as dbConnectionState } from '@/db'; import { connectionState as dbConnectionState } from '@/db';
import { OnShutdown } from '@/decorators/on-shutdown';
import { OrchestrationService } from '../orchestration.service'; import { OrchestrationService } from '../orchestration.service';

View File

@@ -1,10 +1,10 @@
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import { Debounce } from '@n8n/decorators';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import ioRedis from 'ioredis'; import ioRedis from 'ioredis';
import type { Cluster, RedisOptions } from 'ioredis'; import type { Cluster, RedisOptions } from 'ioredis';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
import { Debounce } from '@/decorators/debounce';
import { TypedEmitter } from '@/typed-emitter'; import { TypedEmitter } from '@/typed-emitter';
import type { RedisClientType } from '../scaling/redis/redis.types'; import type { RedisClientType } from '../scaling/redis/redis.types';

View File

@@ -1,10 +1,11 @@
import { ShutdownRegistryMetadata } from '@n8n/decorators';
import type { ServiceClass } from '@n8n/decorators/src/types';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { ErrorReporter } from 'n8n-core'; import type { ErrorReporter } from 'n8n-core';
import { UnexpectedError } from 'n8n-workflow'; import { UnexpectedError } from 'n8n-workflow';
import type { ServiceClass } from '@/shutdown/shutdown.service'; import { ShutdownService } from '../shutdown.service';
import { ShutdownService } from '@/shutdown/shutdown.service';
class MockComponent { class MockComponent {
onShutdown() {} onShutdown() {}
@@ -15,9 +16,11 @@ describe('ShutdownService', () => {
let mockComponent: MockComponent; let mockComponent: MockComponent;
let onShutdownSpy: jest.SpyInstance; let onShutdownSpy: jest.SpyInstance;
const errorReporter = mock<ErrorReporter>(); const errorReporter = mock<ErrorReporter>();
const shutdownRegistryMetadata = Container.get(ShutdownRegistryMetadata);
beforeEach(() => { beforeEach(() => {
shutdownService = new ShutdownService(mock(), errorReporter); shutdownRegistryMetadata.clear();
shutdownService = new ShutdownService(mock(), errorReporter, shutdownRegistryMetadata);
mockComponent = new MockComponent(); mockComponent = new MockComponent();
Container.set(MockComponent, mockComponent); Container.set(MockComponent, mockComponent);
onShutdownSpy = jest.spyOn(mockComponent, 'onShutdown'); onShutdownSpy = jest.spyOn(mockComponent, 'onShutdown');

View File

@@ -1,18 +1,9 @@
import type { ShutdownHandler } from '@n8n/decorators';
import { ShutdownRegistryMetadata } from '@n8n/decorators';
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import { type Class, ErrorReporter } from 'n8n-core'; import { ErrorReporter, Logger } from 'n8n-core';
import { Logger } from 'n8n-core';
import { assert, UnexpectedError, UserError } from 'n8n-workflow'; import { assert, UnexpectedError, UserError } from 'n8n-workflow';
import { LOWEST_SHUTDOWN_PRIORITY, HIGHEST_SHUTDOWN_PRIORITY } from '@/constants';
type HandlerFn = () => Promise<void> | void;
export type ServiceClass = Class<Record<string, HandlerFn>>;
export interface ShutdownHandler {
serviceClass: ServiceClass;
methodName: string;
}
/** Error reported when a listener fails to shutdown gracefully */ /** Error reported when a listener fails to shutdown gracefully */
export class ComponentShutdownError extends UnexpectedError { export class ComponentShutdownError extends UnexpectedError {
constructor(componentName: string, cause: Error) { constructor(componentName: string, cause: Error) {
@@ -26,33 +17,22 @@ export class ComponentShutdownError extends UnexpectedError {
/** Service responsible for orchestrating a graceful shutdown of the application */ /** Service responsible for orchestrating a graceful shutdown of the application */
@Service() @Service()
export class ShutdownService { export class ShutdownService {
private readonly handlersByPriority: ShutdownHandler[][] = [];
private shutdownPromise: Promise<void> | undefined; private shutdownPromise: Promise<void> | undefined;
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,
private readonly errorReporter: ErrorReporter, private readonly errorReporter: ErrorReporter,
private readonly shutdownRegistry: ShutdownRegistryMetadata,
) {} ) {}
/** Registers given listener to be notified when the application is shutting down */ /** Registers given listener to be notified when the application is shutting down */
register(priority: number, handler: ShutdownHandler) { register(priority: number, handler: ShutdownHandler) {
if (priority < LOWEST_SHUTDOWN_PRIORITY || priority > HIGHEST_SHUTDOWN_PRIORITY) { this.shutdownRegistry.register(priority, handler);
throw new UserError(
`Invalid shutdown priority. Please set it between ${LOWEST_SHUTDOWN_PRIORITY} and ${HIGHEST_SHUTDOWN_PRIORITY}.`,
{ extra: { priority } },
);
}
if (!this.handlersByPriority[priority]) {
this.handlersByPriority[priority] = [];
}
this.handlersByPriority[priority].push(handler);
} }
/** Validates that all the registered shutdown handlers are properly configured */ /** Validates that all the registered shutdown handlers are properly configured */
validate() { validate() {
const handlers = this.handlersByPriority.flat(); const handlers = this.shutdownRegistry.getHandlersByPriority().flat();
for (const { serviceClass, methodName } of handlers) { for (const { serviceClass, methodName } of handlers) {
if (!Container.has(serviceClass)) { if (!Container.has(serviceClass)) {
@@ -93,7 +73,8 @@ export class ShutdownService {
} }
private async startShutdown() { private async startShutdown() {
const handlers = Object.values(this.handlersByPriority).reverse(); const handlers = Object.values(this.shutdownRegistry.getHandlersByPriority()).reverse();
for (const handlerGroup of handlers) { for (const handlerGroup of handlers) {
await Promise.allSettled( await Promise.allSettled(
handlerGroup.map(async (handler) => await this.shutdownComponent(handler)), handlerGroup.map(async (handler) => await this.shutdownComponent(handler)),

View File

@@ -1,11 +1,11 @@
import { SamlAcsDto, SamlPreferences, SamlToggleDto } from '@n8n/api-types'; import { SamlAcsDto, SamlPreferences, SamlToggleDto } from '@n8n/api-types';
import { Get, Post, RestController, GlobalScope, Body } from '@n8n/decorators';
import { Response } from 'express'; import { Response } from 'express';
import querystring from 'querystring'; import querystring from 'querystring';
import type { PostBindingContext } from 'samlify/types/src/entity'; import type { PostBindingContext } from 'samlify/types/src/entity';
import url from 'url'; import url from 'url';
import { AuthService } from '@/auth/auth.service'; import { AuthService } from '@/auth/auth.service';
import { Get, Post, RestController, GlobalScope, Body } from '@/decorators';
import { AuthError } from '@/errors/response-errors/auth.error'; import { AuthError } from '@/errors/response-errors/auth.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { AuthenticatedRequest, AuthlessRequest } from '@/requests'; import { AuthenticatedRequest, AuthlessRequest } from '@/requests';

View File

@@ -1,10 +1,10 @@
import { TaskRunnersConfig } from '@n8n/config'; import { TaskRunnersConfig } from '@n8n/config';
import { OnShutdown } from '@n8n/decorators';
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import { ErrorReporter, Logger } from 'n8n-core'; import { ErrorReporter, Logger } from 'n8n-core';
import { sleep } from 'n8n-workflow'; import { sleep } from 'n8n-workflow';
import * as a from 'node:assert/strict'; import * as a from 'node:assert/strict';
import { OnShutdown } from '@/decorators/on-shutdown';
import type { TaskRunnerRestartLoopError } from '@/task-runners/errors/task-runner-restart-loop-error'; import type { TaskRunnerRestartLoopError } from '@/task-runners/errors/task-runner-restart-loop-error';
import { TaskBrokerWsServer } from '@/task-runners/task-broker/task-broker-ws-server'; import { TaskBrokerWsServer } from '@/task-runners/task-broker/task-broker-ws-server';
import type { TaskRunnerProcess } from '@/task-runners/task-runner-process'; import type { TaskRunnerProcess } from '@/task-runners/task-runner-process';

View File

@@ -1,12 +1,11 @@
import { TaskRunnersConfig } from '@n8n/config'; import { TaskRunnersConfig } from '@n8n/config';
import { OnShutdown } from '@n8n/decorators';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
import * as a from 'node:assert/strict'; import * as a from 'node:assert/strict';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import * as process from 'node:process'; import * as process from 'node:process';
import { OnShutdown } from '@/decorators/on-shutdown';
import { forwardToLogger } from './forward-to-logger'; import { forwardToLogger } from './forward-to-logger';
import { NodeProcessOomDetector } from './node-process-oom-detector'; import { NodeProcessOomDetector } from './node-process-oom-detector';
import { TaskBrokerAuthService } from './task-broker/auth/task-broker-auth.service'; import { TaskBrokerAuthService } from './task-broker/auth/task-broker-auth.service';

View File

@@ -1,4 +1,5 @@
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import { OnShutdown } from '@n8n/decorators';
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import type RudderStack from '@rudderstack/rudder-sdk-node'; import type RudderStack from '@rudderstack/rudder-sdk-node';
import axios from 'axios'; import axios from 'axios';
@@ -10,7 +11,6 @@ import { ProjectRelationRepository } from '@/databases/repositories/project-rela
import { ProjectRepository } from '@/databases/repositories/project.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { OnShutdown } from '@/decorators/on-shutdown';
import type { IExecutionTrackProperties } from '@/interfaces'; import type { IExecutionTrackProperties } from '@/interfaces';
import { License } from '@/license'; import { License } from '@/license';
import { PostHogClient } from '@/posthog'; import { PostHogClient } from '@/posthog';

View File

@@ -1,7 +1,7 @@
import { PaginationDto } from '@n8n/api-types'; import { PaginationDto } from '@n8n/api-types';
import { RestController, Get, Middleware, Query } from '@n8n/decorators';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { RestController, Get, Middleware, Query } from '@/decorators';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { SharedWorkflowNotFoundError } from '@/errors/shared-workflow-not-found.error'; import { SharedWorkflowNotFoundError } from '@/errors/shared-workflow-not-found.error';
import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-version-not-found.error'; import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-version-not-found.error';

View File

@@ -4,6 +4,19 @@ import {
TransferWorkflowBodyDto, TransferWorkflowBodyDto,
} from '@n8n/api-types'; } from '@n8n/api-types';
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import {
Body,
Delete,
Get,
Licensed,
Param,
Patch,
Post,
ProjectScope,
Put,
Query,
RestController,
} from '@n8n/decorators';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In, type FindOptionsRelations } from '@n8n/typeorm'; import { In, type FindOptionsRelations } from '@n8n/typeorm';
import axios from 'axios'; import axios from 'axios';
@@ -21,19 +34,6 @@ import { SharedWorkflowRepository } from '@/databases/repositories/shared-workfl
import { TagRepository } from '@/databases/repositories/tag.repository'; import { TagRepository } from '@/databases/repositories/tag.repository';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import * as Db from '@/db'; import * as Db from '@/db';
import {
Body,
Delete,
Get,
Licensed,
Param,
Patch,
Post,
ProjectScope,
Put,
Query,
RestController,
} from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';

View File

@@ -8,8 +8,8 @@ import { URL } from 'url';
import { AuthService } from '@/auth/auth.service'; import { AuthService } from '@/auth/auth.service';
import config from '@/config'; import config from '@/config';
import { AUTH_COOKIE_NAME } from '@/constants'; import { AUTH_COOKIE_NAME } from '@/constants';
import { ControllerRegistry } from '@/controller.registry';
import type { User } from '@/databases/entities/user'; import type { User } from '@/databases/entities/user';
import { ControllerRegistry } from '@/decorators';
import { License } from '@/license'; import { License } from '@/license';
import { rawBodyReader, bodyParser } from '@/middlewares'; import { rawBodyReader, bodyParser } from '@/middlewares';
import { PostHogClient } from '@/posthog'; import { PostHogClient } from '@/posthog';

View File

@@ -27,6 +27,7 @@
{ "path": "../@n8n/client-oauth2/tsconfig.build.json" }, { "path": "../@n8n/client-oauth2/tsconfig.build.json" },
{ "path": "../@n8n/config/tsconfig.build.json" }, { "path": "../@n8n/config/tsconfig.build.json" },
{ "path": "../@n8n/constants/tsconfig.build.json" }, { "path": "../@n8n/constants/tsconfig.build.json" },
{ "path": "../@n8n/decorators/tsconfig.build.json" },
{ "path": "../@n8n/di/tsconfig.build.json" }, { "path": "../@n8n/di/tsconfig.build.json" },
{ "path": "../@n8n/nodes-langchain/tsconfig.build.json" }, { "path": "../@n8n/nodes-langchain/tsconfig.build.json" },
{ "path": "../@n8n/permissions/tsconfig.build.json" } { "path": "../@n8n/permissions/tsconfig.build.json" }

View File

@@ -4,7 +4,6 @@ export * from './binary-data';
export * from './constants'; export * from './constants';
export * from './credentials'; export * from './credentials';
export * from './data-deduplication-service'; export * from './data-deduplication-service';
export * from './decorators';
export * from './encryption'; export * from './encryption';
export * from './errors'; export * from './errors';
export * from './execution-engine'; export * from './execution-engine';

31
pnpm-lock.yaml generated
View File

@@ -419,6 +419,34 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../typescript-config version: link:../typescript-config
packages/@n8n/decorators:
dependencies:
'@n8n/constants':
specifier: workspace:^
version: link:../constants
'@n8n/di':
specifier: workspace:^
version: link:../di
'@n8n/permissions':
specifier: workspace:^
version: link:../permissions
lodash:
specifier: 'catalog:'
version: 4.17.21
n8n-workflow:
specifier: workspace:^
version: link:../../workflow
devDependencies:
'@n8n/typescript-config':
specifier: workspace:*
version: link:../typescript-config
'@types/express':
specifier: 'catalog:'
version: 5.0.1
'@types/lodash':
specifier: 'catalog:'
version: 4.14.195
packages/@n8n/di: packages/@n8n/di:
dependencies: dependencies:
reflect-metadata: reflect-metadata:
@@ -932,6 +960,9 @@ importers:
'@n8n/constants': '@n8n/constants':
specifier: workspace:^ specifier: workspace:^
version: link:../@n8n/constants version: link:../@n8n/constants
'@n8n/decorators':
specifier: workspace:*
version: link:../@n8n/decorators
'@n8n/di': '@n8n/di':
specifier: workspace:* specifier: workspace:*
version: link:../@n8n/di version: link:../@n8n/di

View File

@@ -28,6 +28,7 @@
"dependsOn": [ "dependsOn": [
"@n8n/api-types#lint", "@n8n/api-types#lint",
"@n8n/config#lint", "@n8n/config#lint",
"@n8n/decorators#lint",
"@n8n/constants#lint", "@n8n/constants#lint",
"@n8n/di#lint", "@n8n/di#lint",
"@n8n/client-oauth2#lint", "@n8n/client-oauth2#lint",
@@ -64,6 +65,7 @@
"dependsOn": [ "dependsOn": [
"@n8n/api-types#test", "@n8n/api-types#test",
"@n8n/config#test", "@n8n/config#test",
"@n8n/decorators#test",
"@n8n/di#test", "@n8n/di#test",
"@n8n/client-oauth2#test", "@n8n/client-oauth2#test",
"@n8n/imap#test", "@n8n/imap#test",