mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
refactor(core): Restructure decorators and add tests (#15348)
This commit is contained in:
committed by
GitHub
parent
cd1d6c9dfc
commit
c42df1c268
@@ -4,4 +4,5 @@ module.exports = {
|
|||||||
transform: {
|
transform: {
|
||||||
'^.+\\.ts$': ['ts-jest', { isolatedModules: false }],
|
'^.+\\.ts$': ['ts-jest', { isolatedModules: false }],
|
||||||
},
|
},
|
||||||
|
coveragePathIgnorePatterns: ['index.ts'],
|
||||||
};
|
};
|
||||||
|
|||||||
195
packages/@n8n/decorators/src/__tests__/redactable.test.ts
Normal file
195
packages/@n8n/decorators/src/__tests__/redactable.test.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { UnexpectedError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { Redactable, RedactableError } from '../redactable';
|
||||||
|
|
||||||
|
describe('Redactable Decorator', () => {
|
||||||
|
class TestClass {
|
||||||
|
@Redactable()
|
||||||
|
methodWithUser(arg: {
|
||||||
|
user: { id: string; email?: string; firstName?: string; lastName?: string; role: string };
|
||||||
|
}) {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable('inviter')
|
||||||
|
methodWithInviter(arg: {
|
||||||
|
inviter: { id: string; email?: string; firstName?: string; lastName?: string; role: string };
|
||||||
|
}) {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable('invitee')
|
||||||
|
methodWithInvitee(arg: {
|
||||||
|
invitee: { id: string; email?: string; firstName?: string; lastName?: string; role: string };
|
||||||
|
}) {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
methodWithMultipleArgs(
|
||||||
|
firstArg: { something: string },
|
||||||
|
secondArg: {
|
||||||
|
user: { id: string; email?: string; firstName?: string; lastName?: string; role: string };
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return { firstArg, secondArg };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
methodWithoutUser(arg: { something: string }) {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance: TestClass;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
instance = new TestClass();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RedactableError', () => {
|
||||||
|
it('should extend UnexpectedError', () => {
|
||||||
|
const error = new RedactableError('user', 'testArg');
|
||||||
|
expect(error).toBeInstanceOf(UnexpectedError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct error message', () => {
|
||||||
|
const error = new RedactableError('user', 'testArg');
|
||||||
|
expect(error.message).toBe(
|
||||||
|
'Failed to find "user" property in argument "testArg". Please set the decorator `@Redactable()` only on `LogStreamingEventRelay` methods where the argument contains a "user" property.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@Redactable() decorator', () => {
|
||||||
|
it('should transform user properties in a method with a user argument', () => {
|
||||||
|
const input = {
|
||||||
|
user: {
|
||||||
|
id: '123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = instance.methodWithUser(input);
|
||||||
|
|
||||||
|
expect(result.user).toEqual({
|
||||||
|
userId: '123',
|
||||||
|
_email: 'test@example.com',
|
||||||
|
_firstName: 'John',
|
||||||
|
_lastName: 'Doe',
|
||||||
|
globalRole: 'admin',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform inviter properties when fieldName is set to "inviter"', () => {
|
||||||
|
const input = {
|
||||||
|
inviter: {
|
||||||
|
id: '123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = instance.methodWithInviter(input);
|
||||||
|
|
||||||
|
expect(result.inviter).toEqual({
|
||||||
|
userId: '123',
|
||||||
|
_email: 'test@example.com',
|
||||||
|
_firstName: 'John',
|
||||||
|
_lastName: 'Doe',
|
||||||
|
globalRole: 'admin',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform invitee properties when fieldName is set to "invitee"', () => {
|
||||||
|
const input = {
|
||||||
|
invitee: {
|
||||||
|
id: '123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = instance.methodWithInvitee(input);
|
||||||
|
|
||||||
|
expect(result.invitee).toEqual({
|
||||||
|
userId: '123',
|
||||||
|
_email: 'test@example.com',
|
||||||
|
_firstName: 'John',
|
||||||
|
_lastName: 'Doe',
|
||||||
|
globalRole: 'admin',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle user object with missing optional properties', () => {
|
||||||
|
const input = {
|
||||||
|
user: {
|
||||||
|
id: '123',
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = instance.methodWithUser(input);
|
||||||
|
|
||||||
|
expect(result.user).toEqual({
|
||||||
|
userId: '123',
|
||||||
|
_email: undefined,
|
||||||
|
_firstName: undefined,
|
||||||
|
_lastName: undefined,
|
||||||
|
globalRole: 'admin',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find user property in any argument', () => {
|
||||||
|
const firstArg = { something: 'test' };
|
||||||
|
const secondArg = {
|
||||||
|
user: {
|
||||||
|
id: '123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = instance.methodWithMultipleArgs(firstArg, secondArg);
|
||||||
|
|
||||||
|
expect(result.secondArg.user).toEqual({
|
||||||
|
userId: '123',
|
||||||
|
_email: 'test@example.com',
|
||||||
|
_firstName: undefined,
|
||||||
|
_lastName: undefined,
|
||||||
|
globalRole: 'admin',
|
||||||
|
});
|
||||||
|
expect(result.firstArg).toEqual(firstArg);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw RedactableError when no user property is found', () => {
|
||||||
|
expect(() => {
|
||||||
|
instance.methodWithoutUser({ something: 'test' });
|
||||||
|
}).toThrow(RedactableError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly apply the original method', () => {
|
||||||
|
const spy = jest.spyOn(instance, 'methodWithUser');
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
user: {
|
||||||
|
id: '123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
instance.methodWithUser(input);
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
133
packages/@n8n/decorators/src/controller/__tests__/args.test.ts
Normal file
133
packages/@n8n/decorators/src/controller/__tests__/args.test.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { Container } from '@n8n/di';
|
||||||
|
|
||||||
|
import { Body, Query, Param } from '../args';
|
||||||
|
import { ControllerRegistryMetadata } from '../controller-registry-metadata';
|
||||||
|
import type { Controller } from '../types';
|
||||||
|
|
||||||
|
describe('Args Decorators', () => {
|
||||||
|
let controllerRegistryMetadata: ControllerRegistryMetadata;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
controllerRegistryMetadata = new ControllerRegistryMetadata();
|
||||||
|
Container.set(ControllerRegistryMetadata, controllerRegistryMetadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
{ decorator: Body, type: 'Body', expectedArg: { type: 'body' } },
|
||||||
|
{ decorator: Query, type: 'Query', expectedArg: { type: 'query' } },
|
||||||
|
])('@$type decorator', ({ decorator, type, expectedArg }) => {
|
||||||
|
it(`should set ${type} arg at correct parameter index`, () => {
|
||||||
|
class TestController {
|
||||||
|
testMethod(@decorator _parameter: unknown) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameterIndex = 0;
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'testMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.args[parameterIndex]).toEqual(expectedArg);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should handle multiple parameters with ${type}`, () => {
|
||||||
|
class TestController {
|
||||||
|
testMethod(_first: string, @decorator _second: unknown, _third: number) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameterIndex = 1;
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'testMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.args[parameterIndex]).toEqual(expectedArg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@Param decorator', () => {
|
||||||
|
it('should set param arg with key at correct parameter index', () => {
|
||||||
|
class TestController {
|
||||||
|
testMethod(@Param('id') _id: string) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameterIndex = 0;
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'testMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.args[parameterIndex]).toEqual({ type: 'param', key: 'id' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple Param decorators with different keys', () => {
|
||||||
|
class TestController {
|
||||||
|
testMethod(@Param('id') _id: string, @Param('userId') _userId: string) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'testMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.args[0]).toEqual({ type: 'param', key: 'id' });
|
||||||
|
expect(routeMetadata.args[1]).toEqual({ type: 'param', key: 'userId' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with all decorators combined', () => {
|
||||||
|
class TestController {
|
||||||
|
testMethod(@Body _body: unknown, @Query _query: unknown, @Param('id') _id: string) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'testMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.args[0]).toEqual({ type: 'body' });
|
||||||
|
expect(routeMetadata.args[1]).toEqual({ type: 'query' });
|
||||||
|
expect(routeMetadata.args[2]).toEqual({ type: 'param', key: 'id' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with complex parameter combinations', () => {
|
||||||
|
class TestController {
|
||||||
|
simpleMethod(@Body _body: unknown) {}
|
||||||
|
|
||||||
|
queryMethod(@Query _query: unknown) {}
|
||||||
|
|
||||||
|
mixedMethod(
|
||||||
|
@Param('id') _id: string,
|
||||||
|
_undecorated: number,
|
||||||
|
@Body _body: unknown,
|
||||||
|
@Query _query: unknown,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const simpleRouteMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'simpleMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryRouteMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'queryMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
const mixedRouteMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'mixedMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(simpleRouteMetadata.args[0]).toEqual({ type: 'body' });
|
||||||
|
|
||||||
|
expect(queryRouteMetadata.args[0]).toEqual({ type: 'query' });
|
||||||
|
|
||||||
|
expect(mixedRouteMetadata.args[0]).toEqual({ type: 'param', key: 'id' });
|
||||||
|
expect(mixedRouteMetadata.args[1]).toBeUndefined(); // undecorated parameter
|
||||||
|
expect(mixedRouteMetadata.args[2]).toEqual({ type: 'body' });
|
||||||
|
expect(mixedRouteMetadata.args[3]).toEqual({ type: 'query' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { ControllerRegistryMetadata } from '../controller-registry-metadata';
|
||||||
|
import type { Controller, HandlerName } from '../types';
|
||||||
|
|
||||||
|
describe('ControllerRegistryMetadata', () => {
|
||||||
|
let registry: ControllerRegistryMetadata;
|
||||||
|
|
||||||
|
const TestController = class TestController {
|
||||||
|
async testHandler() {
|
||||||
|
return 'test';
|
||||||
|
}
|
||||||
|
|
||||||
|
anotherHandler() {}
|
||||||
|
} as Controller;
|
||||||
|
|
||||||
|
const AnotherController = class AnotherController {
|
||||||
|
async handler() {}
|
||||||
|
} as Controller;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
registry = new ControllerRegistryMetadata();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getControllerMetadata', () => {
|
||||||
|
it('should create and return default metadata for a new controller', () => {
|
||||||
|
const metadata = registry.getControllerMetadata(TestController);
|
||||||
|
|
||||||
|
expect(metadata).toEqual({
|
||||||
|
basePath: '/',
|
||||||
|
middlewares: [],
|
||||||
|
routes: expect.any(Map),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return existing metadata for a registered controller', () => {
|
||||||
|
// Get metadata first time to register
|
||||||
|
const initialMetadata = registry.getControllerMetadata(TestController);
|
||||||
|
// Update metadata
|
||||||
|
initialMetadata.basePath = '/api';
|
||||||
|
initialMetadata.middlewares.push('auth');
|
||||||
|
|
||||||
|
// Get metadata second time
|
||||||
|
const metadata = registry.getControllerMetadata(TestController);
|
||||||
|
|
||||||
|
expect(metadata).toBe(initialMetadata);
|
||||||
|
expect(metadata.basePath).toBe('/api');
|
||||||
|
expect(metadata.middlewares).toEqual(['auth']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRouteMetadata', () => {
|
||||||
|
it('should create and return default route metadata for a new handler', () => {
|
||||||
|
const handlerName: HandlerName = 'testHandler';
|
||||||
|
const routeMetadata = registry.getRouteMetadata(TestController, handlerName);
|
||||||
|
|
||||||
|
expect(routeMetadata).toEqual({
|
||||||
|
args: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return existing route metadata for a registered handler', () => {
|
||||||
|
const handlerName: HandlerName = 'testHandler';
|
||||||
|
|
||||||
|
const initialRouteMetadata = registry.getRouteMetadata(TestController, handlerName);
|
||||||
|
|
||||||
|
initialRouteMetadata.method = 'get';
|
||||||
|
initialRouteMetadata.path = '/test';
|
||||||
|
initialRouteMetadata.args.push({ type: 'query' });
|
||||||
|
|
||||||
|
const routeMetadata = registry.getRouteMetadata(TestController, handlerName);
|
||||||
|
|
||||||
|
expect(routeMetadata).toBe(initialRouteMetadata);
|
||||||
|
expect(routeMetadata.method).toBe('get');
|
||||||
|
expect(routeMetadata.path).toBe('/test');
|
||||||
|
expect(routeMetadata.args).toEqual([{ type: 'query' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('controllerClasses', () => {
|
||||||
|
it('should return an iterator of registered controller classes', () => {
|
||||||
|
registry.getControllerMetadata(TestController);
|
||||||
|
registry.getControllerMetadata(AnotherController);
|
||||||
|
|
||||||
|
const iteratorClasses = registry.controllerClasses;
|
||||||
|
const controllers = Array.from(iteratorClasses);
|
||||||
|
|
||||||
|
expect(controllers).toHaveLength(2);
|
||||||
|
expect(controllers).toContain(TestController);
|
||||||
|
expect(controllers).toContain(AnotherController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty iterator when no controllers are registered', () => {
|
||||||
|
const iteratorClasses = registry.controllerClasses;
|
||||||
|
const controllers = Array.from(iteratorClasses);
|
||||||
|
|
||||||
|
expect(controllers).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complete controller and routes registration correctly', () => {
|
||||||
|
const controllerMetadata = registry.getControllerMetadata(TestController);
|
||||||
|
controllerMetadata.basePath = '/test-api';
|
||||||
|
controllerMetadata.middlewares = ['global'];
|
||||||
|
|
||||||
|
const route1 = registry.getRouteMetadata(TestController, 'testHandler');
|
||||||
|
route1.method = 'get';
|
||||||
|
route1.path = '/items';
|
||||||
|
route1.args = [{ type: 'query' }];
|
||||||
|
route1.middlewares = [() => {}];
|
||||||
|
route1.skipAuth = true;
|
||||||
|
|
||||||
|
const route2 = registry.getRouteMetadata(TestController, 'anotherHandler');
|
||||||
|
route2.method = 'post';
|
||||||
|
route2.path = '/items/:id';
|
||||||
|
route2.args = [{ type: 'param', key: 'id' }, { type: 'body' }];
|
||||||
|
|
||||||
|
const retrievedMetadata = registry.getControllerMetadata(TestController);
|
||||||
|
expect(retrievedMetadata.basePath).toBe('/test-api');
|
||||||
|
expect(retrievedMetadata.middlewares).toEqual(['global']);
|
||||||
|
|
||||||
|
expect(retrievedMetadata.routes.size).toBe(2);
|
||||||
|
|
||||||
|
const retrievedRoute1 = retrievedMetadata.routes.get('testHandler');
|
||||||
|
expect(retrievedRoute1?.method).toBe('get');
|
||||||
|
expect(retrievedRoute1?.path).toBe('/items');
|
||||||
|
expect(retrievedRoute1?.args).toEqual([{ type: 'query' }]);
|
||||||
|
expect(retrievedRoute1?.skipAuth).toBe(true);
|
||||||
|
expect(retrievedRoute1?.middlewares).toHaveLength(1);
|
||||||
|
|
||||||
|
const retrievedRoute2 = retrievedMetadata.routes.get('anotherHandler');
|
||||||
|
expect(retrievedRoute2?.method).toBe('post');
|
||||||
|
expect(retrievedRoute2?.path).toBe('/items/:id');
|
||||||
|
expect(retrievedRoute2?.args).toEqual([{ type: 'param', key: 'id' }, { type: 'body' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import type { BooleanLicenseFeature } from '@n8n/constants';
|
||||||
|
import { Container } from '@n8n/di';
|
||||||
|
|
||||||
|
import { ControllerRegistryMetadata } from '../controller-registry-metadata';
|
||||||
|
import { Licensed } from '../licensed';
|
||||||
|
import type { Controller } from '../types';
|
||||||
|
|
||||||
|
describe('@Licensed Decorator', () => {
|
||||||
|
let controllerRegistryMetadata: ControllerRegistryMetadata;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
controllerRegistryMetadata = new ControllerRegistryMetadata();
|
||||||
|
Container.set(ControllerRegistryMetadata, controllerRegistryMetadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set license feature on route metadata', () => {
|
||||||
|
const licenseFeature: BooleanLicenseFeature = 'feat:variables';
|
||||||
|
|
||||||
|
class TestController {
|
||||||
|
@Licensed(licenseFeature)
|
||||||
|
testMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'testMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.licenseFeature).toBe(licenseFeature);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with different license features', () => {
|
||||||
|
class TestController {
|
||||||
|
@Licensed('feat:ldap')
|
||||||
|
ldapMethod() {}
|
||||||
|
|
||||||
|
@Licensed('feat:saml')
|
||||||
|
samlMethod() {}
|
||||||
|
|
||||||
|
@Licensed('feat:sharing')
|
||||||
|
sharingMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ldapMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'ldapMethod',
|
||||||
|
);
|
||||||
|
expect(ldapMetadata.licenseFeature).toBe('feat:ldap');
|
||||||
|
|
||||||
|
const samlMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'samlMethod',
|
||||||
|
);
|
||||||
|
expect(samlMetadata.licenseFeature).toBe('feat:saml');
|
||||||
|
|
||||||
|
const sharingMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'sharingMethod',
|
||||||
|
);
|
||||||
|
expect(sharingMetadata.licenseFeature).toBe('feat:sharing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work alongside other decorators', () => {
|
||||||
|
// Assuming we have a Get decorator imported
|
||||||
|
const Get = (path: string) => {
|
||||||
|
return (target: object, handlerName: string | symbol) => {
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
target.constructor as Controller,
|
||||||
|
String(handlerName),
|
||||||
|
);
|
||||||
|
routeMetadata.method = 'get';
|
||||||
|
routeMetadata.path = path;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class TestController {
|
||||||
|
@Get('/test')
|
||||||
|
@Licensed('feat:variables')
|
||||||
|
testMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'testMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.licenseFeature).toBe('feat:variables');
|
||||||
|
expect(routeMetadata.method).toBe('get');
|
||||||
|
expect(routeMetadata.path).toBe('/test');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Container } from '@n8n/di';
|
||||||
|
|
||||||
|
import { ControllerRegistryMetadata } from '../controller-registry-metadata';
|
||||||
|
import { RestController } from '../rest-controller';
|
||||||
|
import type { Controller } from '../types';
|
||||||
|
|
||||||
|
describe('@RestController Decorator', () => {
|
||||||
|
let controllerRegistryMetadata: ControllerRegistryMetadata;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
controllerRegistryMetadata = new ControllerRegistryMetadata();
|
||||||
|
Container.set(ControllerRegistryMetadata, controllerRegistryMetadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set default base path when no path provided', () => {
|
||||||
|
@RestController()
|
||||||
|
class TestController {}
|
||||||
|
|
||||||
|
const metadata = controllerRegistryMetadata.getControllerMetadata(TestController as Controller);
|
||||||
|
expect(metadata.basePath).toBe('/');
|
||||||
|
expect(Container.has(TestController)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set custom base path when provided', () => {
|
||||||
|
@RestController('/test')
|
||||||
|
class TestController {}
|
||||||
|
|
||||||
|
const metadata = controllerRegistryMetadata.getControllerMetadata(TestController as Controller);
|
||||||
|
expect(metadata.basePath).toBe('/test');
|
||||||
|
expect(Container.has(TestController)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register the controller in the registry', () => {
|
||||||
|
@RestController('/users')
|
||||||
|
class UsersController {}
|
||||||
|
|
||||||
|
@RestController('/projects')
|
||||||
|
class ProjectsController {}
|
||||||
|
|
||||||
|
const controllers = Array.from(controllerRegistryMetadata.controllerClasses);
|
||||||
|
expect(controllers).toEqual([UsersController, ProjectsController]);
|
||||||
|
expect(Container.has(UsersController)).toBe(true);
|
||||||
|
expect(Container.has(ProjectsController)).toBe(true);
|
||||||
|
expect(
|
||||||
|
controllerRegistryMetadata.getControllerMetadata(UsersController as Controller).basePath,
|
||||||
|
).toBe('/users');
|
||||||
|
expect(
|
||||||
|
controllerRegistryMetadata.getControllerMetadata(ProjectsController as Controller).basePath,
|
||||||
|
).toBe('/projects');
|
||||||
|
});
|
||||||
|
});
|
||||||
158
packages/@n8n/decorators/src/controller/__tests__/route.test.ts
Normal file
158
packages/@n8n/decorators/src/controller/__tests__/route.test.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { Container } from '@n8n/di';
|
||||||
|
|
||||||
|
import { ControllerRegistryMetadata } from '../controller-registry-metadata';
|
||||||
|
import { Get, Post, Put, Patch, Delete } from '../route';
|
||||||
|
import type { Controller } from '../types';
|
||||||
|
|
||||||
|
describe('Route Decorators', () => {
|
||||||
|
let controllerRegistryMetadata: ControllerRegistryMetadata;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
controllerRegistryMetadata = new ControllerRegistryMetadata();
|
||||||
|
Container.set(ControllerRegistryMetadata, controllerRegistryMetadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
{ decorator: Get, method: 'Get' },
|
||||||
|
{ decorator: Post, method: 'Post' },
|
||||||
|
{ decorator: Put, method: 'Put' },
|
||||||
|
{ decorator: Patch, method: 'Patch' },
|
||||||
|
{ decorator: Delete, method: 'Delete' },
|
||||||
|
])('@$method decorator', ({ decorator, method }) => {
|
||||||
|
it('should set correct metadata with default options', () => {
|
||||||
|
class TestController {
|
||||||
|
@decorator('/test')
|
||||||
|
testMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerName = 'testMethod';
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
handlerName,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.method).toBe(method.toLowerCase());
|
||||||
|
expect(routeMetadata.path).toBe('/test');
|
||||||
|
expect(routeMetadata.middlewares).toEqual([]);
|
||||||
|
expect(routeMetadata.usesTemplates).toBe(false);
|
||||||
|
expect(routeMetadata.skipAuth).toBe(false);
|
||||||
|
expect(routeMetadata.rateLimit).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept and apply route options', () => {
|
||||||
|
const middleware = () => {};
|
||||||
|
|
||||||
|
class TestController {
|
||||||
|
@decorator('/test', {
|
||||||
|
middlewares: [middleware],
|
||||||
|
usesTemplates: true,
|
||||||
|
skipAuth: true,
|
||||||
|
rateLimit: { limit: 10, windowMs: 60000 },
|
||||||
|
})
|
||||||
|
testMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'testMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.middlewares).toEqual([middleware]);
|
||||||
|
expect(routeMetadata.usesTemplates).toBe(true);
|
||||||
|
expect(routeMetadata.skipAuth).toBe(true);
|
||||||
|
expect(routeMetadata.rateLimit).toEqual({ limit: 10, windowMs: 60000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with boolean rateLimit option', () => {
|
||||||
|
class TestController {
|
||||||
|
@decorator('/test', { rateLimit: true })
|
||||||
|
testMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'testMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.rateLimit).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with multiple routes on the same controller', () => {
|
||||||
|
class TestController {
|
||||||
|
@decorator('/first')
|
||||||
|
firstMethod() {}
|
||||||
|
|
||||||
|
@decorator('/second', { skipAuth: true })
|
||||||
|
secondMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstRouteMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'firstMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
const secondRouteMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'secondMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(firstRouteMetadata.method).toBe(method.toLowerCase());
|
||||||
|
expect(firstRouteMetadata.path).toBe('/first');
|
||||||
|
expect(firstRouteMetadata.skipAuth).toBe(false);
|
||||||
|
|
||||||
|
expect(secondRouteMetadata.method).toBe(method.toLowerCase());
|
||||||
|
expect(secondRouteMetadata.path).toBe('/second');
|
||||||
|
expect(secondRouteMetadata.skipAuth).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow different HTTP methods on the same controller', () => {
|
||||||
|
class TestController {
|
||||||
|
@Get('/users')
|
||||||
|
getUsers() {}
|
||||||
|
|
||||||
|
@Post('/users')
|
||||||
|
createUser() {}
|
||||||
|
|
||||||
|
@Put('/users/:id')
|
||||||
|
updateUser() {}
|
||||||
|
|
||||||
|
@Delete('/users/:id')
|
||||||
|
deleteUser() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'getUsers',
|
||||||
|
);
|
||||||
|
|
||||||
|
const postMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'createUser',
|
||||||
|
);
|
||||||
|
|
||||||
|
const putMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'updateUser',
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'deleteUser',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getMetadata.method).toBe('get');
|
||||||
|
expect(getMetadata.path).toBe('/users');
|
||||||
|
|
||||||
|
expect(postMetadata.method).toBe('post');
|
||||||
|
expect(postMetadata.path).toBe('/users');
|
||||||
|
|
||||||
|
expect(putMetadata.method).toBe('put');
|
||||||
|
expect(putMetadata.path).toBe('/users/:id');
|
||||||
|
|
||||||
|
expect(deleteMetadata.method).toBe('delete');
|
||||||
|
expect(deleteMetadata.path).toBe('/users/:id');
|
||||||
|
});
|
||||||
|
});
|
||||||
188
packages/@n8n/decorators/src/controller/__tests__/scoped.test.ts
Normal file
188
packages/@n8n/decorators/src/controller/__tests__/scoped.test.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { Container } from '@n8n/di';
|
||||||
|
import type { Scope } from '@n8n/permissions';
|
||||||
|
|
||||||
|
import { ControllerRegistryMetadata } from '../controller-registry-metadata';
|
||||||
|
import { GlobalScope, ProjectScope } from '../scoped';
|
||||||
|
import type { Controller } from '../types';
|
||||||
|
|
||||||
|
describe('Scope Decorators', () => {
|
||||||
|
let controllerRegistryMetadata: ControllerRegistryMetadata;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
controllerRegistryMetadata = new ControllerRegistryMetadata();
|
||||||
|
Container.set(ControllerRegistryMetadata, controllerRegistryMetadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@GlobalScope', () => {
|
||||||
|
it('should set global scope on route metadata', () => {
|
||||||
|
const scope: Scope = 'user:read';
|
||||||
|
|
||||||
|
class TestController {
|
||||||
|
@GlobalScope(scope)
|
||||||
|
testMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'testMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.accessScope).toEqual({
|
||||||
|
scope,
|
||||||
|
globalOnly: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with different scopes', () => {
|
||||||
|
class TestController {
|
||||||
|
@GlobalScope('user:read')
|
||||||
|
readUserMethod() {}
|
||||||
|
|
||||||
|
@GlobalScope('user:create')
|
||||||
|
createUserMethod() {}
|
||||||
|
|
||||||
|
@GlobalScope('user:delete')
|
||||||
|
deleteUserMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'readUserMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
const createMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'createUserMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'deleteUserMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(readMetadata.accessScope).toEqual({ scope: 'user:read', globalOnly: true });
|
||||||
|
expect(createMetadata.accessScope).toEqual({ scope: 'user:create', globalOnly: true });
|
||||||
|
expect(deleteMetadata.accessScope).toEqual({ scope: 'user:delete', globalOnly: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@ProjectScope', () => {
|
||||||
|
it('should set project scope on route metadata', () => {
|
||||||
|
const scope: Scope = 'workflow:read';
|
||||||
|
|
||||||
|
class TestController {
|
||||||
|
@ProjectScope(scope)
|
||||||
|
testMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'testMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routeMetadata.accessScope).toEqual({
|
||||||
|
scope,
|
||||||
|
globalOnly: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with different scopes', () => {
|
||||||
|
class TestController {
|
||||||
|
@ProjectScope('workflow:read')
|
||||||
|
readWorkflowMethod() {}
|
||||||
|
|
||||||
|
@ProjectScope('workflow:create')
|
||||||
|
createWorkflowMethod() {}
|
||||||
|
|
||||||
|
@ProjectScope('workflow:delete')
|
||||||
|
deleteWorkflowMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'readWorkflowMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
const createMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'createWorkflowMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'deleteWorkflowMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(readMetadata.accessScope).toEqual({ scope: 'workflow:read', globalOnly: false });
|
||||||
|
expect(createMetadata.accessScope).toEqual({ scope: 'workflow:create', globalOnly: false });
|
||||||
|
expect(deleteMetadata.accessScope).toEqual({ scope: 'workflow:delete', globalOnly: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with both scope types on the same controller', () => {
|
||||||
|
class TestController {
|
||||||
|
@GlobalScope('user:read')
|
||||||
|
readUserMethod() {}
|
||||||
|
|
||||||
|
@ProjectScope('workflow:read')
|
||||||
|
readWorkflowMethod() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'readUserMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
const workflowMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'readWorkflowMethod',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(userMetadata.accessScope).toEqual({ scope: 'user:read', globalOnly: true });
|
||||||
|
expect(workflowMetadata.accessScope).toEqual({ scope: 'workflow:read', globalOnly: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work alongside other decorators', () => {
|
||||||
|
// Assuming we have a Get decorator imported
|
||||||
|
const Get = (path: string) => {
|
||||||
|
return (target: object, handlerName: string | symbol) => {
|
||||||
|
const routeMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
target.constructor as Controller,
|
||||||
|
String(handlerName),
|
||||||
|
);
|
||||||
|
routeMetadata.method = 'get';
|
||||||
|
routeMetadata.path = path;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class TestController {
|
||||||
|
@Get('/users')
|
||||||
|
@GlobalScope('user:read')
|
||||||
|
getUsers() {}
|
||||||
|
|
||||||
|
@Get('/workflows')
|
||||||
|
@ProjectScope('workflow:read')
|
||||||
|
getWorkflows() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'getUsers',
|
||||||
|
);
|
||||||
|
|
||||||
|
const workflowsMetadata = controllerRegistryMetadata.getRouteMetadata(
|
||||||
|
TestController as Controller,
|
||||||
|
'getWorkflows',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(usersMetadata.method).toBe('get');
|
||||||
|
expect(usersMetadata.path).toBe('/users');
|
||||||
|
expect(usersMetadata.accessScope).toEqual({ scope: 'user:read', globalOnly: true });
|
||||||
|
|
||||||
|
expect(workflowsMetadata.method).toBe('get');
|
||||||
|
expect(workflowsMetadata.path).toBe('/workflows');
|
||||||
|
expect(workflowsMetadata.accessScope).toEqual({ scope: 'workflow:read', globalOnly: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
8
packages/@n8n/decorators/src/controller/index.ts
Normal file
8
packages/@n8n/decorators/src/controller/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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 type { AccessScope, Controller, RateLimit } from './types';
|
||||||
49
packages/@n8n/decorators/src/controller/types.ts
Normal file
49
packages/@n8n/decorators/src/controller/types.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { BooleanLicenseFeature } from '@n8n/constants';
|
||||||
|
import type { Constructable } from '@n8n/di';
|
||||||
|
import type { Scope } from '@n8n/permissions';
|
||||||
|
import type { RequestHandler } from 'express';
|
||||||
|
|
||||||
|
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
||||||
|
|
||||||
|
export type Arg = { type: 'body' | 'query' } | { type: 'param'; key: string };
|
||||||
|
|
||||||
|
export interface RateLimit {
|
||||||
|
/**
|
||||||
|
* The maximum number of requests to allow during the `window` before rate limiting the client.
|
||||||
|
* @default 5
|
||||||
|
*/
|
||||||
|
limit?: number;
|
||||||
|
/**
|
||||||
|
* How long we should remember the requests.
|
||||||
|
* @default 300_000 (5 minutes)
|
||||||
|
*/
|
||||||
|
windowMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HandlerName = string;
|
||||||
|
|
||||||
|
export interface AccessScope {
|
||||||
|
scope: Scope;
|
||||||
|
globalOnly: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteMetadata {
|
||||||
|
method: Method;
|
||||||
|
path: string;
|
||||||
|
middlewares: RequestHandler[];
|
||||||
|
usesTemplates: boolean;
|
||||||
|
skipAuth: boolean;
|
||||||
|
rateLimit?: boolean | RateLimit;
|
||||||
|
licenseFeature?: BooleanLicenseFeature;
|
||||||
|
accessScope?: AccessScope;
|
||||||
|
args: Arg[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControllerMetadata {
|
||||||
|
basePath: `/${string}`;
|
||||||
|
middlewares: HandlerName[];
|
||||||
|
routes: Map<HandlerName, RouteMetadata>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Controller = Constructable<object> &
|
||||||
|
Record<HandlerName, (...args: unknown[]) => Promise<unknown>>;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Container, Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
|
|
||||||
import { NonMethodError } from '../errors';
|
import { NonMethodError } from '../../errors';
|
||||||
import { LifecycleMetadata } from '../lifecycle-metadata';
|
import { LifecycleMetadata } from '../lifecycle-metadata';
|
||||||
import { OnLifecycleEvent } from '../on-lifecycle-event';
|
import { OnLifecycleEvent } from '../on-lifecycle-event';
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export { OnLifecycleEvent } from './on-lifecycle-event';
|
||||||
|
export type {
|
||||||
|
LifecycleContext,
|
||||||
|
NodeExecuteBeforeContext,
|
||||||
|
NodeExecuteAfterContext,
|
||||||
|
WorkflowExecuteBeforeContext,
|
||||||
|
WorkflowExecuteAfterContext,
|
||||||
|
} from './lifecycle-metadata';
|
||||||
|
export { LifecycleMetadata } from './lifecycle-metadata';
|
||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
Workflow,
|
Workflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import type { Class } from './types';
|
import type { Class } from '../types';
|
||||||
|
|
||||||
export type LifecycleHandlerClass = Class<
|
export type LifecycleHandlerClass = Class<
|
||||||
Record<string, (ctx: LifecycleContext) => Promise<void> | void>
|
Record<string, (ctx: LifecycleContext) => Promise<void> | void>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
|
|
||||||
import { NonMethodError } from './errors';
|
|
||||||
import type { LifecycleEvent, LifecycleHandlerClass } from './lifecycle-metadata';
|
import type { LifecycleEvent, LifecycleHandlerClass } from './lifecycle-metadata';
|
||||||
import { LifecycleMetadata } from './lifecycle-metadata';
|
import { LifecycleMetadata } from './lifecycle-metadata';
|
||||||
|
import { NonMethodError } from '../errors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decorator that registers a method to be called when a specific lifecycle event occurs.
|
* Decorator that registers a method to be called when a specific lifecycle event occurs.
|
||||||
* For more information, see `execution-lifecyle-hooks.ts` in `cli` and `core`.
|
* For more information, see `execution-lifecycle-hooks.ts` in `cli` and `core`.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
*
|
*
|
||||||
@@ -1,32 +1,8 @@
|
|||||||
export { Body, Query, Param } from './args';
|
export * from './controller';
|
||||||
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 { OnShutdown } from './on-shutdown';
|
|
||||||
export { Redactable } from './redactable';
|
|
||||||
export { BaseN8nModule, N8nModule } from './module';
|
|
||||||
export { ModuleMetadata } from './module-metadata';
|
|
||||||
export { Debounce } from './debounce';
|
export { Debounce } from './debounce';
|
||||||
export type { AccessScope, Controller, RateLimit } from './types';
|
export * from './execution-lifecycle';
|
||||||
export type { ShutdownHandler } from './types';
|
|
||||||
export { MultiMainMetadata } from './multi-main-metadata';
|
|
||||||
export { OnLeaderTakeover, OnLeaderStepdown } from './on-multi-main-event';
|
|
||||||
export { Memoized } from './memoized';
|
export { Memoized } from './memoized';
|
||||||
export { OnLifecycleEvent } from './on-lifecycle-event';
|
export * from './module';
|
||||||
export type {
|
export * from './multi-main';
|
||||||
LifecycleContext,
|
export { Redactable } from './redactable';
|
||||||
NodeExecuteBeforeContext,
|
export * from './shutdown';
|
||||||
NodeExecuteAfterContext,
|
|
||||||
WorkflowExecuteBeforeContext,
|
|
||||||
WorkflowExecuteAfterContext,
|
|
||||||
} from './lifecycle-metadata';
|
|
||||||
export { LifecycleMetadata } from './lifecycle-metadata';
|
|
||||||
|
|||||||
103
packages/@n8n/decorators/src/module/__tests__/module.test.ts
Normal file
103
packages/@n8n/decorators/src/module/__tests__/module.test.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { Container } from '@n8n/di';
|
||||||
|
|
||||||
|
import { N8nModule } from '../module';
|
||||||
|
import { ModuleMetadata } from '../module-metadata';
|
||||||
|
|
||||||
|
describe('@N8nModule Decorator', () => {
|
||||||
|
let moduleMetadata: ModuleMetadata;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
moduleMetadata = new ModuleMetadata();
|
||||||
|
Container.set(ModuleMetadata, moduleMetadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register module in ModuleMetadata', () => {
|
||||||
|
@N8nModule()
|
||||||
|
class TestModule {
|
||||||
|
initialize() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registeredModules = Array.from(moduleMetadata.getModules());
|
||||||
|
|
||||||
|
expect(registeredModules).toContain(TestModule);
|
||||||
|
expect(registeredModules).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register multiple modules', () => {
|
||||||
|
@N8nModule()
|
||||||
|
class FirstModule {
|
||||||
|
initialize() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@N8nModule()
|
||||||
|
class SecondModule {
|
||||||
|
initialize() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@N8nModule()
|
||||||
|
class ThirdModule {
|
||||||
|
initialize() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registeredModules = Array.from(moduleMetadata.getModules());
|
||||||
|
|
||||||
|
expect(registeredModules).toContain(FirstModule);
|
||||||
|
expect(registeredModules).toContain(SecondModule);
|
||||||
|
expect(registeredModules).toContain(ThirdModule);
|
||||||
|
expect(registeredModules).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with modules without initialize method', () => {
|
||||||
|
@N8nModule()
|
||||||
|
class TestModule {}
|
||||||
|
|
||||||
|
const registeredModules = Array.from(moduleMetadata.getModules());
|
||||||
|
|
||||||
|
expect(registeredModules).toContain(TestModule);
|
||||||
|
expect(registeredModules).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support async initialize method', async () => {
|
||||||
|
const mockInitialize = jest.fn();
|
||||||
|
|
||||||
|
@N8nModule()
|
||||||
|
class TestModule {
|
||||||
|
async initialize() {
|
||||||
|
mockInitialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registeredModules = Array.from(moduleMetadata.getModules());
|
||||||
|
|
||||||
|
expect(registeredModules).toContain(TestModule);
|
||||||
|
|
||||||
|
const moduleInstance = new TestModule();
|
||||||
|
await moduleInstance.initialize();
|
||||||
|
|
||||||
|
expect(mockInitialize).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ModuleMetadata', () => {
|
||||||
|
it('should allow retrieving and checking registered modules', () => {
|
||||||
|
@N8nModule()
|
||||||
|
class FirstModule {}
|
||||||
|
|
||||||
|
@N8nModule()
|
||||||
|
class SecondModule {}
|
||||||
|
|
||||||
|
const registeredModules = Array.from(moduleMetadata.getModules());
|
||||||
|
|
||||||
|
expect(registeredModules).toContain(FirstModule);
|
||||||
|
expect(registeredModules).toContain(SecondModule);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply Service decorator', () => {
|
||||||
|
@N8nModule()
|
||||||
|
class TestModule {}
|
||||||
|
|
||||||
|
expect(Container.has(TestModule)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
2
packages/@n8n/decorators/src/module/index.ts
Normal file
2
packages/@n8n/decorators/src/module/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { BaseN8nModule, N8nModule } from './module';
|
||||||
|
export { ModuleMetadata } from './module-metadata';
|
||||||
@@ -3,7 +3,7 @@ import { Container, Service, type Constructable } from '@n8n/di';
|
|||||||
import { ModuleMetadata } from './module-metadata';
|
import { ModuleMetadata } from './module-metadata';
|
||||||
|
|
||||||
export interface BaseN8nModule {
|
export interface BaseN8nModule {
|
||||||
initialize?(): void;
|
initialize?(): void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Module = Constructable<BaseN8nModule>;
|
export type Module = Constructable<BaseN8nModule>;
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Service } from '@n8n/di';
|
|
||||||
|
|
||||||
import type { Class } from './types';
|
|
||||||
|
|
||||||
export const LEADER_TAKEOVER_EVENT_NAME = 'leader-takeover';
|
|
||||||
export const LEADER_STEPDOWN_EVENT_NAME = 'leader-stepdown';
|
|
||||||
|
|
||||||
export type MultiMainEvent = typeof LEADER_TAKEOVER_EVENT_NAME | typeof LEADER_STEPDOWN_EVENT_NAME;
|
|
||||||
|
|
||||||
type EventHandlerFn = () => Promise<void> | void;
|
|
||||||
|
|
||||||
export type EventHandlerClass = Class<Record<string, EventHandlerFn>>;
|
|
||||||
|
|
||||||
type EventHandler = {
|
|
||||||
/** Class holding the method to call on a multi-main event. */
|
|
||||||
eventHandlerClass: EventHandlerClass;
|
|
||||||
|
|
||||||
/** Name of the method to call on a multi-main event. */
|
|
||||||
methodName: string;
|
|
||||||
|
|
||||||
/** Name of the multi-main event to listen to. */
|
|
||||||
eventName: MultiMainEvent;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class MultiMainMetadata {
|
|
||||||
private readonly handlers: EventHandler[] = [];
|
|
||||||
|
|
||||||
register(handler: EventHandler) {
|
|
||||||
this.handlers.push(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
getHandlers(): EventHandler[] {
|
|
||||||
return this.handlers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import { Container } from '@n8n/di';
|
|||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
|
|
||||||
import { NonMethodError } from '../errors';
|
import { NonMethodError } from '../../errors';
|
||||||
import { MultiMainMetadata } from '../multi-main-metadata';
|
import { MultiMainMetadata } from '../multi-main-metadata';
|
||||||
import { LEADER_TAKEOVER_EVENT_NAME, LEADER_STEPDOWN_EVENT_NAME } from '../multi-main-metadata';
|
import { LEADER_TAKEOVER_EVENT_NAME, LEADER_STEPDOWN_EVENT_NAME } from '../multi-main-metadata';
|
||||||
import { OnLeaderStepdown, OnLeaderTakeover } from '../on-multi-main-event';
|
import { OnLeaderStepdown, OnLeaderTakeover } from '../on-multi-main-event';
|
||||||
2
packages/@n8n/decorators/src/multi-main/index.ts
Normal file
2
packages/@n8n/decorators/src/multi-main/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { MultiMainMetadata } from './multi-main-metadata';
|
||||||
|
export { OnLeaderTakeover, OnLeaderStepdown } from './on-multi-main-event';
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Service } from '@n8n/di';
|
||||||
|
|
||||||
|
import type { EventHandler } from '../types';
|
||||||
|
|
||||||
|
export const LEADER_TAKEOVER_EVENT_NAME = 'leader-takeover';
|
||||||
|
export const LEADER_STEPDOWN_EVENT_NAME = 'leader-stepdown';
|
||||||
|
|
||||||
|
export type MultiMainEvent = typeof LEADER_TAKEOVER_EVENT_NAME | typeof LEADER_STEPDOWN_EVENT_NAME;
|
||||||
|
|
||||||
|
type MultiMainEventHandler = EventHandler<MultiMainEvent>;
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class MultiMainMetadata {
|
||||||
|
private readonly handlers: MultiMainEventHandler[] = [];
|
||||||
|
|
||||||
|
register(handler: MultiMainEventHandler) {
|
||||||
|
this.handlers.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHandlers(): MultiMainEventHandler[] {
|
||||||
|
return this.handlers;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
|
|
||||||
import { NonMethodError } from './errors';
|
import type { MultiMainEvent } from './multi-main-metadata';
|
||||||
import type { EventHandlerClass, MultiMainEvent } from './multi-main-metadata';
|
|
||||||
import {
|
import {
|
||||||
LEADER_TAKEOVER_EVENT_NAME,
|
LEADER_TAKEOVER_EVENT_NAME,
|
||||||
LEADER_STEPDOWN_EVENT_NAME,
|
LEADER_STEPDOWN_EVENT_NAME,
|
||||||
MultiMainMetadata,
|
MultiMainMetadata,
|
||||||
} from './multi-main-metadata';
|
} from './multi-main-metadata';
|
||||||
|
import { NonMethodError } from '../errors';
|
||||||
|
import type { EventHandlerClass } from '../types';
|
||||||
|
|
||||||
const OnMultiMainEvent =
|
const OnMultiMainEvent =
|
||||||
(eventName: MultiMainEvent): MethodDecorator =>
|
(eventName: MultiMainEvent): MethodDecorator =>
|
||||||
8
packages/@n8n/decorators/src/shutdown/index.ts
Normal file
8
packages/@n8n/decorators/src/shutdown/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export {
|
||||||
|
HIGHEST_SHUTDOWN_PRIORITY,
|
||||||
|
DEFAULT_SHUTDOWN_PRIORITY,
|
||||||
|
LOWEST_SHUTDOWN_PRIORITY,
|
||||||
|
} from './constants';
|
||||||
|
export { ShutdownRegistryMetadata } from './shutdown-registry-metadata';
|
||||||
|
export { OnShutdown } from './on-shutdown';
|
||||||
|
export type { ShutdownHandler, ShutdownServiceClass } from './types';
|
||||||
@@ -1,9 +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 './shutdown/constants';
|
import { DEFAULT_SHUTDOWN_PRIORITY } from './constants';
|
||||||
import { ShutdownRegistryMetadata } from './shutdown-registry-metadata';
|
import { ShutdownRegistryMetadata } from './shutdown-registry-metadata';
|
||||||
import type { ServiceClass } from './types';
|
import type { ShutdownServiceClass } 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
|
||||||
@@ -27,7 +27,7 @@ import type { ServiceClass } from './types';
|
|||||||
export const OnShutdown =
|
export const OnShutdown =
|
||||||
(priority = DEFAULT_SHUTDOWN_PRIORITY): MethodDecorator =>
|
(priority = DEFAULT_SHUTDOWN_PRIORITY): MethodDecorator =>
|
||||||
(prototype, propertyKey, descriptor) => {
|
(prototype, propertyKey, descriptor) => {
|
||||||
const serviceClass = prototype.constructor as ServiceClass;
|
const serviceClass = prototype.constructor as ShutdownServiceClass;
|
||||||
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') {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { UserError } from 'n8n-workflow';
|
import { UserError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { HIGHEST_SHUTDOWN_PRIORITY, LOWEST_SHUTDOWN_PRIORITY } from './shutdown/constants';
|
import { HIGHEST_SHUTDOWN_PRIORITY, LOWEST_SHUTDOWN_PRIORITY } from './constants';
|
||||||
import type { ShutdownHandler } from './types';
|
import type { ShutdownHandler } from './types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
9
packages/@n8n/decorators/src/shutdown/types.ts
Normal file
9
packages/@n8n/decorators/src/shutdown/types.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Class } from '../types';
|
||||||
|
|
||||||
|
type ShutdownHandlerFn = () => Promise<void> | void;
|
||||||
|
export type ShutdownServiceClass = Class<Record<string, ShutdownHandlerFn>>;
|
||||||
|
|
||||||
|
export interface ShutdownHandler {
|
||||||
|
serviceClass: ShutdownServiceClass;
|
||||||
|
methodName: string;
|
||||||
|
}
|
||||||
@@ -1,60 +1,14 @@
|
|||||||
import type { BooleanLicenseFeature } from '@n8n/constants';
|
|
||||||
import type { Constructable } from '@n8n/di';
|
|
||||||
import type { Scope } from '@n8n/permissions';
|
|
||||||
import type { RequestHandler } from 'express';
|
|
||||||
|
|
||||||
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
||||||
|
|
||||||
export type Arg = { type: 'body' | 'query' } | { type: 'param'; key: string };
|
|
||||||
|
|
||||||
export interface RateLimit {
|
|
||||||
/**
|
|
||||||
* The maximum number of requests to allow during the `window` before rate limiting the client.
|
|
||||||
* @default 5
|
|
||||||
*/
|
|
||||||
limit?: number;
|
|
||||||
/**
|
|
||||||
* How long we should remember the requests.
|
|
||||||
* @default 300_000 (5 minutes)
|
|
||||||
*/
|
|
||||||
windowMs?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HandlerName = string;
|
|
||||||
|
|
||||||
export interface AccessScope {
|
|
||||||
scope: Scope;
|
|
||||||
globalOnly: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RouteMetadata {
|
|
||||||
method: Method;
|
|
||||||
path: string;
|
|
||||||
middlewares: RequestHandler[];
|
|
||||||
usesTemplates: boolean;
|
|
||||||
skipAuth: boolean;
|
|
||||||
rateLimit?: boolean | RateLimit;
|
|
||||||
licenseFeature?: BooleanLicenseFeature;
|
|
||||||
accessScope?: AccessScope;
|
|
||||||
args: Arg[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ControllerMetadata {
|
|
||||||
basePath: `/${string}`;
|
|
||||||
middlewares: HandlerName[];
|
|
||||||
routes: Map<HandlerName, RouteMetadata>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Controller = Constructable<object> &
|
|
||||||
Record<HandlerName, (...args: unknown[]) => Promise<unknown>>;
|
|
||||||
|
|
||||||
type RouteHandlerFn = () => Promise<void> | void;
|
|
||||||
|
|
||||||
export type Class<T = object, A extends unknown[] = unknown[]> = new (...args: A) => T;
|
export type Class<T = object, A extends unknown[] = unknown[]> = new (...args: A) => T;
|
||||||
|
|
||||||
export type ServiceClass = Class<Record<string, RouteHandlerFn>>;
|
type EventHandlerFn = () => Promise<void> | void;
|
||||||
|
export type EventHandlerClass = Class<Record<string, EventHandlerFn>>;
|
||||||
|
export type EventHandler<T extends string> = {
|
||||||
|
/** Class holding the method to call on an event. */
|
||||||
|
eventHandlerClass: EventHandlerClass;
|
||||||
|
|
||||||
export interface ShutdownHandler {
|
/** Name of the method to call on an event. */
|
||||||
serviceClass: ServiceClass;
|
|
||||||
methodName: string;
|
methodName: string;
|
||||||
}
|
|
||||||
|
/** Name of the event to listen to. */
|
||||||
|
eventName: T;
|
||||||
|
};
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export abstract class BaseCommand extends Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Container.get(ModuleRegistry).initializeModules();
|
await Container.get(ModuleRegistry).initializeModules();
|
||||||
|
|
||||||
if (this.instanceSettings.isMultiMain) {
|
if (this.instanceSettings.isMultiMain) {
|
||||||
Container.get(MultiMainSetup).registerEventHandlers();
|
Container.get(MultiMainSetup).registerEventHandlers();
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export class ModuleRegistry {
|
|||||||
private readonly lifecycleMetadata: LifecycleMetadata,
|
private readonly lifecycleMetadata: LifecycleMetadata,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
initializeModules() {
|
async initializeModules() {
|
||||||
for (const ModuleClass of this.moduleMetadata.getModules()) {
|
for (const ModuleClass of this.moduleMetadata.getModules()) {
|
||||||
Container.get(ModuleClass).initialize?.();
|
await Container.get(ModuleClass).initialize?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ShutdownRegistryMetadata } from '@n8n/decorators';
|
import { ShutdownRegistryMetadata } from '@n8n/decorators';
|
||||||
import type { ServiceClass } from '@n8n/decorators/src/types';
|
import type { ShutdownServiceClass } from '@n8n/decorators';
|
||||||
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';
|
||||||
@@ -29,7 +29,7 @@ describe('ShutdownService', () => {
|
|||||||
describe('shutdown', () => {
|
describe('shutdown', () => {
|
||||||
it('should signal shutdown', () => {
|
it('should signal shutdown', () => {
|
||||||
shutdownService.register(10, {
|
shutdownService.register(10, {
|
||||||
serviceClass: MockComponent as unknown as ServiceClass,
|
serviceClass: MockComponent as unknown as ShutdownServiceClass,
|
||||||
methodName: 'onShutdown',
|
methodName: 'onShutdown',
|
||||||
});
|
});
|
||||||
shutdownService.shutdown();
|
shutdownService.shutdown();
|
||||||
@@ -51,12 +51,12 @@ describe('ShutdownService', () => {
|
|||||||
jest.spyOn(mockService, 'onShutdownLowPrio').mockImplementation(() => order.push('low'));
|
jest.spyOn(mockService, 'onShutdownLowPrio').mockImplementation(() => order.push('low'));
|
||||||
|
|
||||||
shutdownService.register(100, {
|
shutdownService.register(100, {
|
||||||
serviceClass: MockService as unknown as ServiceClass,
|
serviceClass: MockService as unknown as ShutdownServiceClass,
|
||||||
methodName: 'onShutdownHighPrio',
|
methodName: 'onShutdownHighPrio',
|
||||||
});
|
});
|
||||||
|
|
||||||
shutdownService.register(10, {
|
shutdownService.register(10, {
|
||||||
serviceClass: MockService as unknown as ServiceClass,
|
serviceClass: MockService as unknown as ShutdownServiceClass,
|
||||||
methodName: 'onShutdownLowPrio',
|
methodName: 'onShutdownLowPrio',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ describe('ShutdownService', () => {
|
|||||||
it('should throw error if shutdown is already in progress', () => {
|
it('should throw error if shutdown is already in progress', () => {
|
||||||
shutdownService.register(10, {
|
shutdownService.register(10, {
|
||||||
methodName: 'onShutdown',
|
methodName: 'onShutdown',
|
||||||
serviceClass: MockComponent as unknown as ServiceClass,
|
serviceClass: MockComponent as unknown as ShutdownServiceClass,
|
||||||
});
|
});
|
||||||
shutdownService.shutdown();
|
shutdownService.shutdown();
|
||||||
expect(() => shutdownService.shutdown()).toThrow('App is already shutting down');
|
expect(() => shutdownService.shutdown()).toThrow('App is already shutting down');
|
||||||
@@ -80,7 +80,7 @@ describe('ShutdownService', () => {
|
|||||||
throw componentError;
|
throw componentError;
|
||||||
});
|
});
|
||||||
shutdownService.register(10, {
|
shutdownService.register(10, {
|
||||||
serviceClass: MockComponent as unknown as ServiceClass,
|
serviceClass: MockComponent as unknown as ShutdownServiceClass,
|
||||||
methodName: 'onShutdown',
|
methodName: 'onShutdown',
|
||||||
});
|
});
|
||||||
shutdownService.shutdown();
|
shutdownService.shutdown();
|
||||||
@@ -100,7 +100,7 @@ describe('ShutdownService', () => {
|
|||||||
describe('waitForShutdown', () => {
|
describe('waitForShutdown', () => {
|
||||||
it('should wait for shutdown', async () => {
|
it('should wait for shutdown', async () => {
|
||||||
shutdownService.register(10, {
|
shutdownService.register(10, {
|
||||||
serviceClass: MockComponent as unknown as ServiceClass,
|
serviceClass: MockComponent as unknown as ShutdownServiceClass,
|
||||||
methodName: 'onShutdown',
|
methodName: 'onShutdown',
|
||||||
});
|
});
|
||||||
shutdownService.shutdown();
|
shutdownService.shutdown();
|
||||||
@@ -117,7 +117,7 @@ describe('ShutdownService', () => {
|
|||||||
describe('isShuttingDown', () => {
|
describe('isShuttingDown', () => {
|
||||||
it('should return true if app is shutting down', () => {
|
it('should return true if app is shutting down', () => {
|
||||||
shutdownService.register(10, {
|
shutdownService.register(10, {
|
||||||
serviceClass: MockComponent as unknown as ServiceClass,
|
serviceClass: MockComponent as unknown as ShutdownServiceClass,
|
||||||
methodName: 'onShutdown',
|
methodName: 'onShutdown',
|
||||||
});
|
});
|
||||||
shutdownService.shutdown();
|
shutdownService.shutdown();
|
||||||
@@ -136,7 +136,7 @@ describe('ShutdownService', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shutdownService.register(10, {
|
shutdownService.register(10, {
|
||||||
serviceClass: UnregisteredComponent as unknown as ServiceClass,
|
serviceClass: UnregisteredComponent as unknown as ShutdownServiceClass,
|
||||||
methodName: 'onShutdown',
|
methodName: 'onShutdown',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ describe('ShutdownService', () => {
|
|||||||
class TestComponent {}
|
class TestComponent {}
|
||||||
|
|
||||||
shutdownService.register(10, {
|
shutdownService.register(10, {
|
||||||
serviceClass: TestComponent as unknown as ServiceClass,
|
serviceClass: TestComponent as unknown as ShutdownServiceClass,
|
||||||
methodName: 'onShutdown',
|
methodName: 'onShutdown',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user