refactor(core): Use Dependency Injection for all Controller classes (no-changelog) (#8146)

## Review / Merge checklist
- [x] PR title and summary are descriptive
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-12-27 11:50:43 +01:00 committed by GitHub
parent 518a99e528
commit f69ddcd796
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 209 additions and 522 deletions

View file

@ -10,7 +10,6 @@ import { N8N_VERSION, inDevelopment, inTest } from '@/constants';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { N8nInstanceType } from '@/Interfaces'; import { N8nInstanceType } from '@/Interfaces';
import type { IExternalHooksClass } from '@/Interfaces';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { send, sendErrorResponse } from '@/ResponseHelper'; import { send, sendErrorResponse } from '@/ResponseHelper';
import { rawBodyReader, bodyParser, corsMiddleware } from '@/middlewares'; import { rawBodyReader, bodyParser, corsMiddleware } from '@/middlewares';
@ -31,7 +30,7 @@ export abstract class AbstractServer {
readonly app: express.Application; readonly app: express.Application;
protected externalHooks: IExternalHooksClass; protected externalHooks: ExternalHooks;
protected activeWorkflowRunner: ActiveWorkflowRunner; protected activeWorkflowRunner: ActiveWorkflowRunner;

View file

@ -1,10 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import { Service } from 'typedi'; import { Service } from 'typedi';
import type { import type { IExternalHooksFileData, IExternalHooksFunctions } from '@/Interfaces';
IExternalHooksClass,
IExternalHooksFileData,
IExternalHooksFunctions,
} from '@/Interfaces';
import config from '@/config'; import config from '@/config';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { CredentialsRepository } from '@db/repositories/credentials.repository';
@ -13,7 +9,7 @@ import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { ApplicationError } from 'n8n-workflow'; import { ApplicationError } from 'n8n-workflow';
@Service() @Service()
export class ExternalHooks implements IExternalHooksClass { export class ExternalHooks {
externalHooks: { externalHooks: {
[key: string]: Array<() => {}>; [key: string]: Array<() => {}>;
} = {}; } = {};

View file

@ -1,12 +1,10 @@
import { Authorized, Get, Post, RestController, RequireGlobalScope } from '@/decorators'; import { Authorized, Get, Post, RestController, RequireGlobalScope } from '@/decorators';
import { ExternalSecretsRequest } from '@/requests'; import { ExternalSecretsRequest } from '@/requests';
import { Response } from 'express'; import { Response } from 'express';
import { Service } from 'typedi';
import { ExternalSecretsService } from './ExternalSecrets.service.ee'; import { ExternalSecretsService } from './ExternalSecrets.service.ee';
import { ExternalSecretsProviderNotFoundError } from '@/errors/external-secrets-provider-not-found.error'; import { ExternalSecretsProviderNotFoundError } from '@/errors/external-secrets-provider-not-found.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
@Service()
@Authorized() @Authorized()
@RestController('/external-secrets') @RestController('/external-secrets')
export class ExternalSecretsController { export class ExternalSecretsController {

View file

@ -44,6 +44,7 @@ import type { CredentialsRepository } from '@db/repositories/credentials.reposit
import type { SettingsRepository } from '@db/repositories/settings.repository'; import type { SettingsRepository } from '@db/repositories/settings.repository';
import type { UserRepository } from '@db/repositories/user.repository'; import type { UserRepository } from '@db/repositories/user.repository';
import type { WorkflowRepository } from '@db/repositories/workflow.repository'; import type { WorkflowRepository } from '@db/repositories/workflow.repository';
import type { ExternalHooks } from './ExternalHooks';
import type { LICENSE_FEATURES, LICENSE_QUOTAS } from './constants'; import type { LICENSE_FEATURES, LICENSE_QUOTAS } from './constants';
import type { WorkflowWithSharingsAndCredentials } from './workflows/workflows.types'; import type { WorkflowWithSharingsAndCredentials } from './workflows/workflows.types';
import type { WorkerJobStatusSummary } from './services/orchestration/worker/types'; import type { WorkerJobStatusSummary } from './services/orchestration/worker/types';
@ -254,11 +255,6 @@ export interface IExternalHooksFunctions {
}; };
} }
export interface IExternalHooksClass {
init(): Promise<void>;
run(hookName: string, hookParameters?: any[]): Promise<void>;
}
export type WebhookCORSRequest = Request & { method: 'OPTIONS' }; export type WebhookCORSRequest = Request & { method: 'OPTIONS' };
export type WebhookRequest = Request<{ path: string }> & { export type WebhookRequest = Request<{ path: string }> & {
@ -326,134 +322,6 @@ export interface ITelemetryUserDeletionData {
migration_user_id?: string; migration_user_id?: string;
} }
export interface IInternalHooksClass {
onN8nStop(): Promise<void>;
onServerStarted(
diagnosticInfo: IDiagnosticInfo,
firstWorkflowCreatedAt?: Date,
): Promise<unknown[]>;
onPersonalizationSurveySubmitted(userId: string, answers: Record<string, string>): Promise<void>;
onWorkflowCreated(user: User, workflow: IWorkflowBase, publicApi: boolean): Promise<void>;
onWorkflowDeleted(user: User, workflowId: string, publicApi: boolean): Promise<void>;
onWorkflowSaved(user: User, workflow: IWorkflowBase, publicApi: boolean): Promise<void>;
onWorkflowBeforeExecute(executionId: string, data: IWorkflowExecutionDataProcess): Promise<void>;
onWorkflowPostExecute(
executionId: string,
workflow: IWorkflowBase,
runData?: IRun,
userId?: string,
): Promise<void>;
onNodeBeforeExecute(
executionId: string,
workflow: IWorkflowBase,
nodeName: string,
): Promise<void>;
onNodePostExecute(executionId: string, workflow: IWorkflowBase, nodeName: string): Promise<void>;
onUserDeletion(userDeletionData: {
user: User;
telemetryData: ITelemetryUserDeletionData;
publicApi: boolean;
}): Promise<void>;
onUserInvite(userInviteData: {
user: User;
target_user_id: string[];
public_api: boolean;
email_sent: boolean;
invitee_role: string;
}): Promise<void>;
onUserRoleChange(userInviteData: {
user: User;
target_user_id: string;
public_api: boolean;
target_user_new_role: string;
}): Promise<void>;
onUserReinvite(userReinviteData: {
user: User;
target_user_id: string;
public_api: boolean;
}): Promise<void>;
onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }): Promise<void>;
onUserInviteEmailClick(userInviteClickData: { inviter: User; invitee: User }): Promise<void>;
onUserPasswordResetEmailClick(userPasswordResetData: { user: User }): Promise<void>;
onUserTransactionalEmail(
userTransactionalEmailData: {
user_id: string;
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
public_api: boolean;
},
user?: User,
): Promise<void>;
onEmailFailed(failedEmailData: {
user: User;
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
public_api: boolean;
}): Promise<void>;
onUserCreatedCredentials(userCreatedCredentialsData: {
user: User;
credential_name: string;
credential_type: string;
credential_id: string;
public_api: boolean;
}): Promise<void>;
onUserSharedCredentials(userSharedCredentialsData: {
user: User;
credential_name: string;
credential_type: string;
credential_id: string;
user_id_sharer: string;
user_ids_sharees_added: string[];
sharees_removed: number | null;
}): Promise<void>;
onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise<void>;
onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }, user?: User): Promise<void>;
onUserSignup(
user: User,
userSignupData: {
user_type: AuthProviderType;
was_disabled_ldap_user: boolean;
},
): Promise<void>;
onCommunityPackageInstallFinished(installationData: {
user: User;
input_string: string;
package_name: string;
success: boolean;
package_version?: string;
package_node_names?: string[];
package_author?: string;
package_author_email?: string;
failure_reason?: string;
}): Promise<void>;
onCommunityPackageUpdateFinished(updateData: {
user: User;
package_name: string;
package_version_current: string;
package_version_new: string;
package_node_names: string[];
package_author?: string;
package_author_email?: string;
}): Promise<void>;
onCommunityPackageDeleteFinished(deleteData: {
user: User;
package_name: string;
package_version?: string;
package_node_names?: string[];
package_author?: string;
package_author_email?: string;
}): Promise<void>;
onApiKeyCreated(apiKeyDeletedData: { user: User; public_api: boolean }): Promise<void>;
onApiKeyDeleted(apiKeyDeletedData: { user: User; public_api: boolean }): Promise<void>;
onVariableCreated(createData: { variable_type: string }): Promise<void>;
onExternalSecretsProviderSettingsSaved(saveData: {
user_id?: string;
vault_type: string;
is_valid: boolean;
is_new: boolean;
error_message?: string;
}): Promise<void>;
}
export interface IVersionNotificationSettings { export interface IVersionNotificationSettings {
enabled: boolean; enabled: boolean;
endpoint: string; endpoint: string;
@ -839,7 +707,7 @@ export interface PublicUser {
export interface N8nApp { export interface N8nApp {
app: Application; app: Application;
restEndpoint: string; restEndpoint: string;
externalHooks: IExternalHooksClass; externalHooks: ExternalHooks;
activeWorkflowRunner: ActiveWorkflowRunner; activeWorkflowRunner: ActiveWorkflowRunner;
} }

View file

@ -13,7 +13,6 @@ import { TelemetryHelpers } from 'n8n-workflow';
import { get as pslGet } from 'psl'; import { get as pslGet } from 'psl';
import type { import type {
IDiagnosticInfo, IDiagnosticInfo,
IInternalHooksClass,
ITelemetryUserDeletionData, ITelemetryUserDeletionData,
IWorkflowDb, IWorkflowDb,
IExecutionTrackProperties, IExecutionTrackProperties,
@ -49,7 +48,7 @@ function userToPayload(user: User): {
} }
@Service() @Service()
export class InternalHooks implements IInternalHooksClass { export class InternalHooks {
constructor( constructor(
private telemetry: Telemetry, private telemetry: Telemetry,
private nodeTypes: NodeTypes, private nodeTypes: NodeTypes,

View file

@ -3,7 +3,7 @@ import { Container, Service } from 'typedi';
import path from 'path'; import path from 'path';
import fsPromises from 'fs/promises'; import fsPromises from 'fs/promises';
import type { DirectoryLoader, Types } from 'n8n-core'; import type { Class, DirectoryLoader, Types } from 'n8n-core';
import { import {
CUSTOM_EXTENSION_ENV, CUSTOM_EXTENSION_ENV,
InstanceSettings, InstanceSettings,
@ -250,7 +250,7 @@ export class LoadNodesAndCredentials {
* Run a loader of source files of nodes and credentials in a directory. * Run a loader of source files of nodes and credentials in a directory.
*/ */
private async runDirectoryLoader<T extends DirectoryLoader>( private async runDirectoryLoader<T extends DirectoryLoader>(
constructor: new (...args: ConstructorParameters<typeof DirectoryLoader>) => T, constructor: Class<T, ConstructorParameters<typeof DirectoryLoader>>,
dir: string, dir: string,
) { ) {
const loader = new constructor(dir, this.excludeNodes, this.includeNodes); const loader = new constructor(dir, this.excludeNodes, this.includeNodes);

View file

@ -1,9 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/no-shadow */ /* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Container, Service } from 'typedi'; import { Container, Service } from 'typedi';
@ -20,14 +17,9 @@ import type { ServeStaticOptions } from 'serve-static';
import type { FindManyOptions, FindOptionsWhere } from 'typeorm'; import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
import { Not, In } from 'typeorm'; import { Not, In } from 'typeorm';
import { InstanceSettings } from 'n8n-core'; import { type Class, InstanceSettings } from 'n8n-core';
import type { import type { ExecutionStatus, IExecutionsSummary, IN8nUISettings } from 'n8n-workflow';
ICredentialTypes,
ExecutionStatus,
IExecutionsSummary,
IN8nUISettings,
} from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
// @ts-ignore // @ts-ignore
@ -52,7 +44,6 @@ import { registerController } from '@/decorators';
import { AuthController } from '@/controllers/auth.controller'; import { AuthController } from '@/controllers/auth.controller';
import { BinaryDataController } from '@/controllers/binaryData.controller'; import { BinaryDataController } from '@/controllers/binaryData.controller';
import { DynamicNodeParametersController } from '@/controllers/dynamicNodeParameters.controller'; import { DynamicNodeParametersController } from '@/controllers/dynamicNodeParameters.controller';
import { LdapController } from '@/controllers/ldap.controller';
import { MeController } from '@/controllers/me.controller'; import { MeController } from '@/controllers/me.controller';
import { MFAController } from '@/controllers/mfa.controller'; import { MFAController } from '@/controllers/mfa.controller';
import { NodeTypesController } from '@/controllers/nodeTypes.controller'; import { NodeTypesController } from '@/controllers/nodeTypes.controller';
@ -70,9 +61,7 @@ import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi';
import type { ICredentialsOverwrite, IDiagnosticInfo, IExecutionsStopData } from '@/Interfaces'; import type { ICredentialsOverwrite, IDiagnosticInfo, IExecutionsStopData } from '@/Interfaces';
import { ActiveExecutions } from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { CredentialTypes } from '@/CredentialTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import { WaitTracker } from '@/WaitTracker'; import { WaitTracker } from '@/WaitTracker';
import { toHttpNodeParameters } from '@/CurlConverterHelper'; import { toHttpNodeParameters } from '@/CurlConverterHelper';
@ -91,7 +80,6 @@ import { getStatusUsingPreviousExecutionStatusMethod } from './executions/execut
import { SamlController } from './sso/saml/routes/saml.controller.ee'; import { SamlController } from './sso/saml/routes/saml.controller.ee';
import { SamlService } from './sso/saml/saml.service.ee'; import { SamlService } from './sso/saml/saml.service.ee';
import { VariablesController } from './environments/variables/variables.controller.ee'; import { VariablesController } from './environments/variables/variables.controller.ee';
import { LdapManager } from './Ldap/LdapManager.ee';
import { import {
isLdapCurrentAuthenticationMethod, isLdapCurrentAuthenticationMethod,
isSamlCurrentAuthenticationMethod, isSamlCurrentAuthenticationMethod,
@ -101,16 +89,10 @@ import { SourceControlController } from '@/environments/sourceControl/sourceCont
import type { ExecutionEntity } from '@db/entities/ExecutionEntity'; import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
import { ExecutionRepository } from '@db/repositories/execution.repository'; import { ExecutionRepository } from '@db/repositories/execution.repository';
import { SettingsRepository } from '@db/repositories/settings.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { MfaService } from './Mfa/mfa.service';
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers'; import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
import type { FrontendService } from './services/frontend.service'; import type { FrontendService } from './services/frontend.service';
import { RoleService } from './services/role.service';
import { UserService } from './services/user.service';
import { ActiveWorkflowsController } from './controllers/activeWorkflows.controller'; import { ActiveWorkflowsController } from './controllers/activeWorkflows.controller';
import { OrchestrationController } from './controllers/orchestration.controller'; import { OrchestrationController } from './controllers/orchestration.controller';
import { WorkflowHistoryController } from './workflows/workflowHistory/workflowHistory.controller.ee'; import { WorkflowHistoryController } from './workflows/workflowHistory/workflowHistory.controller.ee';
@ -120,7 +102,6 @@ import { RoleController } from './controllers/role.controller';
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 { MultiMainSetup } from './services/orchestration/main/MultiMainSetup.ee'; import { MultiMainSetup } from './services/orchestration/main/MultiMainSetup.ee';
import { PasswordUtility } from './services/password.utility';
const exec = promisify(callbackExec); const exec = promisify(callbackExec);
@ -136,16 +117,8 @@ export class Server extends AbstractServer {
private loadNodesAndCredentials: LoadNodesAndCredentials; private loadNodesAndCredentials: LoadNodesAndCredentials;
private nodeTypes: NodeTypes;
private credentialTypes: ICredentialTypes;
private frontendService?: FrontendService; private frontendService?: FrontendService;
private postHog: PostHogClient;
private collaborationService: CollaborationService;
constructor() { constructor() {
super('main'); super('main');
@ -159,8 +132,6 @@ export class Server extends AbstractServer {
async start() { async start() {
this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials); this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
this.credentialTypes = Container.get(CredentialTypes);
this.nodeTypes = Container.get(NodeTypes);
if (!config.getEnv('endpoints.disableUi')) { if (!config.getEnv('endpoints.disableUi')) {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@ -169,7 +140,6 @@ export class Server extends AbstractServer {
this.activeExecutionsInstance = Container.get(ActiveExecutions); this.activeExecutionsInstance = Container.get(ActiveExecutions);
this.waitTracker = Container.get(WaitTracker); this.waitTracker = Container.get(WaitTracker);
this.postHog = Container.get(PostHogClient);
this.presetCredentialsLoaded = false; this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint'); this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
@ -241,101 +211,70 @@ export class Server extends AbstractServer {
.then(async (workflow) => .then(async (workflow) =>
Container.get(InternalHooks).onServerStarted(diagnosticInfo, workflow?.createdAt), Container.get(InternalHooks).onServerStarted(diagnosticInfo, workflow?.createdAt),
); );
this.collaborationService = Container.get(CollaborationService);
Container.get(CollaborationService);
} }
private async registerControllers(ignoredEndpoints: Readonly<string[]>) { private async registerControllers(ignoredEndpoints: Readonly<string[]>) {
const { app, externalHooks, activeWorkflowRunner, nodeTypes, logger } = this; const { app } = this;
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint); setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint);
const internalHooks = Container.get(InternalHooks); const controllers: Array<Class<object>> = [
const userService = Container.get(UserService); EventBusController,
const postHog = this.postHog; EventBusControllerEE,
const mfaService = Container.get(MfaService); AuthController,
LicenseController,
const controllers: object[] = [ OAuth1CredentialController,
new EventBusController(), OAuth2CredentialController,
new EventBusControllerEE(), OwnerController,
Container.get(AuthController), MeController,
Container.get(LicenseController), DynamicNodeParametersController,
Container.get(OAuth1CredentialController), NodeTypesController,
Container.get(OAuth2CredentialController), PasswordResetController,
new OwnerController( TagsController,
config, TranslationController,
logger, UsersController,
internalHooks, SamlController,
Container.get(SettingsRepository), SourceControlController,
userService, WorkflowStatisticsController,
Container.get(PasswordUtility), ExternalSecretsController,
postHog, OrchestrationController,
), WorkflowHistoryController,
Container.get(MeController), BinaryDataController,
Container.get(DynamicNodeParametersController), VariablesController,
new NodeTypesController(config, nodeTypes), InvitationController,
Container.get(PasswordResetController), VariablesController,
Container.get(TagsController), RoleController,
new TranslationController(config, this.credentialTypes), ActiveWorkflowsController,
new UsersController(
logger,
externalHooks,
internalHooks,
Container.get(SharedCredentialsRepository),
Container.get(SharedWorkflowRepository),
activeWorkflowRunner,
Container.get(RoleService),
userService,
Container.get(License),
),
Container.get(SamlController),
Container.get(SourceControlController),
Container.get(WorkflowStatisticsController),
Container.get(ExternalSecretsController),
Container.get(OrchestrationController),
Container.get(WorkflowHistoryController),
Container.get(BinaryDataController),
Container.get(VariablesController),
new InvitationController(
config,
logger,
internalHooks,
externalHooks,
Container.get(UserService),
Container.get(License),
Container.get(PasswordUtility),
postHog,
),
Container.get(VariablesController),
Container.get(RoleController),
Container.get(ActiveWorkflowsController),
]; ];
if (Container.get(MultiMainSetup).isEnabled) { if (Container.get(MultiMainSetup).isEnabled) {
const { DebugController } = await import('./controllers/debug.controller'); const { DebugController } = await import('./controllers/debug.controller');
controllers.push(Container.get(DebugController)); controllers.push(DebugController);
} }
if (isLdapEnabled()) { if (isLdapEnabled()) {
const { service, sync } = LdapManager.getInstance(); const { LdapController } = await require('@/controllers/ldap.controller');
controllers.push(new LdapController(service, sync, internalHooks)); controllers.push(LdapController);
} }
if (config.getEnv('nodes.communityPackages.enabled')) { if (config.getEnv('nodes.communityPackages.enabled')) {
const { CommunityPackagesController } = await import( const { CommunityPackagesController } = await import(
'@/controllers/communityPackages.controller' '@/controllers/communityPackages.controller'
); );
controllers.push(Container.get(CommunityPackagesController)); controllers.push(CommunityPackagesController);
} }
if (inE2ETests) { if (inE2ETests) {
const { E2EController } = await import('./controllers/e2e.controller'); const { E2EController } = await import('./controllers/e2e.controller');
controllers.push(Container.get(E2EController)); controllers.push(E2EController);
} }
if (isMfaFeatureEnabled()) { if (isMfaFeatureEnabled()) {
controllers.push(new MFAController(mfaService)); controllers.push(MFAController);
} }
controllers.forEach((controller) => registerController(app, config, controller)); controllers.forEach((controller) => registerController(app, controller));
} }
async configure(): Promise<void> { async configure(): Promise<void> {
@ -356,7 +295,7 @@ export class Server extends AbstractServer {
await this.externalHooks.run('frontend.settings', [frontendService.getSettings()]); await this.externalHooks.run('frontend.settings', [frontendService.getSettings()]);
} }
await this.postHog.init(); await Container.get(PostHogClient).init();
const publicApiEndpoint = config.getEnv('publicApi.path'); const publicApiEndpoint = config.getEnv('publicApi.path');
const excludeEndpoints = config.getEnv('security.excludeEndpoints'); const excludeEndpoints = config.getEnv('security.excludeEndpoints');
@ -450,11 +389,7 @@ export class Server extends AbstractServer {
// ---------------------------------------- // ----------------------------------------
this.app.post( this.app.post(
`/${this.restEndpoint}/curl-to-json`, `/${this.restEndpoint}/curl-to-json`,
ResponseHelper.send( ResponseHelper.send(async (req: CurlHelper.ToJson) => {
async (
req: CurlHelper.ToJson,
res: express.Response,
): Promise<{ [key: string]: string }> => {
const curlCommand = req.body.curlCommand ?? ''; const curlCommand = req.body.curlCommand ?? '';
try { try {
@ -463,8 +398,7 @@ export class Server extends AbstractServer {
} catch (e) { } catch (e) {
throw new BadRequestError('Invalid cURL command'); throw new BadRequestError('Invalid cURL command');
} }
}, }),
),
); );
// ---------------------------------------- // ----------------------------------------
@ -687,9 +621,7 @@ export class Server extends AbstractServer {
// Returns all the available timezones // Returns all the available timezones
this.app.get( this.app.get(
`/${this.restEndpoint}/options/timezones`, `/${this.restEndpoint}/options/timezones`,
ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<object> => { ResponseHelper.send(async () => timezones),
return timezones;
}),
); );
// ---------------------------------------- // ----------------------------------------
@ -701,13 +633,8 @@ export class Server extends AbstractServer {
this.app.get( this.app.get(
`/${this.restEndpoint}/settings`, `/${this.restEndpoint}/settings`,
ResponseHelper.send( ResponseHelper.send(
async (req: express.Request, res: express.Response): Promise<IN8nUISettings> => { async (req: express.Request): Promise<IN8nUISettings> =>
void Container.get(InternalHooks).onFrontendSettingsAPI( frontendService.getSettings(req.headers.sessionid as string),
req.headers.sessionid as string,
);
return frontendService.getSettings();
},
), ),
); );
} }
@ -766,6 +693,7 @@ export class Server extends AbstractServer {
}; };
const serveIcons: express.RequestHandler = async (req, res) => { const serveIcons: express.RequestHandler = async (req, res) => {
// eslint-disable-next-line prefer-const
let { scope, packageName } = req.params; let { scope, packageName } = req.params;
if (scope) packageName = `@${scope}/${packageName}`; if (scope) packageName = `@${scope}/${packageName}`;
const filePath = this.loadNodesAndCredentials.resolveIcon(packageName, req.originalUrl); const filePath = this.loadNodesAndCredentials.resolveIcon(packageName, req.originalUrl);

View file

@ -14,7 +14,7 @@ import { initErrorHandling } from '@/ErrorReporting';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import type { IExternalHooksClass, N8nInstanceType } from '@/Interfaces'; import type { N8nInstanceType } from '@/Interfaces';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { PostHogClient } from '@/posthog'; import { PostHogClient } from '@/posthog';
import { License } from '@/License'; import { License } from '@/License';
@ -27,7 +27,7 @@ import { ShutdownService } from '@/shutdown/Shutdown.service';
export abstract class BaseCommand extends Command { export abstract class BaseCommand extends Command {
protected logger = Container.get(Logger); protected logger = Container.get(Logger);
protected externalHooks?: IExternalHooksClass; protected externalHooks?: ExternalHooks;
protected nodeTypes: NodeTypes; protected nodeTypes: NodeTypes;

View file

@ -73,4 +73,3 @@ setGlobalState({
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default config; export default config;
export type Config = typeof config;

View file

@ -1,9 +1,7 @@
import { Service } from 'typedi';
import { Authorized, Get, RestController } from '@/decorators'; import { Authorized, Get, RestController } from '@/decorators';
import { WorkflowRequest } from '@/requests'; import { WorkflowRequest } from '@/requests';
import { ActiveWorkflowsService } from '@/services/activeWorkflows.service'; import { ActiveWorkflowsService } from '@/services/activeWorkflows.service';
@Service()
@Authorized() @Authorized()
@RestController('/active-workflows') @RestController('/active-workflows')
export class ActiveWorkflowsController { export class ActiveWorkflowsController {

View file

@ -1,6 +1,5 @@
import validator from 'validator'; import validator from 'validator';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { Service } from 'typedi';
import { Authorized, Get, Post, RestController } from '@/decorators'; import { Authorized, Get, Post, RestController } from '@/decorators';
import { issueCookie, resolveJwt } from '@/auth/jwt'; import { issueCookie, resolveJwt } from '@/auth/jwt';
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from '@/constants'; import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from '@/constants';
@ -27,7 +26,6 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { ApplicationError } from 'n8n-workflow'; import { ApplicationError } from 'n8n-workflow';
@Service()
@RestController() @RestController()
export class AuthController { export class AuthController {
constructor( constructor(

View file

@ -1,11 +1,9 @@
import { Service } from 'typedi';
import express from 'express'; import express from 'express';
import { BinaryDataService, FileNotFoundError, isValidNonDefaultMode } from 'n8n-core'; import { BinaryDataService, FileNotFoundError, isValidNonDefaultMode } from 'n8n-core';
import { Get, RestController } from '@/decorators'; import { Get, RestController } from '@/decorators';
import { BinaryDataRequest } from '@/requests'; import { BinaryDataRequest } from '@/requests';
@RestController('/binary-data') @RestController('/binary-data')
@Service()
export class BinaryDataController { export class BinaryDataController {
constructor(private readonly binaryDataService: BinaryDataService) {} constructor(private readonly binaryDataService: BinaryDataService) {}

View file

@ -1,4 +1,3 @@
import { Service } from 'typedi';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import config from '@/config'; import config from '@/config';
import { import {
@ -42,14 +41,13 @@ export function isNpmError(error: unknown): error is { code: number; stdout: str
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error; return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
} }
@Service()
@Authorized() @Authorized()
@RestController('/community-packages') @RestController('/community-packages')
export class CommunityPackagesController { export class CommunityPackagesController {
constructor( constructor(
private push: Push, private readonly push: Push,
private internalHooks: InternalHooks, private readonly internalHooks: InternalHooks,
private communityPackagesService: CommunityPackagesService, private readonly communityPackagesService: CommunityPackagesService,
) {} ) {}
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')` // TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`

View file

@ -1,4 +1,3 @@
import { Service } from 'typedi';
import { Get, RestController } from '@/decorators'; import { Get, RestController } from '@/decorators';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
@ -6,7 +5,6 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository
import { In } from 'typeorm'; import { In } from 'typeorm';
@RestController('/debug') @RestController('/debug')
@Service()
export class DebugController { export class DebugController {
constructor( constructor(
private readonly multiMainSetup: MultiMainSetup, private readonly multiMainSetup: MultiMainSetup,

View file

@ -1,4 +1,3 @@
import { Service } from 'typedi';
import type { RequestHandler } from 'express'; import type { RequestHandler } from 'express';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import type { import type {
@ -22,7 +21,6 @@ const assertMethodName: RequestHandler = (req, res, next) => {
next(); next();
}; };
@Service()
@Authorized() @Authorized()
@RestController('/dynamic-node-parameters') @RestController('/dynamic-node-parameters')
export class DynamicNodeParametersController { export class DynamicNodeParametersController {

View file

@ -1,5 +1,4 @@
import { Request } from 'express'; import { Request } from 'express';
import { Container, Service } from 'typedi';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import config from '@/config'; import config from '@/config';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
@ -64,7 +63,6 @@ type PushRequest = Request<
} }
>; >;
@Service()
@NoAuthRequired() @NoAuthRequired()
@RestController('/e2e') @RestController('/e2e')
export class E2EController { export class E2EController {
@ -89,12 +87,13 @@ export class E2EController {
constructor( constructor(
license: License, license: License,
private roleRepo: RoleRepository, private readonly roleRepo: RoleRepository,
private settingsRepo: SettingsRepository, private readonly settingsRepo: SettingsRepository,
private userRepo: UserRepository, private readonly userRepo: UserRepository,
private workflowRunner: ActiveWorkflowRunner, private readonly workflowRunner: ActiveWorkflowRunner,
private mfaService: MfaService, private readonly mfaService: MfaService,
private cacheService: CacheService, private readonly cacheService: CacheService,
private readonly push: Push,
private readonly passwordUtility: PasswordUtility, private readonly passwordUtility: PasswordUtility,
) { ) {
license.isFeatureEnabled = (feature: BooleanLicenseFeature) => license.isFeatureEnabled = (feature: BooleanLicenseFeature) =>
@ -112,14 +111,8 @@ export class E2EController {
} }
@Post('/push') @Post('/push')
async push(req: PushRequest) { async pushSend(req: PushRequest) {
const pushInstance = Container.get(Push); this.push.broadcast(req.body.type, req.body.data);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const sessionId = Object.keys(pushInstance.getBackend().connections as object)[0];
pushInstance.send(req.body.type, req.body.data, sessionId);
} }
@Patch('/feature') @Patch('/feature')

View file

@ -1,12 +1,11 @@
import { In } from 'typeorm'; import { In } from 'typeorm';
import Container, { Service } from 'typedi'; import { Response } from 'express';
import config from '@/config';
import { Authorized, NoAuthRequired, Post, RequireGlobalScope, RestController } from '@/decorators'; import { Authorized, NoAuthRequired, Post, RequireGlobalScope, RestController } from '@/decorators';
import { issueCookie } from '@/auth/jwt'; import { issueCookie } from '@/auth/jwt';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { Response } from 'express';
import { UserRequest } from '@/requests'; import { UserRequest } from '@/requests';
import { Config } from '@/config';
import { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces';
import { License } from '@/License'; import { License } from '@/License';
import { UserService } from '@/services/user.service'; import { UserService } from '@/services/user.service';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
@ -17,20 +16,20 @@ import type { User } from '@/databases/entities/User';
import validator from 'validator'; import validator from 'validator';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { InternalHooks } from '@/InternalHooks';
import { ExternalHooks } from '@/ExternalHooks';
@Service()
@Authorized() @Authorized()
@RestController('/invitations') @RestController('/invitations')
export class InvitationController { export class InvitationController {
constructor( constructor(
private readonly config: Config,
private readonly logger: Logger, private readonly logger: Logger,
private readonly internalHooks: IInternalHooksClass, private readonly internalHooks: InternalHooks,
private readonly externalHooks: IExternalHooksClass, private readonly externalHooks: ExternalHooks,
private readonly userService: UserService, private readonly userService: UserService,
private readonly license: License, private readonly license: License,
private readonly passwordUtility: PasswordUtility, private readonly passwordUtility: PasswordUtility,
private readonly postHog?: PostHogClient, private readonly postHog: PostHogClient,
) {} ) {}
/** /**
@ -40,7 +39,7 @@ export class InvitationController {
@Post('/') @Post('/')
@RequireGlobalScope('user:create') @RequireGlobalScope('user:create')
async inviteUser(req: UserRequest.Invite) { async inviteUser(req: UserRequest.Invite) {
const isWithinUsersLimit = Container.get(License).isWithinUsersLimit(); const isWithinUsersLimit = this.license.isWithinUsersLimit();
if (isSamlLicensedAndEnabled()) { if (isSamlLicensedAndEnabled()) {
this.logger.debug( this.logger.debug(
@ -58,7 +57,7 @@ export class InvitationController {
throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED); throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
} }
if (!this.config.getEnv('userManagement.isInstanceOwnerSetUp')) { if (!config.getEnv('userManagement.isInstanceOwnerSetUp')) {
this.logger.debug( this.logger.debug(
'Request to send email invite(s) to user(s) failed because the owner account is not set up', 'Request to send email invite(s) to user(s) failed because the owner account is not set up',
); );

View file

@ -1,8 +1,9 @@
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import { Authorized, Get, Post, Put, RestController, RequireGlobalScope } from '@/decorators'; import { Authorized, Get, Post, Put, RestController, RequireGlobalScope } from '@/decorators';
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers'; import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers';
import { LdapService } from '@/Ldap/LdapService.ee'; import { LdapManager } from '@/Ldap/LdapManager.ee';
import { LdapSync } from '@/Ldap/LdapSync.ee'; import type { LdapService } from '@/Ldap/LdapService.ee';
import type { LdapSync } from '@/Ldap/LdapSync.ee';
import { LdapConfiguration } from '@/Ldap/types'; import { LdapConfiguration } from '@/Ldap/types';
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants'; import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
@ -11,11 +12,15 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Authorized() @Authorized()
@RestController('/ldap') @RestController('/ldap')
export class LdapController { export class LdapController {
constructor( private ldapService: LdapService;
private ldapService: LdapService,
private ldapSync: LdapSync, private ldapSync: LdapSync;
private internalHooks: InternalHooks,
) {} constructor(private readonly internalHooks: InternalHooks) {
const { service, sync } = LdapManager.getInstance();
this.ldapService = service;
this.ldapSync = sync;
}
@Get('/config') @Get('/config')
@RequireGlobalScope('ldap:manage') @RequireGlobalScope('ldap:manage')

View file

@ -1,7 +1,6 @@
import validator from 'validator'; import validator from 'validator';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { Response } from 'express'; import { Response } from 'express';
import { Service } from 'typedi';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators'; import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators';
import { PasswordUtility } from '@/services/password.utility'; import { PasswordUtility } from '@/services/password.utility';
@ -22,7 +21,6 @@ import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Service()
@Authorized() @Authorized()
@RestController('/me') @RestController('/me')
export class MeController { export class MeController {

View file

@ -1,10 +1,8 @@
import { Service } from 'typedi';
import { Authorized, Delete, Get, Post, RestController } from '@/decorators'; import { Authorized, Delete, Get, Post, RestController } from '@/decorators';
import { AuthenticatedRequest, MFA } from '@/requests'; import { AuthenticatedRequest, MFA } from '@/requests';
import { MfaService } from '@/Mfa/mfa.service'; import { MfaService } from '@/Mfa/mfa.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Service()
@Authorized() @Authorized()
@RestController('/mfa') @RestController('/mfa')
export class MFAController { export class MFAController {

View file

@ -3,22 +3,19 @@ import get from 'lodash/get';
import { Request } from 'express'; import { Request } from 'express';
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow'; import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
import { Authorized, Post, RestController } from '@/decorators'; import { Authorized, Post, RestController } from '@/decorators';
import { Config } from '@/config'; import config from '@/config';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
@Authorized() @Authorized()
@RestController('/node-types') @RestController('/node-types')
export class NodeTypesController { export class NodeTypesController {
constructor( constructor(private readonly nodeTypes: NodeTypes) {}
private readonly config: Config,
private readonly nodeTypes: NodeTypes,
) {}
@Post('/') @Post('/')
async getNodeInfo(req: Request) { async getNodeInfo(req: Request) {
const nodeInfos = get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[]; const nodeInfos = get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[];
const defaultLocale = this.config.getEnv('defaultLocale'); const defaultLocale = config.getEnv('defaultLocale');
if (defaultLocale === 'en') { if (defaultLocale === 'en') {
return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => { return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => {

View file

@ -1,4 +1,3 @@
import { Service } from 'typedi';
import { Response } from 'express'; import { Response } from 'express';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import axios from 'axios'; import axios from 'axios';
@ -30,7 +29,6 @@ const algorithmMap = {
/* eslint-enable */ /* eslint-enable */
} as const; } as const;
@Service()
@Authorized() @Authorized()
@RestController('/oauth1-credential') @RestController('/oauth1-credential')
export class OAuth1CredentialController extends AbstractOAuthController { export class OAuth1CredentialController extends AbstractOAuthController {

View file

@ -1,4 +1,3 @@
import { Service } from 'typedi';
import type { ClientOAuth2Options } from '@n8n/client-oauth2'; import type { ClientOAuth2Options } from '@n8n/client-oauth2';
import { ClientOAuth2 } from '@n8n/client-oauth2'; import { ClientOAuth2 } from '@n8n/client-oauth2';
import Csrf from 'csrf'; import Csrf from 'csrf';
@ -31,7 +30,6 @@ interface CsrfStateParam {
token: string; token: string;
} }
@Service()
@Authorized() @Authorized()
@RestController('/oauth2-credential') @RestController('/oauth2-credential')
export class OAuth2CredentialController extends AbstractOAuthController { export class OAuth2CredentialController extends AbstractOAuthController {

View file

@ -1,12 +1,10 @@
import { Authorized, Post, RestController, RequireGlobalScope } from '@/decorators'; import { Authorized, Post, RestController, RequireGlobalScope } from '@/decorators';
import { OrchestrationRequest } from '@/requests'; import { OrchestrationRequest } from '@/requests';
import { Service } from 'typedi';
import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup'; import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup';
import { License } from '../License'; import { License } from '@/License';
@Authorized() @Authorized()
@RestController('/orchestration') @RestController('/orchestration')
@Service()
export class OrchestrationController { export class OrchestrationController {
constructor( constructor(
private readonly singleMainSetup: SingleMainSetup, private readonly singleMainSetup: SingleMainSetup,

View file

@ -1,29 +1,29 @@
import validator from 'validator'; import validator from 'validator';
import { Response } from 'express';
import config from '@/config';
import { validateEntity } from '@/GenericHelpers'; import { validateEntity } from '@/GenericHelpers';
import { Authorized, Post, RestController } from '@/decorators'; import { Authorized, Post, RestController } from '@/decorators';
import { PasswordUtility } from '@/services/password.utility'; import { PasswordUtility } from '@/services/password.utility';
import { issueCookie } from '@/auth/jwt'; import { issueCookie } from '@/auth/jwt';
import { Response } from 'express';
import { Config } from '@/config';
import { OwnerRequest } from '@/requests'; import { OwnerRequest } from '@/requests';
import { IInternalHooksClass } from '@/Interfaces';
import { SettingsRepository } from '@db/repositories/settings.repository'; import { SettingsRepository } from '@db/repositories/settings.repository';
import { PostHogClient } from '@/posthog'; import { PostHogClient } from '@/posthog';
import { UserService } from '@/services/user.service'; import { UserService } from '@/services/user.service';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalHooks } from '@/InternalHooks';
@Authorized(['global', 'owner']) @Authorized(['global', 'owner'])
@RestController('/owner') @RestController('/owner')
export class OwnerController { export class OwnerController {
constructor( constructor(
private readonly config: Config,
private readonly logger: Logger, private readonly logger: Logger,
private readonly internalHooks: IInternalHooksClass, private readonly internalHooks: InternalHooks,
private readonly settingsRepository: SettingsRepository, private readonly settingsRepository: SettingsRepository,
private readonly userService: UserService, private readonly userService: UserService,
private readonly passwordUtility: PasswordUtility, private readonly passwordUtility: PasswordUtility,
private readonly postHog?: PostHogClient, private readonly postHog: PostHogClient,
) {} ) {}
/** /**
@ -35,7 +35,7 @@ export class OwnerController {
const { email, firstName, lastName, password } = req.body; const { email, firstName, lastName, password } = req.body;
const { id: userId, globalRole } = req.user; const { id: userId, globalRole } = req.user;
if (this.config.getEnv('userManagement.isInstanceOwnerSetUp')) { if (config.getEnv('userManagement.isInstanceOwnerSetUp')) {
this.logger.debug( this.logger.debug(
'Request to claim instance ownership failed because instance owner already exists', 'Request to claim instance ownership failed because instance owner already exists',
{ {
@ -94,7 +94,7 @@ export class OwnerController {
{ value: JSON.stringify(true) }, { value: JSON.stringify(true) },
); );
this.config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.isInstanceOwnerSetUp', true);
this.logger.debug('Setting isInstanceOwnerSetUp updated successfully', { userId }); this.logger.debug('Setting isInstanceOwnerSetUp updated successfully', { userId });

View file

@ -1,6 +1,5 @@
import { Response } from 'express'; import { Response } from 'express';
import { rateLimit } from 'express-rate-limit'; import { rateLimit } from 'express-rate-limit';
import { Service } from 'typedi';
import { IsNull, Not } from 'typeorm'; import { IsNull, Not } from 'typeorm';
import validator from 'validator'; import validator from 'validator';
@ -31,7 +30,6 @@ const throttle = rateLimit({
message: { message: 'Too many requests' }, message: { message: 'Too many requests' },
}); });
@Service()
@RestController() @RestController()
export class PasswordResetController { export class PasswordResetController {
constructor( constructor(

View file

@ -1,9 +1,7 @@
import { License } from '@/License'; import { License } from '@/License';
import { Get, RestController } from '@/decorators'; import { Get, RestController } from '@/decorators';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
import { Service } from 'typedi';
@Service()
@RestController('/roles') @RestController('/roles')
export class RoleController { export class RoleController {
constructor( constructor(

View file

@ -12,16 +12,14 @@ import {
} from '@/decorators'; } from '@/decorators';
import { TagService } from '@/services/tag.service'; import { TagService } from '@/services/tag.service';
import { TagsRequest } from '@/requests'; import { TagsRequest } from '@/requests';
import { Service } from 'typedi';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Authorized() @Authorized()
@RestController('/tags') @RestController('/tags')
@Service()
export class TagsController { export class TagsController {
private config = config; private config = config;
constructor(private tagService: TagService) {} constructor(private readonly tagService: TagService) {}
// TODO: move this into a new decorator `@IfEnabled('workflowTagsDisabled')` // TODO: move this into a new decorator `@IfEnabled('workflowTagsDisabled')`
@Middleware() @Middleware()

View file

@ -1,12 +1,12 @@
import type { Request } from 'express'; import type { Request } from 'express';
import { ICredentialTypes } from 'n8n-workflow';
import { join } from 'path'; import { join } from 'path';
import { access } from 'fs/promises'; import { access } from 'fs/promises';
import { Authorized, Get, RestController } from '@/decorators'; import { Authorized, Get, RestController } from '@/decorators';
import { Config } from '@/config'; import config from '@/config';
import { NODES_BASE_DIR } from '@/constants'; import { NODES_BASE_DIR } from '@/constants';
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 { CredentialTypes } from '@/CredentialTypes';
export const CREDENTIAL_TRANSLATIONS_DIR = 'n8n-nodes-base/dist/credentials/translations'; export const CREDENTIAL_TRANSLATIONS_DIR = 'n8n-nodes-base/dist/credentials/translations';
export const NODE_HEADERS_PATH = join(NODES_BASE_DIR, 'dist/nodes/headers'); export const NODE_HEADERS_PATH = join(NODES_BASE_DIR, 'dist/nodes/headers');
@ -18,10 +18,7 @@ export declare namespace TranslationRequest {
@Authorized() @Authorized()
@RestController('/') @RestController('/')
export class TranslationController { export class TranslationController {
constructor( constructor(private readonly credentialTypes: CredentialTypes) {}
private config: Config,
private credentialTypes: ICredentialTypes,
) {}
@Get('/credential-translation') @Get('/credential-translation')
async getCredentialTranslation(req: TranslationRequest.Credential) { async getCredentialTranslation(req: TranslationRequest.Credential) {
@ -30,7 +27,7 @@ export class TranslationController {
if (!this.credentialTypes.recognizes(credentialType)) if (!this.credentialTypes.recognizes(credentialType))
throw new BadRequestError(`Invalid Credential type: "${credentialType}"`); throw new BadRequestError(`Invalid Credential type: "${credentialType}"`);
const defaultLocale = this.config.getEnv('defaultLocale'); const defaultLocale = config.getEnv('defaultLocale');
const translationPath = join( const translationPath = join(
CREDENTIAL_TRANSLATIONS_DIR, CREDENTIAL_TRANSLATIONS_DIR,
defaultLocale, defaultLocale,

View file

@ -6,7 +6,6 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { RequireGlobalScope, Authorized, Delete, Get, RestController, Patch } from '@/decorators'; import { RequireGlobalScope, Authorized, Delete, Get, RestController, Patch } from '@/decorators';
import { ListQuery, UserRequest, UserSettingsUpdatePayload } from '@/requests'; import { ListQuery, UserRequest, UserSettingsUpdatePayload } from '@/requests';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces';
import type { PublicUser, ITelemetryUserDeletionData } from '@/Interfaces'; import type { PublicUser, ITelemetryUserDeletionData } from '@/Interfaces';
import { AuthIdentity } from '@db/entities/AuthIdentity'; import { AuthIdentity } from '@db/entities/AuthIdentity';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
@ -20,14 +19,16 @@ import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { License } from '@/License'; import { License } from '@/License';
import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooks } from '@/InternalHooks';
@Authorized() @Authorized()
@RestController('/users') @RestController('/users')
export class UsersController { export class UsersController {
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,
private readonly externalHooks: IExternalHooksClass, private readonly externalHooks: ExternalHooks,
private readonly internalHooks: IInternalHooksClass, private readonly internalHooks: InternalHooks,
private readonly sharedCredentialsRepository: SharedCredentialsRepository, private readonly sharedCredentialsRepository: SharedCredentialsRepository,
private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly activeWorkflowRunner: ActiveWorkflowRunner, private readonly activeWorkflowRunner: ActiveWorkflowRunner,

View file

@ -1,4 +1,3 @@
import { Service } from 'typedi';
import { Response, NextFunction } from 'express'; import { Response, NextFunction } from 'express';
import { Get, Middleware, RestController } from '@/decorators'; import { Get, Middleware, RestController } from '@/decorators';
import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics'; import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics';
@ -18,12 +17,11 @@ interface WorkflowStatisticsData<T> {
manualError: T; manualError: T;
} }
@Service()
@RestController('/workflow-stats') @RestController('/workflow-stats')
export class WorkflowStatisticsController { export class WorkflowStatisticsController {
constructor( constructor(
private sharedWorkflowRepository: SharedWorkflowRepository, private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private workflowStatisticsRepository: WorkflowStatisticsRepository, private readonly workflowStatisticsRepository: WorkflowStatisticsRepository,
private readonly logger: Logger, private readonly logger: Logger,
) {} ) {}

View file

@ -7,6 +7,7 @@ import {
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import config from '@/config'; import config from '@/config';
import type { Class } from 'n8n-core';
import { generateNanoId } from '../utils/generators'; import { generateNanoId } from '../utils/generators';
const dbType = config.getEnv('database.type'); const dbType = config.getEnv('database.type');
@ -27,9 +28,7 @@ const tsColumnOptions: ColumnOptions = {
type: datetimeColumnType, type: datetimeColumnType,
}; };
type Constructor<T> = new (...args: any[]) => T; function mixinStringId<T extends Class<{}, any[]>>(base: T) {
function mixinStringId<T extends Constructor<{}>>(base: T) {
class Derived extends base { class Derived extends base {
@PrimaryColumn('varchar') @PrimaryColumn('varchar')
id: string; id: string;
@ -44,7 +43,7 @@ function mixinStringId<T extends Constructor<{}>>(base: T) {
return Derived; return Derived;
} }
function mixinTimestamps<T extends Constructor<{}>>(base: T) { function mixinTimestamps<T extends Class<{}, any[]>>(base: T) {
class Derived extends base { class Derived extends base {
@CreateDateColumn(tsColumnOptions) @CreateDateColumn(tsColumnOptions)
createdAt: Date; createdAt: Date;

View file

@ -24,6 +24,7 @@ import { type ServiceClass, ShutdownService } from '@/shutdown/Shutdown.service'
export const OnShutdown = export const OnShutdown =
(priority = 100): MethodDecorator => (priority = 100): MethodDecorator =>
(prototype, propertyKey, descriptor) => { (prototype, propertyKey, descriptor) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const serviceClass = prototype.constructor as ServiceClass; const serviceClass = prototype.constructor as ServiceClass;
const methodName = String(propertyKey); const methodName = String(propertyKey);
// TODO: assert that serviceClass is decorated with @Service // TODO: assert that serviceClass is decorated with @Service

View file

@ -1,7 +1,10 @@
import { Service } from 'typedi';
import { CONTROLLER_BASE_PATH } from './constants'; import { CONTROLLER_BASE_PATH } from './constants';
export const RestController = export const RestController =
(basePath: `/${string}` = '/'): ClassDecorator => (basePath: `/${string}` = '/'): ClassDecorator =>
(target: object) => { (target: object) => {
Reflect.defineMetadata(CONTROLLER_BASE_PATH, basePath, target); Reflect.defineMetadata(CONTROLLER_BASE_PATH, basePath, target);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Service()(target);
}; };

View file

@ -1,6 +1,9 @@
import { Container } from 'typedi';
import { Router } from 'express'; import { Router } from 'express';
import type { Application, Request, Response, RequestHandler } from 'express'; import type { Application, Request, Response, RequestHandler } from 'express';
import type { Config } from '@/config'; import type { Class } from 'n8n-core';
import config from '@/config';
import type { AuthenticatedRequest } from '@/requests'; import type { AuthenticatedRequest } from '@/requests';
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
import { import {
@ -21,7 +24,7 @@ import type {
ScopeMetadata, ScopeMetadata,
} from './types'; } from './types';
import type { BooleanLicenseFeature } from '@/Interfaces'; import type { BooleanLicenseFeature } from '@/Interfaces';
import Container from 'typedi';
import { License } from '@/License'; import { License } from '@/License';
import type { Scope } from '@n8n/permissions'; import type { Scope } from '@n8n/permissions';
import { ApplicationError } from 'n8n-workflow'; import { ApplicationError } from 'n8n-workflow';
@ -81,9 +84,8 @@ const authFreeRoutes: string[] = [];
export const canSkipAuth = (method: string, path: string): boolean => export const canSkipAuth = (method: string, path: string): boolean =>
authFreeRoutes.includes(`${method.toLowerCase()} ${path}`); authFreeRoutes.includes(`${method.toLowerCase()} ${path}`);
export const registerController = (app: Application, config: Config, cObj: object) => { export const registerController = (app: Application, controllerClass: Class<object>) => {
const controller = cObj as Controller; const controller = Container.get(controllerClass as Class<Controller>);
const controllerClass = controller.constructor;
const controllerBasePath = Reflect.getMetadata(CONTROLLER_BASE_PATH, controllerClass) as const controllerBasePath = Reflect.getMetadata(CONTROLLER_BASE_PATH, controllerClass) as
| string | string
| undefined; | undefined;

View file

@ -10,7 +10,6 @@ export const SOURCE_CONTROL_SSH_FOLDER = 'ssh';
export const SOURCE_CONTROL_SSH_KEY_NAME = 'key'; export const SOURCE_CONTROL_SSH_KEY_NAME = 'key';
export const SOURCE_CONTROL_DEFAULT_BRANCH = 'main'; export const SOURCE_CONTROL_DEFAULT_BRANCH = 'main';
export const SOURCE_CONTROL_ORIGIN = 'origin'; export const SOURCE_CONTROL_ORIGIN = 'origin';
export const SOURCE_CONTROL_API_ROOT = 'source-control';
export const SOURCE_CONTROL_README = ` export const SOURCE_CONTROL_README = `
# n8n Source Control # n8n Source Control
`; `;

View file

@ -1,4 +1,3 @@
import { Container, Service } from 'typedi';
import type { PullResult } from 'simple-git'; import type { PullResult } from 'simple-git';
import express from 'express'; import express from 'express';
import { Authorized, Get, Post, Patch, RestController, RequireGlobalScope } from '@/decorators'; import { Authorized, Get, Post, Patch, RestController, RequireGlobalScope } from '@/decorators';
@ -11,20 +10,20 @@ import { SourceControlRequest } from './types/requests';
import { SourceControlPreferencesService } from './sourceControlPreferences.service.ee'; import { SourceControlPreferencesService } from './sourceControlPreferences.service.ee';
import type { SourceControlPreferences } from './types/sourceControlPreferences'; import type { SourceControlPreferences } from './types/sourceControlPreferences';
import type { SourceControlledFile } from './types/sourceControlledFile'; import type { SourceControlledFile } from './types/sourceControlledFile';
import { SOURCE_CONTROL_API_ROOT, SOURCE_CONTROL_DEFAULT_BRANCH } from './constants'; import { SOURCE_CONTROL_DEFAULT_BRANCH } from './constants';
import type { ImportResult } from './types/importResult'; import type { ImportResult } from './types/importResult';
import { InternalHooks } from '../../InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { getRepoType } from './sourceControlHelper.ee'; import { getRepoType } from './sourceControlHelper.ee';
import { SourceControlGetStatus } from './types/sourceControlGetStatus'; import { SourceControlGetStatus } from './types/sourceControlGetStatus';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Service()
@Authorized() @Authorized()
@RestController(`/${SOURCE_CONTROL_API_ROOT}`) @RestController('/source-control')
export class SourceControlController { export class SourceControlController {
constructor( constructor(
private sourceControlService: SourceControlService, private readonly sourceControlService: SourceControlService,
private sourceControlPreferencesService: SourceControlPreferencesService, private readonly sourceControlPreferencesService: SourceControlPreferencesService,
private readonly internalHooks: InternalHooks,
) {} ) {}
@Authorized('none') @Authorized('none')
@ -85,7 +84,7 @@ export class SourceControlController {
const resultingPreferences = this.sourceControlPreferencesService.getPreferences(); const resultingPreferences = this.sourceControlPreferencesService.getPreferences();
// #region Tracking Information // #region Tracking Information
// located in controller so as to not call this multiple times when updating preferences // located in controller so as to not call this multiple times when updating preferences
void Container.get(InternalHooks).onSourceControlSettingsUpdated({ void this.internalHooks.onSourceControlSettingsUpdated({
branch_name: resultingPreferences.branchName, branch_name: resultingPreferences.branchName,
connected: resultingPreferences.connected, connected: resultingPreferences.connected,
read_only_instance: resultingPreferences.branchReadOnly, read_only_instance: resultingPreferences.branchReadOnly,
@ -130,7 +129,7 @@ export class SourceControlController {
} }
await this.sourceControlService.init(); await this.sourceControlService.init();
const resultingPreferences = this.sourceControlPreferencesService.getPreferences(); const resultingPreferences = this.sourceControlPreferencesService.getPreferences();
void Container.get(InternalHooks).onSourceControlSettingsUpdated({ void this.internalHooks.onSourceControlSettingsUpdated({
branch_name: resultingPreferences.branchName, branch_name: resultingPreferences.branchName,
connected: resultingPreferences.connected, connected: resultingPreferences.connected,
read_only_instance: resultingPreferences.branchReadOnly, read_only_instance: resultingPreferences.branchReadOnly,

View file

@ -1,5 +1,3 @@
import { Service } from 'typedi';
import { VariablesRequest } from '@/requests'; import { VariablesRequest } from '@/requests';
import { import {
Authorized, Authorized,
@ -17,11 +15,10 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { VariableValidationError } from '@/errors/variable-validation.error'; import { VariableValidationError } from '@/errors/variable-validation.error';
import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error'; import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error';
@Service()
@Authorized() @Authorized()
@RestController('/variables') @RestController('/variables')
export class VariablesController { export class VariablesController {
constructor(private variablesService: VariablesService) {} constructor(private readonly variablesService: VariablesService) {}
@Get('/') @Get('/')
@RequireGlobalScope('variable:list') @RequireGlobalScope('variable:list')

View file

@ -1,9 +1,7 @@
import { Service } from 'typedi';
import { Authorized, Get, Post, RequireGlobalScope, RestController } from '@/decorators'; import { Authorized, Get, Post, RequireGlobalScope, RestController } from '@/decorators';
import { LicenseRequest } from '@/requests'; import { LicenseRequest } from '@/requests';
import { LicenseService } from './license.service'; import { LicenseService } from './license.service';
@Service()
@Authorized() @Authorized()
@RestController('/license') @RestController('/license')
export class LicenseController { export class LicenseController {

View file

@ -30,6 +30,7 @@ import { UserManagementMailer } from '@/UserManagement/email';
import type { CommunityPackagesService } from '@/services/communityPackages.service'; import type { CommunityPackagesService } from '@/services/communityPackages.service';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { UrlService } from './url.service'; import { UrlService } from './url.service';
import { InternalHooks } from '@/InternalHooks';
@Service() @Service()
export class FrontendService { export class FrontendService {
@ -46,6 +47,7 @@ export class FrontendService {
private readonly mailer: UserManagementMailer, private readonly mailer: UserManagementMailer,
private readonly instanceSettings: InstanceSettings, private readonly instanceSettings: InstanceSettings,
private readonly urlService: UrlService, private readonly urlService: UrlService,
private readonly internalHooks: InternalHooks,
) { ) {
loadNodesAndCredentials.addPostProcessor(async () => this.generateTypes()); loadNodesAndCredentials.addPostProcessor(async () => this.generateTypes());
void this.generateTypes(); void this.generateTypes();
@ -218,7 +220,9 @@ export class FrontendService {
this.writeStaticJSON('credentials', credentials); this.writeStaticJSON('credentials', credentials);
} }
getSettings(): IN8nUISettings { getSettings(sessionId?: string): IN8nUISettings {
void this.internalHooks.onFrontendSettingsAPI(sessionId);
const restEndpoint = config.getEnv('endpoints.rest'); const restEndpoint = config.getEnv('endpoints.rest');
// Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel` // Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel`

View file

@ -1,10 +1,10 @@
import { Container, Service } from 'typedi'; import { Container, Service } from 'typedi';
import { ApplicationError, ErrorReporterProxy, assert } from 'n8n-workflow'; import { ApplicationError, ErrorReporterProxy, assert } from 'n8n-workflow';
import type { Class } from 'n8n-core';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
export interface ServiceClass { type HandlerFn = () => Promise<void> | void;
new (): Record<string, () => Promise<void> | void>; export type ServiceClass = Class<Record<string, HandlerFn>>;
}
export interface ShutdownHandler { export interface ShutdownHandler {
serviceClass: ServiceClass; serviceClass: ServiceClass;

View file

@ -1,5 +1,4 @@
import express from 'express'; import express from 'express';
import { Container, Service } from 'typedi';
import { import {
Authorized, Authorized,
Get, Get,
@ -36,13 +35,13 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { AuthError } from '@/errors/response-errors/auth.error'; import { AuthError } from '@/errors/response-errors/auth.error';
import { UrlService } from '@/services/url.service'; import { UrlService } from '@/services/url.service';
@Service()
@Authorized() @Authorized()
@RestController('/sso/saml') @RestController('/sso/saml')
export class SamlController { export class SamlController {
constructor( constructor(
private readonly samlService: SamlService, private readonly samlService: SamlService,
private readonly urlService: UrlService, private readonly urlService: UrlService,
private readonly internalHooks: InternalHooks,
) {} ) {}
@NoAuthRequired() @NoAuthRequired()
@ -142,7 +141,7 @@ export class SamlController {
} }
} }
if (loginResult.authenticatedUser) { if (loginResult.authenticatedUser) {
void Container.get(InternalHooks).onUserLoginSuccess({ void this.internalHooks.onUserLoginSuccess({
user: loginResult.authenticatedUser, user: loginResult.authenticatedUser,
authenticationMethod: 'saml', authenticationMethod: 'saml',
}); });
@ -159,7 +158,7 @@ export class SamlController {
return res.status(202).send(loginResult.attributes); return res.status(202).send(loginResult.attributes);
} }
} }
void Container.get(InternalHooks).onUserLoginFailed({ void this.internalHooks.onUserLoginFailed({
user: loginResult.attributes.email ?? 'unknown', user: loginResult.attributes.email ?? 'unknown',
authenticationMethod: 'saml', authenticationMethod: 'saml',
}); });
@ -168,7 +167,7 @@ export class SamlController {
if (isConnectionTestRequest(req)) { if (isConnectionTestRequest(req)) {
return res.send(getSamlConnectionTestFailedView((error as Error).message)); return res.send(getSamlConnectionTestFailedView((error as Error).message));
} }
void Container.get(InternalHooks).onUserLoginFailed({ void this.internalHooks.onUserLoginFailed({
user: 'unknown', user: 'unknown',
authenticationMethod: 'saml', authenticationMethod: 'saml',
}); });

View file

@ -1,6 +1,5 @@
import { Authorized, RestController, Get, Middleware } from '@/decorators'; import { Authorized, RestController, Get, Middleware } from '@/decorators';
import { WorkflowHistoryRequest } from '@/requests'; import { WorkflowHistoryRequest } from '@/requests';
import { Service } from 'typedi';
import { WorkflowHistoryService } from './workflowHistory.service.ee'; import { WorkflowHistoryService } from './workflowHistory.service.ee';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { isWorkflowHistoryEnabled, isWorkflowHistoryLicensed } from './workflowHistoryHelper.ee'; import { isWorkflowHistoryEnabled, isWorkflowHistoryLicensed } from './workflowHistoryHelper.ee';
@ -12,7 +11,6 @@ import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-v
const DEFAULT_TAKE = 20; const DEFAULT_TAKE = 20;
@Service()
@Authorized() @Authorized()
@RestController('/workflow-history') @RestController('/workflow-history')
export class WorkflowHistoryController { export class WorkflowHistoryController {

View file

@ -1,22 +1,18 @@
import { Container } from 'typedi';
import type { SuperAgentTest } from 'supertest'; import type { SuperAgentTest } from 'supertest';
import { SOURCE_CONTROL_API_ROOT } from '@/environments/sourceControl/constants';
import * as utils from '../shared/utils/';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
import Container from 'typedi';
import config from '@/config'; import config from '@/config';
import { SourceControlPreferencesService } from '@/environments/sourceControl/sourceControlPreferences.service.ee'; import { SourceControlPreferencesService } from '@/environments/sourceControl/sourceControlPreferences.service.ee';
import { SourceControlService } from '@/environments/sourceControl/sourceControl.service.ee'; import { SourceControlService } from '@/environments/sourceControl/sourceControl.service.ee';
import type { SourceControlledFile } from '@/environments/sourceControl/types/sourceControlledFile'; import type { SourceControlledFile } from '@/environments/sourceControl/types/sourceControlledFile';
import { getGlobalMemberRole, getGlobalOwnerRole } from '../shared/db/roles';
import * as utils from '../shared/utils/';
import { getGlobalOwnerRole } from '../shared/db/roles';
import { createUser } from '../shared/db/users'; import { createUser } from '../shared/db/users';
let authOwnerAgent: SuperAgentTest; let authOwnerAgent: SuperAgentTest;
let authMemberAgent: SuperAgentTest;
let owner: User; let owner: User;
let member: User;
const sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true);
const testServer = utils.setupTestServer({ const testServer = utils.setupTestServer({
endpointGroups: ['sourceControl', 'license', 'auth'], endpointGroups: ['sourceControl', 'license', 'auth'],
@ -25,11 +21,8 @@ const testServer = utils.setupTestServer({
beforeAll(async () => { beforeAll(async () => {
const globalOwnerRole = await getGlobalOwnerRole(); const globalOwnerRole = await getGlobalOwnerRole();
const globalMemberRole = await getGlobalMemberRole();
owner = await createUser({ globalRole: globalOwnerRole }); owner = await createUser({ globalRole: globalOwnerRole });
member = await createUser({ globalRole: globalMemberRole });
authOwnerAgent = testServer.authAgentFor(owner); authOwnerAgent = testServer.authAgentFor(owner);
authMemberAgent = testServer.authAgentFor(member);
Container.get(SourceControlPreferencesService).isSourceControlConnected = () => true; Container.get(SourceControlPreferencesService).isSourceControlConnected = () => true;
}); });
@ -37,7 +30,7 @@ beforeAll(async () => {
describe('GET /sourceControl/preferences', () => { describe('GET /sourceControl/preferences', () => {
test('should return Source Control preferences', async () => { test('should return Source Control preferences', async () => {
await authOwnerAgent await authOwnerAgent
.get(`/${SOURCE_CONTROL_API_ROOT}/preferences`) .get('/source-control/preferences')
.expect(200) .expect(200)
.expect((res) => { .expect((res) => {
return 'repositoryUrl' in res.body && 'branchName' in res.body; return 'repositoryUrl' in res.body && 'branchName' in res.body;
@ -60,7 +53,7 @@ describe('GET /sourceControl/preferences', () => {
] as SourceControlledFile[]; ] as SourceControlledFile[];
}; };
await authOwnerAgent await authOwnerAgent
.get(`/${SOURCE_CONTROL_API_ROOT}/get-status`) .get('/source-control/get-status')
.query({ direction: 'push', preferLocalVersion: 'true', verbose: 'false' }) .query({ direction: 'push', preferLocalVersion: 'true', verbose: 'false' })
.expect(200) .expect(200)
.expect((res) => { .expect((res) => {
@ -73,7 +66,7 @@ describe('GET /sourceControl/preferences', () => {
test('refreshing key pairsshould return new rsa key', async () => { test('refreshing key pairsshould return new rsa key', async () => {
config.set('sourceControl.defaultKeyPairType', 'rsa'); config.set('sourceControl.defaultKeyPairType', 'rsa');
await authOwnerAgent await authOwnerAgent
.post(`/${SOURCE_CONTROL_API_ROOT}/generate-key-pair`) .post('/source-control/generate-key-pair')
.send() .send()
.expect(200) .expect(200)
.expect((res) => { .expect((res) => {

View file

@ -1,6 +1,7 @@
import type { DataSourceOptions as ConnectionOptions, Repository } from 'typeorm'; import type { DataSourceOptions as ConnectionOptions, Repository } from 'typeorm';
import { DataSource as Connection } from 'typeorm'; import { DataSource as Connection } from 'typeorm';
import { Container } from 'typedi'; import { Container } from 'typedi';
import type { Class } from 'n8n-core';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
@ -116,7 +117,7 @@ const repositories = [
*/ */
export async function truncate(names: Array<(typeof repositories)[number]>) { export async function truncate(names: Array<(typeof repositories)[number]>) {
for (const name of names) { for (const name of names) {
const RepositoryClass: { new (): Repository<any> } = ( const RepositoryClass: Class<Repository<object>> = (
await import(`@db/repositories/${name.charAt(0).toLowerCase() + name.slice(1)}.repository`) await import(`@db/repositories/${name.charAt(0).toLowerCase() + name.slice(1)}.repository`)
)[`${name}Repository`]; )[`${name}Repository`];
await Container.get(RepositoryClass).delete({}); await Container.get(RepositoryClass).delete({});

View file

@ -14,14 +14,13 @@ import { rawBodyReader, bodyParser, setupAuthMiddlewares } from '@/middlewares';
import { PostHogClient } from '@/posthog'; import { PostHogClient } from '@/posthog';
import { License } from '@/License'; import { License } from '@/License';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { InternalHooks } from '@/InternalHooks';
import { mockInstance } from '../../../shared/mocking'; import { mockInstance } from '../../../shared/mocking';
import * as testDb from '../../shared/testDb'; import * as testDb from '../../shared/testDb';
import { AUTHLESS_ENDPOINTS, PUBLIC_API_REST_PATH_SEGMENT, REST_PATH_SEGMENT } from '../constants'; import { AUTHLESS_ENDPOINTS, PUBLIC_API_REST_PATH_SEGMENT, REST_PATH_SEGMENT } from '../constants';
import type { SetupProps, TestServer } from '../types'; import type { SetupProps, TestServer } from '../types';
import { InternalHooks } from '@/InternalHooks';
import { LicenseMocker } from '../license'; import { LicenseMocker } from '../license';
import { PasswordUtility } from '@/services/password.utility';
/** /**
* Plugin to prefix a path segment into a request URL pathname. * Plugin to prefix a path segment into a request URL pathname.
@ -76,7 +75,7 @@ export const setupTestServer = ({
app.use(cookieParser()); app.use(cookieParser());
// Mock all telemetry and logging // Mock all telemetry and logging
const logger = mockInstance(Logger); mockInstance(Logger);
mockInstance(InternalHooks); mockInstance(InternalHooks);
mockInstance(PostHogClient); mockInstance(PostHogClient);
@ -140,12 +139,12 @@ export const setupTestServer = ({
const { VariablesController } = await import( const { VariablesController } = await import(
'@/environments/variables/variables.controller.ee' '@/environments/variables/variables.controller.ee'
); );
registerController(app, config, Container.get(VariablesController)); registerController(app, VariablesController);
break; break;
case 'license': case 'license':
const { LicenseController } = await import('@/license/license.controller'); const { LicenseController } = await import('@/license/license.controller');
registerController(app, config, Container.get(LicenseController)); registerController(app, LicenseController);
break; break;
case 'metrics': case 'metrics':
@ -156,166 +155,108 @@ export const setupTestServer = ({
case 'eventBus': case 'eventBus':
const { EventBusController } = await import('@/eventbus/eventBus.controller'); const { EventBusController } = await import('@/eventbus/eventBus.controller');
const { EventBusControllerEE } = await import('@/eventbus/eventBus.controller.ee'); const { EventBusControllerEE } = await import('@/eventbus/eventBus.controller.ee');
registerController(app, config, new EventBusController()); registerController(app, EventBusController);
registerController(app, config, new EventBusControllerEE()); registerController(app, EventBusControllerEE);
break; break;
case 'auth': case 'auth':
const { AuthController } = await import('@/controllers/auth.controller'); const { AuthController } = await import('@/controllers/auth.controller');
registerController(app, config, Container.get(AuthController)); registerController(app, AuthController);
break; break;
case 'mfa': case 'mfa':
const { MFAController } = await import('@/controllers/mfa.controller'); const { MFAController } = await import('@/controllers/mfa.controller');
registerController(app, config, Container.get(MFAController)); registerController(app, MFAController);
break; break;
case 'ldap': case 'ldap':
const { LdapManager } = await import('@/Ldap/LdapManager.ee');
const { handleLdapInit } = await import('@/Ldap/helpers'); const { handleLdapInit } = await import('@/Ldap/helpers');
const { LdapController } = await import('@/controllers/ldap.controller'); const { LdapController } = await import('@/controllers/ldap.controller');
testServer.license.enable('feat:ldap'); testServer.license.enable('feat:ldap');
await handleLdapInit(); await handleLdapInit();
const { service, sync } = LdapManager.getInstance(); registerController(app, LdapController);
registerController(
app,
config,
new LdapController(service, sync, Container.get(InternalHooks)),
);
break; break;
case 'saml': case 'saml':
const { setSamlLoginEnabled } = await import('@/sso/saml/samlHelpers'); const { setSamlLoginEnabled } = await import('@/sso/saml/samlHelpers');
const { SamlController } = await import('@/sso/saml/routes/saml.controller.ee'); const { SamlController } = await import('@/sso/saml/routes/saml.controller.ee');
await setSamlLoginEnabled(true); await setSamlLoginEnabled(true);
registerController(app, config, Container.get(SamlController)); registerController(app, SamlController);
break; break;
case 'sourceControl': case 'sourceControl':
const { SourceControlController } = await import( const { SourceControlController } = await import(
'@/environments/sourceControl/sourceControl.controller.ee' '@/environments/sourceControl/sourceControl.controller.ee'
); );
registerController(app, config, Container.get(SourceControlController)); registerController(app, SourceControlController);
break; break;
case 'community-packages': case 'community-packages':
const { CommunityPackagesController } = await import( const { CommunityPackagesController } = await import(
'@/controllers/communityPackages.controller' '@/controllers/communityPackages.controller'
); );
registerController(app, config, Container.get(CommunityPackagesController)); registerController(app, CommunityPackagesController);
break; break;
case 'me': case 'me':
const { MeController } = await import('@/controllers/me.controller'); const { MeController } = await import('@/controllers/me.controller');
registerController(app, config, Container.get(MeController)); registerController(app, MeController);
break; break;
case 'passwordReset': case 'passwordReset':
const { PasswordResetController } = await import( const { PasswordResetController } = await import(
'@/controllers/passwordReset.controller' '@/controllers/passwordReset.controller'
); );
registerController(app, config, Container.get(PasswordResetController)); registerController(app, PasswordResetController);
break; break;
case 'owner': case 'owner':
const { UserService } = await import('@/services/user.service');
const { SettingsRepository } = await import('@db/repositories/settings.repository');
const { OwnerController } = await import('@/controllers/owner.controller'); const { OwnerController } = await import('@/controllers/owner.controller');
registerController( registerController(app, OwnerController);
app,
config,
new OwnerController(
config,
logger,
Container.get(InternalHooks),
Container.get(SettingsRepository),
Container.get(UserService),
Container.get(PasswordUtility),
),
);
break; break;
case 'users': case 'users':
const { SharedCredentialsRepository } = await import(
'@db/repositories/sharedCredentials.repository'
);
const { SharedWorkflowRepository } = await import(
'@db/repositories/sharedWorkflow.repository'
);
const { ActiveWorkflowRunner } = await import('@/ActiveWorkflowRunner');
const { UserService: US } = await import('@/services/user.service');
const { ExternalHooks: EH } = await import('@/ExternalHooks');
const { RoleService: RS } = await import('@/services/role.service');
const { UsersController } = await import('@/controllers/users.controller'); const { UsersController } = await import('@/controllers/users.controller');
registerController( registerController(app, UsersController);
app,
config,
new UsersController(
logger,
Container.get(EH),
Container.get(InternalHooks),
Container.get(SharedCredentialsRepository),
Container.get(SharedWorkflowRepository),
Container.get(ActiveWorkflowRunner),
Container.get(RS),
Container.get(US),
Container.get(License),
),
);
break; break;
case 'invitations': case 'invitations':
const { InvitationController } = await import('@/controllers/invitation.controller'); const { InvitationController } = await import('@/controllers/invitation.controller');
const { ExternalHooks: EHS } = await import('@/ExternalHooks'); registerController(app, InvitationController);
const { UserService: USE } = await import('@/services/user.service');
registerController(
app,
config,
new InvitationController(
config,
logger,
Container.get(InternalHooks),
Container.get(EHS),
Container.get(USE),
Container.get(License),
Container.get(PasswordUtility),
),
);
break; break;
case 'tags': case 'tags':
const { TagsController } = await import('@/controllers/tags.controller'); const { TagsController } = await import('@/controllers/tags.controller');
registerController(app, config, Container.get(TagsController)); registerController(app, TagsController);
break; break;
case 'externalSecrets': case 'externalSecrets':
const { ExternalSecretsController } = await import( const { ExternalSecretsController } = await import(
'@/ExternalSecrets/ExternalSecrets.controller.ee' '@/ExternalSecrets/ExternalSecrets.controller.ee'
); );
registerController(app, config, Container.get(ExternalSecretsController)); registerController(app, ExternalSecretsController);
break; break;
case 'workflowHistory': case 'workflowHistory':
const { WorkflowHistoryController } = await import( const { WorkflowHistoryController } = await import(
'@/workflows/workflowHistory/workflowHistory.controller.ee' '@/workflows/workflowHistory/workflowHistory.controller.ee'
); );
registerController(app, config, Container.get(WorkflowHistoryController)); registerController(app, WorkflowHistoryController);
break; break;
case 'binaryData': case 'binaryData':
const { BinaryDataController } = await import('@/controllers/binaryData.controller'); const { BinaryDataController } = await import('@/controllers/binaryData.controller');
registerController(app, config, Container.get(BinaryDataController)); registerController(app, BinaryDataController);
break; break;
case 'role': case 'role':
const { RoleController } = await import('@/controllers/role.controller'); const { RoleController } = await import('@/controllers/role.controller');
registerController(app, config, Container.get(RoleController)); registerController(app, RoleController);
break; break;
case 'debug': case 'debug':
const { DebugController } = await import('@/controllers/debug.controller'); const { DebugController } = await import('@/controllers/debug.controller');
registerController(app, config, Container.get(DebugController)); registerController(app, DebugController);
break; break;
} }
} }

View file

@ -1,12 +1,13 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { DeepPartial } from 'ts-essentials'; import type { DeepPartial } from 'ts-essentials';
import type { Class } from 'n8n-core';
export const mockInstance = <T>( export const mockInstance = <T>(
ctor: new (...args: unknown[]) => T, serviceClass: Class<T>,
data: DeepPartial<T> | undefined = undefined, data: DeepPartial<T> | undefined = undefined,
) => { ) => {
const instance = mock<T>(data); const instance = mock<T>(data);
Container.set(ctor, instance); Container.set(serviceClass, instance);
return instance; return instance;
}; };

View file

@ -1,10 +1,9 @@
import type { CookieOptions, Response } from 'express'; import type { CookieOptions, Response } from 'express';
import { anyObject, captor, mock } from 'jest-mock-extended'; import { anyObject, captor, mock } from 'jest-mock-extended';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import type { IInternalHooksClass } from '@/Interfaces';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { SettingsRepository } from '@db/repositories/settings.repository'; import type { SettingsRepository } from '@db/repositories/settings.repository';
import type { Config } from '@/config'; import config from '@/config';
import type { OwnerRequest } from '@/requests'; import type { OwnerRequest } from '@/requests';
import { OwnerController } from '@/controllers/owner.controller'; import { OwnerController } from '@/controllers/owner.controller';
import { AUTH_COOKIE_NAME } from '@/constants'; import { AUTH_COOKIE_NAME } from '@/constants';
@ -16,32 +15,33 @@ import { badPasswords } from '../shared/testData';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { PasswordUtility } from '@/services/password.utility'; import { PasswordUtility } from '@/services/password.utility';
import Container from 'typedi'; import Container from 'typedi';
import type { InternalHooks } from '@/InternalHooks';
describe('OwnerController', () => { describe('OwnerController', () => {
const config = mock<Config>(); const configGetSpy = jest.spyOn(config, 'getEnv');
const internalHooks = mock<IInternalHooksClass>(); const internalHooks = mock<InternalHooks>();
const userService = mockInstance(UserService); const userService = mockInstance(UserService);
const settingsRepository = mock<SettingsRepository>(); const settingsRepository = mock<SettingsRepository>();
mockInstance(License).isWithinUsersLimit.mockReturnValue(true); mockInstance(License).isWithinUsersLimit.mockReturnValue(true);
const controller = new OwnerController( const controller = new OwnerController(
config,
mock(), mock(),
internalHooks, internalHooks,
settingsRepository, settingsRepository,
userService, userService,
Container.get(PasswordUtility), Container.get(PasswordUtility),
mock(),
); );
describe('setupOwner', () => { describe('setupOwner', () => {
it('should throw a BadRequestError if the instance owner is already setup', async () => { it('should throw a BadRequestError if the instance owner is already setup', async () => {
config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(true); configGetSpy.mockReturnValue(true);
await expect(controller.setupOwner(mock(), mock())).rejects.toThrowError( await expect(controller.setupOwner(mock(), mock())).rejects.toThrowError(
new BadRequestError('Instance owner already setup'), new BadRequestError('Instance owner already setup'),
); );
}); });
it('should throw a BadRequestError if the email is invalid', async () => { it('should throw a BadRequestError if the email is invalid', async () => {
config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(false); configGetSpy.mockReturnValue(false);
const req = mock<OwnerRequest.Post>({ body: { email: 'invalid email' } }); const req = mock<OwnerRequest.Post>({ body: { email: 'invalid email' } });
await expect(controller.setupOwner(req, mock())).rejects.toThrowError( await expect(controller.setupOwner(req, mock())).rejects.toThrowError(
new BadRequestError('Invalid email address'), new BadRequestError('Invalid email address'),
@ -51,7 +51,7 @@ describe('OwnerController', () => {
describe('should throw if the password is invalid', () => { describe('should throw if the password is invalid', () => {
Object.entries(badPasswords).forEach(([password, errorMessage]) => { Object.entries(badPasswords).forEach(([password, errorMessage]) => {
it(password, async () => { it(password, async () => {
config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(false); configGetSpy.mockReturnValue(false);
const req = mock<OwnerRequest.Post>({ body: { email: 'valid@email.com', password } }); const req = mock<OwnerRequest.Post>({ body: { email: 'valid@email.com', password } });
await expect(controller.setupOwner(req, mock())).rejects.toThrowError( await expect(controller.setupOwner(req, mock())).rejects.toThrowError(
new BadRequestError(errorMessage), new BadRequestError(errorMessage),
@ -61,7 +61,7 @@ describe('OwnerController', () => {
}); });
it('should throw a BadRequestError if firstName & lastName are missing ', async () => { it('should throw a BadRequestError if firstName & lastName are missing ', async () => {
config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(false); configGetSpy.mockReturnValue(false);
const req = mock<OwnerRequest.Post>({ const req = mock<OwnerRequest.Post>({
body: { email: 'valid@email.com', password: 'NewPassword123', firstName: '', lastName: '' }, body: { email: 'valid@email.com', password: 'NewPassword123', firstName: '', lastName: '' },
}); });
@ -86,7 +86,7 @@ describe('OwnerController', () => {
user, user,
}); });
const res = mock<Response>(); const res = mock<Response>();
config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(false); configGetSpy.mockReturnValue(false);
userService.save.calledWith(anyObject()).mockResolvedValue(user); userService.save.calledWith(anyObject()).mockResolvedValue(user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');

View file

@ -1,6 +1,6 @@
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { ICredentialTypes } from 'n8n-workflow'; import type { ICredentialTypes } from 'n8n-workflow';
import type { Config } from '@/config'; import config from '@/config';
import type { TranslationRequest } from '@/controllers/translation.controller'; import type { TranslationRequest } from '@/controllers/translation.controller';
import { import {
TranslationController, TranslationController,
@ -9,9 +9,9 @@ import {
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
describe('TranslationController', () => { describe('TranslationController', () => {
const config = mock<Config>(); const configGetSpy = jest.spyOn(config, 'getEnv');
const credentialTypes = mock<ICredentialTypes>(); const credentialTypes = mock<ICredentialTypes>();
const controller = new TranslationController(config, credentialTypes); const controller = new TranslationController(credentialTypes);
describe('getCredentialTranslation', () => { describe('getCredentialTranslation', () => {
it('should throw 400 on invalid credential types', async () => { it('should throw 400 on invalid credential types', async () => {
@ -27,7 +27,7 @@ describe('TranslationController', () => {
it('should return translation json on valid credential types', async () => { it('should return translation json on valid credential types', async () => {
const credentialType = 'credential-type'; const credentialType = 'credential-type';
const req = mock<TranslationRequest.Credential>({ query: { credentialType } }); const req = mock<TranslationRequest.Credential>({ query: { credentialType } });
config.getEnv.calledWith('defaultLocale').mockReturnValue('de'); configGetSpy.mockReturnValue('de');
credentialTypes.recognizes.calledWith(credentialType).mockReturnValue(true); credentialTypes.recognizes.calledWith(credentialType).mockReturnValue(true);
const response = { translation: 'string' }; const response = { translation: 'string' };
jest.mock(`${CREDENTIAL_TRANSLATIONS_DIR}/de/credential-type.json`, () => response, { jest.mock(`${CREDENTIAL_TRANSLATIONS_DIR}/de/credential-type.json`, () => response, {

View file

@ -5,6 +5,8 @@ import type {
ValidationResult, ValidationResult,
} from 'n8n-workflow'; } from 'n8n-workflow';
export type Class<T = object, A extends unknown[] = unknown[]> = new (...args: A) => T;
export interface IProcessMessage { export interface IProcessMessage {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any; data?: any;

View file

@ -3,9 +3,10 @@ import { mock } from 'jest-mock-extended';
import { Duplex } from 'stream'; import { Duplex } from 'stream';
import type { DeepPartial } from 'ts-essentials'; import type { DeepPartial } from 'ts-essentials';
import type { Class } from '@/Interfaces';
export const mockInstance = <T>( export const mockInstance = <T>(
constructor: new (...args: unknown[]) => T, constructor: Class<T>,
data: DeepPartial<T> | undefined = undefined, data: DeepPartial<T> | undefined = undefined,
) => { ) => {
const instance = mock<T>(data); const instance = mock<T>(data);