refactor(core): Switch plain errors in cli to ApplicationError (#7857)

Ensure all errors in `cli` are `ApplicationError` or children of it and
contain no variables in the message, to continue normalizing all the
errors we report to Sentry

Follow-up to: https://github.com/n8n-io/n8n/pull/7839
This commit is contained in:
Iván Ovejero 2023-11-29 12:25:10 +01:00 committed by GitHub
parent 87def60979
commit c08c5cc37b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 277 additions and 195 deletions

View file

@ -7,7 +7,7 @@ import type {
IRun, IRun,
ExecutionStatus, ExecutionStatus,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { WorkflowOperationError, createDeferredPromise } from 'n8n-workflow'; import { ApplicationError, WorkflowOperationError, createDeferredPromise } from 'n8n-workflow';
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';
import type PCancelable from 'p-cancelable'; import type PCancelable from 'p-cancelable';
@ -64,7 +64,7 @@ export class ActiveExecutions {
await Container.get(ExecutionRepository).createNewExecution(fullExecutionData); await Container.get(ExecutionRepository).createNewExecution(fullExecutionData);
executionId = executionResult.id; executionId = executionResult.id;
if (executionId === undefined) { if (executionId === undefined) {
throw new Error('There was an issue assigning an execution id to the execution'); throw new ApplicationError('There was an issue assigning an execution id to the execution');
} }
executionStatus = 'running'; executionStatus = 'running';
} else { } else {
@ -98,9 +98,9 @@ export class ActiveExecutions {
attachWorkflowExecution(executionId: string, workflowExecution: PCancelable<IRun>) { attachWorkflowExecution(executionId: string, workflowExecution: PCancelable<IRun>) {
if (this.activeExecutions[executionId] === undefined) { if (this.activeExecutions[executionId] === undefined) {
throw new Error( throw new ApplicationError('No active execution found to attach to workflow execution to', {
`No active execution with id "${executionId}" got found to attach to workflowExecution to!`, extra: { executionId },
); });
} }
this.activeExecutions[executionId].workflowExecution = workflowExecution; this.activeExecutions[executionId].workflowExecution = workflowExecution;
@ -111,9 +111,9 @@ export class ActiveExecutions {
responsePromise: IDeferredPromise<IExecuteResponsePromiseData>, responsePromise: IDeferredPromise<IExecuteResponsePromiseData>,
): void { ): void {
if (this.activeExecutions[executionId] === undefined) { if (this.activeExecutions[executionId] === undefined) {
throw new Error( throw new ApplicationError('No active execution found to attach to workflow execution to', {
`No active execution with id "${executionId}" got found to attach to workflowExecution to!`, extra: { executionId },
); });
} }
this.activeExecutions[executionId].responsePromise = responsePromise; this.activeExecutions[executionId].responsePromise = responsePromise;

View file

@ -6,7 +6,7 @@ import type {
WorkflowActivateMode, WorkflowActivateMode,
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { WebhookPathTakenError } from 'n8n-workflow'; import { ApplicationError, WebhookPathTakenError } from 'n8n-workflow';
import * as NodeExecuteFunctions from 'n8n-core'; import * as NodeExecuteFunctions from 'n8n-core';
@Service() @Service()
@ -32,7 +32,9 @@ export class ActiveWebhooks {
activation: WorkflowActivateMode, activation: WorkflowActivateMode,
): Promise<void> { ): Promise<void> {
if (workflow.id === undefined) { if (workflow.id === undefined) {
throw new Error('Webhooks can only be added for saved workflows as an id is needed!'); throw new ApplicationError(
'Webhooks can only be added for saved workflows as an ID is needed',
);
} }
if (webhookData.path.endsWith('/')) { if (webhookData.path.endsWith('/')) {
webhookData.path = webhookData.path.slice(0, -1); webhookData.path = webhookData.path.slice(0, -1);

View file

@ -30,6 +30,7 @@ import {
WorkflowActivationError, WorkflowActivationError,
ErrorReporterProxy as ErrorReporter, ErrorReporterProxy as ErrorReporter,
WebhookPathTakenError, WebhookPathTakenError,
ApplicationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type express from 'express'; import type express from 'express';
@ -425,7 +426,7 @@ export class ActiveWorkflowRunner implements IWebhookManager {
}); });
if (workflowData === null) { if (workflowData === null) {
throw new Error(`Could not find workflow with id "${workflowId}"`); throw new ApplicationError('Could not find workflow', { extra: { workflowId } });
} }
const workflow = new Workflow({ const workflow = new Workflow({

View file

@ -1,6 +1,11 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { loadClassInIsolation } from 'n8n-core'; import { loadClassInIsolation } from 'n8n-core';
import type { ICredentialType, ICredentialTypes, LoadedClass } from 'n8n-workflow'; import {
ApplicationError,
type ICredentialType,
type ICredentialTypes,
type LoadedClass,
} from 'n8n-workflow';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
@ -46,6 +51,8 @@ export class CredentialTypes implements ICredentialTypes {
loadedCredentials[type] = { sourcePath, type: loaded }; loadedCredentials[type] = { sourcePath, type: loaded };
return loadedCredentials[type]; return loadedCredentials[type];
} }
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL}: ${type}`); throw new ApplicationError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL, {
tags: { credentialType: type },
});
} }
} }

View file

@ -40,6 +40,7 @@ import {
RoutingNode, RoutingNode,
Workflow, Workflow,
ErrorReporterProxy as ErrorReporter, ErrorReporterProxy as ErrorReporter,
ApplicationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { ICredentialsDb } from '@/Interfaces'; import type { ICredentialsDb } from '@/Interfaces';
@ -81,7 +82,9 @@ const mockNodeTypes: INodeTypes = {
}, },
getByNameAndVersion(nodeType: string, version?: number): INodeType { getByNameAndVersion(nodeType: string, version?: number): INodeType {
if (!mockNodesData[nodeType]) { if (!mockNodesData[nodeType]) {
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_NODE}: ${nodeType}`); throw new ApplicationError(RESPONSE_ERROR_MESSAGES.NO_NODE, {
tags: { nodeType },
});
} }
return NodeHelpers.getVersionedNodeType(mockNodesData[nodeType].type, version); return NodeHelpers.getVersionedNodeType(mockNodesData[nodeType].type, version);
}, },
@ -258,7 +261,10 @@ export class CredentialsHelper extends ICredentialsHelper {
userId?: string, userId?: string,
): Promise<Credentials> { ): Promise<Credentials> {
if (!nodeCredential.id) { if (!nodeCredential.id) {
throw new Error(`Credential "${nodeCredential.name}" of type "${type}" has no ID.`); throw new ApplicationError('Found credential with no ID.', {
extra: { credentialName: nodeCredential.name },
tags: { credentialType: type },
});
} }
let credential: CredentialsEntity; let credential: CredentialsEntity;
@ -291,7 +297,7 @@ export class CredentialsHelper extends ICredentialsHelper {
const credentialTypeData = this.credentialTypes.getByName(type); const credentialTypeData = this.credentialTypes.getByName(type);
if (credentialTypeData === undefined) { if (credentialTypeData === undefined) {
throw new Error(`The credentials of type "${type}" are not known.`); throw new ApplicationError('Unknown credential type', { tags: { credentialType: type } });
} }
if (credentialTypeData.extends === undefined) { if (credentialTypeData.extends === undefined) {

View file

@ -3,7 +3,7 @@ import { Container } from 'typedi';
import type { DataSourceOptions as ConnectionOptions, EntityManager, LoggerOptions } from 'typeorm'; import type { DataSourceOptions as ConnectionOptions, EntityManager, LoggerOptions } from 'typeorm';
import { DataSource as Connection } from 'typeorm'; import { DataSource as Connection } from 'typeorm';
import type { TlsOptions } from 'tls'; import type { TlsOptions } from 'tls';
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
@ -93,7 +93,7 @@ export function getConnectionOptions(dbType: DatabaseType): ConnectionOptions {
return getSqliteConnectionOptions(); return getSqliteConnectionOptions();
default: default:
throw new Error(`The database "${dbType}" is currently not supported!`); throw new ApplicationError('Database type currently not supported', { extra: { dbType } });
} }
} }

View file

@ -10,6 +10,7 @@ import { UserRepository } from '@db/repositories/user.repository';
import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { CredentialsRepository } from '@db/repositories/credentials.repository';
import { SettingsRepository } from '@db/repositories/settings.repository'; import { SettingsRepository } from '@db/repositories/settings.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { ApplicationError } from 'n8n-workflow';
@Service() @Service()
export class ExternalHooks implements IExternalHooksClass { export class ExternalHooks implements IExternalHooksClass {
@ -71,12 +72,13 @@ export class ExternalHooks implements IExternalHooksClass {
const hookFile = require(hookFilePath) as IExternalHooksFileData; const hookFile = require(hookFilePath) as IExternalHooksFileData;
this.loadHooks(hookFile); this.loadHooks(hookFile);
} catch (error) { } catch (e) {
throw new Error( const error = e instanceof Error ? e : new Error(`${e}`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
`Problem loading external hook file "${hookFilePath}": ${error.message}`, throw new ApplicationError('Problem loading external hook file', {
{ cause: error as Error }, extra: { errorMessage: error.message, hookFilePath },
); cause: error,
});
} }
} }
} }

View file

@ -10,7 +10,7 @@ import Container, { Service } from 'typedi';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { jsonParse, type IDataObject } from 'n8n-workflow'; import { jsonParse, type IDataObject, ApplicationError } from 'n8n-workflow';
import { import {
EXTERNAL_SECRETS_INITIAL_BACKOFF, EXTERNAL_SECRETS_INITIAL_BACKOFF,
EXTERNAL_SECRETS_MAX_BACKOFF, EXTERNAL_SECRETS_MAX_BACKOFF,
@ -90,7 +90,7 @@ export class ExternalSecretsManager {
try { try {
return jsonParse(decryptedData); return jsonParse(decryptedData);
} catch (e) { } catch (e) {
throw new Error( throw new ApplicationError(
'External Secrets Settings could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.', 'External Secrets Settings could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.',
); );
} }

View file

@ -2,7 +2,7 @@ import type { SecretsProvider, SecretsProviderSettings, SecretsProviderState } f
import InfisicalClient from 'infisical-node'; import InfisicalClient from 'infisical-node';
import { populateClientWorkspaceConfigsHelper } from 'infisical-node/lib/helpers/key'; import { populateClientWorkspaceConfigsHelper } from 'infisical-node/lib/helpers/key';
import { getServiceTokenData } from 'infisical-node/lib/api/serviceTokenData'; import { getServiceTokenData } from 'infisical-node/lib/api/serviceTokenData';
import type { IDataObject, INodeProperties } from 'n8n-workflow'; import { ApplicationError, type IDataObject, type INodeProperties } from 'n8n-workflow';
import { EXTERNAL_SECRETS_NAME_REGEX } from '../constants'; import { EXTERNAL_SECRETS_NAME_REGEX } from '../constants';
export interface InfisicalSettings { export interface InfisicalSettings {
@ -74,10 +74,10 @@ export class InfisicalProvider implements SecretsProvider {
async update(): Promise<void> { async update(): Promise<void> {
if (!this.client) { if (!this.client) {
throw new Error('Updated attempted on Infisical when initialization failed'); throw new ApplicationError('Updated attempted on Infisical when initialization failed');
} }
if (!(await this.test())[0]) { if (!(await this.test())[0]) {
throw new Error('Infisical provider test failed during update'); throw new ApplicationError('Infisical provider test failed during update');
} }
const secrets = (await this.client.getAllSecrets({ const secrets = (await this.client.getAllSecrets({
environment: this.environment, environment: this.environment,
@ -120,7 +120,7 @@ export class InfisicalProvider implements SecretsProvider {
if (serviceTokenData.scopes) { if (serviceTokenData.scopes) {
return serviceTokenData.scopes[0].environment; return serviceTokenData.scopes[0].environment;
} }
throw new Error("Couldn't find environment for Infisical"); throw new ApplicationError("Couldn't find environment for Infisical");
} }
async test(): Promise<[boolean] | [boolean, string]> { async test(): Promise<[boolean] | [boolean, string]> {

View file

@ -1,3 +1,4 @@
import { ApplicationError } from 'n8n-workflow';
import { LdapService } from './LdapService.ee'; import { LdapService } from './LdapService.ee';
import { LdapSync } from './LdapSync.ee'; import { LdapSync } from './LdapSync.ee';
import type { LdapConfig } from './types'; import type { LdapConfig } from './types';
@ -15,7 +16,7 @@ export class LdapManager {
sync: LdapSync; sync: LdapSync;
} { } {
if (!this.initialized) { if (!this.initialized) {
throw new Error('LDAP Manager has not been initialized'); throw new ApplicationError('LDAP Manager has not been initialized');
} }
return this.ldap; return this.ldap;
} }

View file

@ -4,6 +4,7 @@ import type { LdapConfig } from './types';
import { formatUrl, getMappingAttributes } from './helpers'; import { formatUrl, getMappingAttributes } from './helpers';
import { BINARY_AD_ATTRIBUTES } from './constants'; import { BINARY_AD_ATTRIBUTES } from './constants';
import type { ConnectionOptions } from 'tls'; import type { ConnectionOptions } from 'tls';
import { ApplicationError } from 'n8n-workflow';
export class LdapService { export class LdapService {
private client: Client | undefined; private client: Client | undefined;
@ -25,7 +26,7 @@ export class LdapService {
*/ */
private async getClient() { private async getClient() {
if (this._config === undefined) { if (this._config === undefined) {
throw new Error('Service cannot be used without setting the property config'); throw new ApplicationError('Service cannot be used without setting the property config');
} }
if (this.client === undefined) { if (this.client === undefined) {
const url = formatUrl( const url = formatUrl(

View file

@ -17,6 +17,7 @@ import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHisto
import { Container } from 'typedi'; import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { ApplicationError } from 'n8n-workflow';
export class LdapSync { export class LdapSync {
private intervalId: NodeJS.Timeout | undefined = undefined; private intervalId: NodeJS.Timeout | undefined = undefined;
@ -64,7 +65,7 @@ export class LdapSync {
*/ */
scheduleRun(): void { scheduleRun(): void {
if (!this._config.synchronizationInterval) { if (!this._config.synchronizationInterval) {
throw new Error('Interval variable has to be defined'); throw new ApplicationError('Interval variable has to be defined');
} }
this.intervalId = setInterval(async () => { this.intervalId = setInterval(async () => {
await this.run('live'); await this.run('live');

View file

@ -20,7 +20,7 @@ import {
LDAP_LOGIN_LABEL, LDAP_LOGIN_LABEL,
} from './constants'; } from './constants';
import type { ConnectionSecurity, LdapConfig } from './types'; import type { ConnectionSecurity, LdapConfig } from './types';
import { jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
import { License } from '@/License'; import { License } from '@/License';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { import {
@ -157,7 +157,7 @@ export const updateLdapConfig = async (ldapConfig: LdapConfig): Promise<void> =>
const { valid, message } = validateLdapConfigurationSchema(ldapConfig); const { valid, message } = validateLdapConfigurationSchema(ldapConfig);
if (!valid) { if (!valid) {
throw new Error(message); throw new ApplicationError(message);
} }
if (ldapConfig.loginEnabled && getCurrentAuthenticationMethod() === 'saml') { if (ldapConfig.loginEnabled && getCurrentAuthenticationMethod() === 'saml') {

View file

@ -17,7 +17,7 @@ import type {
INodeTypeData, INodeTypeData,
ICredentialTypeData, ICredentialTypeData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import { import {
@ -56,7 +56,7 @@ export class LoadNodesAndCredentials {
) {} ) {}
async init() { async init() {
if (inTest) throw new Error('Not available in tests'); if (inTest) throw new ApplicationError('Not available in tests');
// Make sure the imported modules can resolve dependencies fine. // Make sure the imported modules can resolve dependencies fine.
const delimiter = process.platform === 'win32' ? ';' : ':'; const delimiter = process.platform === 'win32' ? ';' : ':';

View file

@ -6,7 +6,7 @@ import type {
IVersionedNodeType, IVersionedNodeType,
LoadedClass, LoadedClass,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow'; import { ApplicationError, NodeHelpers } from 'n8n-workflow';
import { Service } from 'typedi'; import { Service } from 'typedi';
import { LoadNodesAndCredentials } from './LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from './LoadNodesAndCredentials';
import { join, dirname } from 'path'; import { join, dirname } from 'path';
@ -30,7 +30,7 @@ export class NodeTypes implements INodeTypes {
const nodeType = this.getNode(nodeTypeName); const nodeType = this.getNode(nodeTypeName);
if (!nodeType) { if (!nodeType) {
throw new Error(`Unknown node type: ${nodeTypeName}`); throw new ApplicationError('Unknown node type', { tags: { nodeTypeName } });
} }
const { description } = NodeHelpers.getVersionedNodeType(nodeType.type, version); const { description } = NodeHelpers.getVersionedNodeType(nodeType.type, version);

View file

@ -1,6 +1,10 @@
import type Bull from 'bull'; import type Bull from 'bull';
import { Service } from 'typedi'; import { Service } from 'typedi';
import type { ExecutionError, IExecuteResponsePromiseData } from 'n8n-workflow'; import {
ApplicationError,
type ExecutionError,
type IExecuteResponsePromiseData,
} from 'n8n-workflow';
import { ActiveExecutions } from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import { decodeWebhookResponse } from '@/helpers/decodeWebhookResponse'; import { decodeWebhookResponse } from '@/helpers/decodeWebhookResponse';
@ -96,7 +100,7 @@ export class Queue {
getBullObjectInstance(): JobQueue { getBullObjectInstance(): JobQueue {
if (this.jobQueue === undefined) { if (this.jobQueue === undefined) {
// if queue is not initialized yet throw an error, since we do not want to hand around an undefined queue // if queue is not initialized yet throw an error, since we do not want to hand around an undefined queue
throw new Error('Queue is not initialized yet!'); throw new ApplicationError('Queue is not initialized yet!');
} }
return this.jobQueue; return this.jobQueue;
} }

View file

@ -27,7 +27,7 @@ import type {
IExecutionsSummary, IExecutionsSummary,
IN8nUISettings, IN8nUISettings,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
// @ts-ignore // @ts-ignore
import timezones from 'google-timezones-json'; import timezones from 'google-timezones-json';
@ -672,7 +672,9 @@ export class Server extends AbstractServer {
const job = currentJobs.find((job) => job.data.executionId === req.params.id); const job = currentJobs.find((job) => job.data.executionId === req.params.id);
if (!job) { if (!job) {
throw new Error(`Could not stop "${req.params.id}" as it is no longer in queue.`); throw new ApplicationError('Could not stop job because it is no longer in queue.', {
extra: { jobId: req.params.id },
});
} else { } else {
await queue.stopJob(job); await queue.stopJob(job);
} }

View file

@ -1,13 +1,14 @@
import type express from 'express'; import type express from 'express';
import { Service } from 'typedi'; import { Service } from 'typedi';
import type { import {
IWebhookData, type IWebhookData,
IWorkflowExecuteAdditionalData, type IWorkflowExecuteAdditionalData,
IHttpRequestMethods, type IHttpRequestMethods,
Workflow, type Workflow,
WorkflowActivateMode, type WorkflowActivateMode,
WorkflowExecuteMode, type WorkflowExecuteMode,
ApplicationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { ActiveWebhooks } from '@/ActiveWebhooks'; import { ActiveWebhooks } from '@/ActiveWebhooks';
@ -215,7 +216,9 @@ export class TestWebhooks implements IWebhookManager {
} }
if (workflow.id === undefined) { if (workflow.id === undefined) {
throw new Error('Webhooks can only be added for saved workflows as an id is needed!'); throw new ApplicationError(
'Webhooks can only be added for saved workflows as an ID is needed',
);
} }
// Remove test-webhooks automatically if they do not get called (after 120 seconds) // Remove test-webhooks automatically if they do not get called (after 120 seconds)

View file

@ -11,6 +11,7 @@ import { getWebhookBaseUrl } from '@/WebhookHelpers';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ApplicationError } from 'n8n-workflow';
export function isSharingEnabled(): boolean { export function isSharingEnabled(): boolean {
return Container.get(License).isSharingEnabled(); return Container.get(License).isSharingEnabled();
@ -94,14 +95,15 @@ export const hashPassword = async (validPassword: string): Promise<string> =>
export async function compareHash(plaintext: string, hashed: string): Promise<boolean | undefined> { export async function compareHash(plaintext: string, hashed: string): Promise<boolean | undefined> {
try { try {
return await compare(plaintext, hashed); return await compare(plaintext, hashed);
} catch (error) { } catch (e) {
const error = e instanceof Error ? e : new Error(`${e}`);
if (error instanceof Error && error.message.includes('Invalid salt version')) { if (error instanceof Error && error.message.includes('Invalid salt version')) {
error.message += error.message +=
'. Comparison against unhashed string. Please check that the value compared against has been hashed.'; '. Comparison against unhashed string. Please check that the value compared against has been hashed.';
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument throw new ApplicationError(error.message, { cause: error });
throw new Error(error);
} }
} }

View file

@ -6,6 +6,7 @@ import { Container, Service } from 'typedi';
import config from '@/config'; import config from '@/config';
import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces'; import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces';
import { NodeMailer } from './NodeMailer'; import { NodeMailer } from './NodeMailer';
import { ApplicationError } from 'n8n-workflow';
type Template = HandlebarsTemplateDelegate<unknown>; type Template = HandlebarsTemplateDelegate<unknown>;
type TemplateName = 'invite' | 'passwordReset'; type TemplateName = 'invite' | 'passwordReset';
@ -50,7 +51,7 @@ export class UserManagementMailer {
} }
async verifyConnection(): Promise<void> { async verifyConnection(): Promise<void> {
if (!this.mailer) throw new Error('No mailer configured.'); if (!this.mailer) throw new ApplicationError('No mailer configured.');
return this.mailer.verifyConnection(); return this.mailer.verifyConnection();
} }

View file

@ -1,4 +1,8 @@
import { ErrorReporterProxy as ErrorReporter, WorkflowOperationError } from 'n8n-workflow'; import {
ApplicationError,
ErrorReporterProxy as ErrorReporter,
WorkflowOperationError,
} from 'n8n-workflow';
import { Container, Service } from 'typedi'; import { Container, Service } from 'typedi';
import type { FindManyOptions, ObjectLiteral } from 'typeorm'; import type { FindManyOptions, ObjectLiteral } from 'typeorm';
import { Not, LessThanOrEqual } from 'typeorm'; import { Not, LessThanOrEqual } from 'typeorm';
@ -106,7 +110,9 @@ export class WaitTracker {
}); });
if (!execution) { if (!execution) {
throw new Error(`The execution ID "${executionId}" could not be found.`); throw new ApplicationError('Execution not found.', {
extra: { executionId },
});
} }
if (!['new', 'unknown', 'waiting', 'running'].includes(execution.status)) { if (!['new', 'unknown', 'waiting', 'running'].includes(execution.status)) {
@ -129,7 +135,9 @@ export class WaitTracker {
}, },
); );
if (!restoredExecution) { if (!restoredExecution) {
throw new Error(`Execution ${executionId} could not be recovered or canceled.`); throw new ApplicationError('Execution could not be recovered or canceled.', {
extra: { executionId },
});
} }
fullExecutionData = restoredExecution; fullExecutionData = restoredExecution;
} }
@ -172,14 +180,14 @@ export class WaitTracker {
}); });
if (!fullExecutionData) { if (!fullExecutionData) {
throw new Error(`The execution with the id "${executionId}" does not exist.`); throw new ApplicationError('Execution does not exist.', { extra: { executionId } });
} }
if (fullExecutionData.finished) { if (fullExecutionData.finished) {
throw new Error('The execution did succeed and can so not be started again.'); throw new ApplicationError('The execution did succeed and can so not be started again.');
} }
if (!fullExecutionData.workflowData.id) { if (!fullExecutionData.workflowData.id) {
throw new Error('Only saved workflows can be resumed.'); throw new ApplicationError('Only saved workflows can be resumed.');
} }
const workflowId = fullExecutionData.workflowData.id; const workflowId = fullExecutionData.workflowData.id;
const user = await this.ownershipService.getWorkflowOwnerCached(workflowId); const user = await this.ownershipService.getWorkflowOwnerCached(workflowId);

View file

@ -1,5 +1,5 @@
import Container from 'typedi'; import Container from 'typedi';
import type { INode, IWorkflowCredentials } from 'n8n-workflow'; import { ApplicationError, type INode, type IWorkflowCredentials } from 'n8n-workflow';
import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { CredentialsRepository } from '@db/repositories/credentials.repository';
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
@ -24,8 +24,9 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
nodeCredentials = node.credentials[type]; nodeCredentials = node.credentials[type];
if (!nodeCredentials.id) { if (!nodeCredentials.id) {
throw new Error( throw new ApplicationError(
`Credentials with name "${nodeCredentials.name}" for type "${type}" miss an ID.`, `Credentials with name "${nodeCredentials.name}" for type "${type}" miss an ID.`,
{ extra: { credentialName: nodeCredentials.name }, tags: { credentialType: type } },
); );
} }
@ -35,9 +36,10 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
type, type,
}); });
if (!foundCredentials) { if (!foundCredentials) {
throw new Error( throw new ApplicationError('Could not find credential.', {
`Could not find credentials for type "${type}" with ID "${nodeCredentials.id}".`, tags: { credentialType: type },
); extra: { credentialId: nodeCredentials.id },
});
} }
returnCredentials[type][nodeCredentials.id] = foundCredentials; returnCredentials[type][nodeCredentials.id] = foundCredentials;

View file

@ -26,6 +26,7 @@ import type {
ExecutionError, ExecutionError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
ApplicationError,
ErrorReporterProxy as ErrorReporter, ErrorReporterProxy as ErrorReporter,
NodeOperationError, NodeOperationError,
Workflow, Workflow,
@ -679,7 +680,7 @@ export async function getWorkflowData(
parentWorkflowSettings?: IWorkflowSettings, parentWorkflowSettings?: IWorkflowSettings,
): Promise<IWorkflowBase> { ): Promise<IWorkflowBase> {
if (workflowInfo.id === undefined && workflowInfo.code === undefined) { if (workflowInfo.id === undefined && workflowInfo.code === undefined) {
throw new Error( throw new ApplicationError(
'No information about the workflow to execute found. Please provide either the "id" or "code"!', 'No information about the workflow to execute found. Please provide either the "id" or "code"!',
); );
} }
@ -691,7 +692,9 @@ export async function getWorkflowData(
workflowData = await WorkflowsService.get({ id: workflowInfo.id }, { relations }); workflowData = await WorkflowsService.get({ id: workflowInfo.id }, { relations });
if (workflowData === undefined || workflowData === null) { if (workflowData === undefined || workflowData === null) {
throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`); throw new ApplicationError('Workflow does not exist.', {
extra: { workflowId: workflowInfo.id },
});
} }
} else { } else {
workflowData = workflowInfo.code ?? null; workflowData = workflowInfo.code ?? null;

View file

@ -10,6 +10,7 @@ import { UserRepository } from '@db/repositories/user.repository';
import { JwtService } from '@/services/jwt.service'; import { JwtService } from '@/services/jwt.service';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { AuthError } from '@/errors/response-errors/auth.error'; import { AuthError } from '@/errors/response-errors/auth.error';
import { ApplicationError } from 'n8n-workflow';
export function issueJWT(user: User): JwtToken { export function issueJWT(user: User): JwtToken {
const { id, email, password } = user; const { id, email, password } = user;
@ -70,7 +71,7 @@ export async function resolveJwtContent(jwtPayload: JwtPayload): Promise<User> {
if (!user || jwtPayload.password !== passwordHash || user.email !== jwtPayload.email) { if (!user || jwtPayload.password !== passwordHash || user.email !== jwtPayload.email) {
// When owner hasn't been set up, the default user // When owner hasn't been set up, the default user
// won't have email nor password (both equals null) // won't have email nor password (both equals null)
throw new Error('Invalid token content'); throw new ApplicationError('Invalid token content');
} }
return user; return user;
} }

View file

@ -2,7 +2,7 @@ import 'reflect-metadata';
import { Command } from '@oclif/command'; import { Command } from '@oclif/command';
import { ExitError } from '@oclif/errors'; import { ExitError } from '@oclif/errors';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow'; import { ApplicationError, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
import { BinaryDataService, InstanceSettings, ObjectStoreService } from 'n8n-core'; import { BinaryDataService, InstanceSettings, ObjectStoreService } from 'n8n-core';
import type { AbstractServer } from '@/AbstractServer'; import type { AbstractServer } from '@/AbstractServer';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
@ -127,7 +127,7 @@ export abstract class BaseCommand extends Command {
if (!isSelected && !isAvailable) return; if (!isSelected && !isAvailable) return;
if (isSelected && !isAvailable) { if (isSelected && !isAvailable) {
throw new Error( throw new ApplicationError(
'External storage selected but unavailable. Please make external storage available by adding "s3" to `N8N_AVAILABLE_BINARY_DATA_MODES`.', 'External storage selected but unavailable. Please make external storage available by adding "s3" to `N8N_AVAILABLE_BINARY_DATA_MODES`.',
); );
} }
@ -171,7 +171,7 @@ export abstract class BaseCommand extends Command {
const host = config.getEnv('externalStorage.s3.host'); const host = config.getEnv('externalStorage.s3.host');
if (host === '') { if (host === '') {
throw new Error( throw new ApplicationError(
'External storage host not configured. Please set `N8N_EXTERNAL_STORAGE_S3_HOST`.', 'External storage host not configured. Please set `N8N_EXTERNAL_STORAGE_S3_HOST`.',
); );
} }
@ -182,13 +182,13 @@ export abstract class BaseCommand extends Command {
}; };
if (bucket.name === '') { if (bucket.name === '') {
throw new Error( throw new ApplicationError(
'External storage bucket name not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_NAME`.', 'External storage bucket name not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_NAME`.',
); );
} }
if (bucket.region === '') { if (bucket.region === '') {
throw new Error( throw new ApplicationError(
'External storage bucket region not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_REGION`.', 'External storage bucket region not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_REGION`.',
); );
} }
@ -199,13 +199,13 @@ export abstract class BaseCommand extends Command {
}; };
if (credentials.accessKey === '') { if (credentials.accessKey === '') {
throw new Error( throw new ApplicationError(
'External storage access key not configured. Please set `N8N_EXTERNAL_STORAGE_S3_ACCESS_KEY`.', 'External storage access key not configured. Please set `N8N_EXTERNAL_STORAGE_S3_ACCESS_KEY`.',
); );
} }
if (credentials.accessSecret === '') { if (credentials.accessSecret === '') {
throw new Error( throw new ApplicationError(
'External storage access secret not configured. Please set `N8N_EXTERNAL_STORAGE_S3_ACCESS_SECRET`.', 'External storage access secret not configured. Please set `N8N_EXTERNAL_STORAGE_S3_ACCESS_SECRET`.',
); );
} }

View file

@ -6,6 +6,7 @@ import type { Risk } from '@/security-audit/types';
import { BaseCommand } from './BaseCommand'; import { BaseCommand } from './BaseCommand';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { ApplicationError } from 'n8n-workflow';
export class SecurityAudit extends BaseCommand { export class SecurityAudit extends BaseCommand {
static description = 'Generate a security audit report for this n8n instance'; static description = 'Generate a security audit report for this n8n instance';
@ -46,7 +47,7 @@ export class SecurityAudit extends BaseCommand {
const hint = `Valid categories are: ${RISK_CATEGORIES.join(', ')}`; const hint = `Valid categories are: ${RISK_CATEGORIES.join(', ')}`;
throw new Error([message, hint].join('. ')); throw new ApplicationError([message, hint].join('. '));
} }
const result = await Container.get(SecurityAuditService).run( const result = await Container.get(SecurityAuditService).run(

View file

@ -2,7 +2,7 @@ import { promises as fs } from 'fs';
import { flags } from '@oclif/command'; import { flags } from '@oclif/command';
import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core'; import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core';
import type { IWorkflowBase } from 'n8n-workflow'; import type { IWorkflowBase } from 'n8n-workflow';
import { ExecutionBaseError } from 'n8n-workflow'; import { ApplicationError, ExecutionBaseError } from 'n8n-workflow';
import { ActiveExecutions } from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
@ -89,7 +89,7 @@ export class Execute extends BaseCommand {
} }
if (!workflowData) { if (!workflowData) {
throw new Error('Failed to retrieve workflow data for requested workflow'); throw new ApplicationError('Failed to retrieve workflow data for requested workflow');
} }
if (!isWorkflowIdValid(workflowId)) { if (!isWorkflowIdValid(workflowId)) {
@ -113,7 +113,7 @@ export class Execute extends BaseCommand {
const data = await activeExecutions.getPostExecutePromise(executionId); const data = await activeExecutions.getPostExecutePromise(executionId);
if (data === undefined) { if (data === undefined) {
throw new Error('Workflow did not return any data!'); throw new ApplicationError('Workflow did not return any data');
} }
if (data.data.resultData.error) { if (data.data.resultData.error) {

View file

@ -3,7 +3,7 @@ import fs from 'fs';
import os from 'os'; import os from 'os';
import { flags } from '@oclif/command'; import { flags } from '@oclif/command';
import type { IRun, ITaskData } from 'n8n-workflow'; import type { IRun, ITaskData } from 'n8n-workflow';
import { jsonParse, sleep } from 'n8n-workflow'; import { ApplicationError, jsonParse, sleep } from 'n8n-workflow';
import { sep } from 'path'; import { sep } from 'path';
import { diff } from 'json-diff'; import { diff } from 'json-diff';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
@ -486,7 +486,7 @@ export class ExecuteBatch extends BaseCommand {
this.updateStatus(); this.updateStatus();
} }
} else { } else {
throw new Error('Wrong execution status - cannot proceed'); throw new ApplicationError('Wrong execution status - cannot proceed');
} }
}); });
} }

View file

@ -7,6 +7,7 @@ import type { ICredentialsDb, ICredentialsDecryptedDb } from '@/Interfaces';
import { BaseCommand } from '../BaseCommand'; import { BaseCommand } from '../BaseCommand';
import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { CredentialsRepository } from '@db/repositories/credentials.repository';
import Container from 'typedi'; import Container from 'typedi';
import { ApplicationError } from 'n8n-workflow';
export class ExportCredentialsCommand extends BaseCommand { export class ExportCredentialsCommand extends BaseCommand {
static description = 'Export credentials'; static description = 'Export credentials';
@ -125,7 +126,7 @@ export class ExportCredentialsCommand extends BaseCommand {
} }
if (credentials.length === 0) { if (credentials.length === 0) {
throw new Error('No credentials found with specified filters.'); throw new ApplicationError('No credentials found with specified filters');
} }
if (flags.separate) { if (flags.separate) {

View file

@ -6,6 +6,7 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { BaseCommand } from '../BaseCommand'; import { BaseCommand } from '../BaseCommand';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
import Container from 'typedi'; import Container from 'typedi';
import { ApplicationError } from 'n8n-workflow';
export class ExportWorkflowsCommand extends BaseCommand { export class ExportWorkflowsCommand extends BaseCommand {
static description = 'Export workflows'; static description = 'Export workflows';
@ -111,7 +112,7 @@ export class ExportWorkflowsCommand extends BaseCommand {
}); });
if (workflows.length === 0) { if (workflows.length === 0) {
throw new Error('No workflows found with specified filters.'); throw new ApplicationError('No workflows found with specified filters');
} }
if (flags.separate) { if (flags.separate) {

View file

@ -12,7 +12,7 @@ import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { disableAutoGeneratedIds } from '@db/utils/commandHelpers'; import { disableAutoGeneratedIds } from '@db/utils/commandHelpers';
import { BaseCommand } from '../BaseCommand'; import { BaseCommand } from '../BaseCommand';
import type { ICredentialsEncrypted } from 'n8n-workflow'; import type { ICredentialsEncrypted } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
import { UM_FIX_INSTRUCTION } from '@/constants'; import { UM_FIX_INSTRUCTION } from '@/constants';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
@ -113,7 +113,7 @@ export class ImportCredentialsCommand extends BaseCommand {
totalImported = credentials.length; totalImported = credentials.length;
if (!Array.isArray(credentials)) { if (!Array.isArray(credentials)) {
throw new Error( throw new ApplicationError(
'File does not seem to contain credentials. Make sure the credentials are contained in an array.', 'File does not seem to contain credentials. Make sure the credentials are contained in an array.',
); );
} }
@ -149,7 +149,7 @@ export class ImportCredentialsCommand extends BaseCommand {
const ownerCredentialRole = await Container.get(RoleService).findCredentialOwnerRole(); const ownerCredentialRole = await Container.get(RoleService).findCredentialOwnerRole();
if (!ownerCredentialRole) { if (!ownerCredentialRole) {
throw new Error(`Failed to find owner credential role. ${UM_FIX_INSTRUCTION}`); throw new ApplicationError(`Failed to find owner credential role. ${UM_FIX_INSTRUCTION}`);
} }
this.ownerCredentialRole = ownerCredentialRole; this.ownerCredentialRole = ownerCredentialRole;
@ -179,7 +179,7 @@ export class ImportCredentialsCommand extends BaseCommand {
(await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole.id })); (await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole.id }));
if (!owner) { if (!owner) {
throw new Error(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
} }
return owner; return owner;
@ -189,7 +189,7 @@ export class ImportCredentialsCommand extends BaseCommand {
const user = await Container.get(UserRepository).findOneBy({ id: userId }); const user = await Container.get(UserRepository).findOneBy({ id: userId });
if (!user) { if (!user) {
throw new Error(`Failed to find user with ID ${userId}`); throw new ApplicationError('Failed to find user', { extra: { userId } });
} }
return user; return user;

View file

@ -1,6 +1,6 @@
import { flags } from '@oclif/command'; import { flags } from '@oclif/command';
import type { INode, INodeCredentialsDetails } from 'n8n-workflow'; import type { INode, INodeCredentialsDetails } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
import fs from 'fs'; import fs from 'fs';
import glob from 'fast-glob'; import glob from 'fast-glob';
import { Container } from 'typedi'; import { Container } from 'typedi';
@ -24,7 +24,7 @@ import { CredentialsRepository } from '@db/repositories/credentials.repository';
function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] { function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] {
if (!Array.isArray(workflows)) { if (!Array.isArray(workflows)) {
throw new Error( throw new ApplicationError(
'File does not seem to contain workflows. Make sure the workflows are contained in an array.', 'File does not seem to contain workflows. Make sure the workflows are contained in an array.',
); );
} }
@ -35,7 +35,7 @@ function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IW
!Object.prototype.hasOwnProperty.call(workflow, 'nodes') || !Object.prototype.hasOwnProperty.call(workflow, 'nodes') ||
!Object.prototype.hasOwnProperty.call(workflow, 'connections') !Object.prototype.hasOwnProperty.call(workflow, 'connections')
) { ) {
throw new Error('File does not seem to contain valid workflows.'); throw new ApplicationError('File does not seem to contain valid workflows.');
} }
} }
} }
@ -217,7 +217,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
const ownerWorkflowRole = await Container.get(RoleService).findWorkflowOwnerRole(); const ownerWorkflowRole = await Container.get(RoleService).findWorkflowOwnerRole();
if (!ownerWorkflowRole) { if (!ownerWorkflowRole) {
throw new Error(`Failed to find owner workflow role. ${UM_FIX_INSTRUCTION}`); throw new ApplicationError(`Failed to find owner workflow role. ${UM_FIX_INSTRUCTION}`);
} }
this.ownerWorkflowRole = ownerWorkflowRole; this.ownerWorkflowRole = ownerWorkflowRole;
@ -244,7 +244,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
(await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole?.id })); (await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole?.id }));
if (!owner) { if (!owner) {
throw new Error(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
} }
return owner; return owner;
@ -254,7 +254,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
const user = await Container.get(UserRepository).findOneBy({ id: userId }); const user = await Container.get(UserRepository).findOneBy({ id: userId });
if (!user) { if (!user) {
throw new Error(`Failed to find user with ID ${userId}`); throw new ApplicationError('Failed to find user', { extra: { userId } });
} }
return user; return user;

View file

@ -13,7 +13,7 @@ import type {
INodeTypes, INodeTypes,
IRun, IRun,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { Workflow, NodeOperationError, sleep } from 'n8n-workflow'; import { Workflow, NodeOperationError, sleep, ApplicationError } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
@ -125,8 +125,9 @@ export class Worker extends BaseCommand {
`Worker failed to find data of execution "${executionId}" in database. Cannot continue.`, `Worker failed to find data of execution "${executionId}" in database. Cannot continue.`,
{ executionId }, { executionId },
); );
throw new Error( throw new ApplicationError(
`Unable to find data of execution "${executionId}" in database. Aborting execution.`, 'Unable to find data of execution in database. Aborting execution.',
{ extra: { executionId } },
); );
} }
const workflowId = fullExecutionData.workflowData.id!; const workflowId = fullExecutionData.workflowData.id!;
@ -150,7 +151,7 @@ export class Worker extends BaseCommand {
'Worker execution failed because workflow could not be found in database.', 'Worker execution failed because workflow could not be found in database.',
{ workflowId, executionId }, { workflowId, executionId },
); );
throw new Error(`The workflow with the ID "${workflowId}" could not be found`); throw new ApplicationError('Workflow could not be found', { extra: { workflowId } });
} }
staticData = workflowData.staticData; staticData = workflowData.staticData;
} }
@ -408,7 +409,7 @@ export class Worker extends BaseCommand {
try { try {
if (!connection.isInitialized) { if (!connection.isInitialized) {
// Connection is not active // Connection is not active
throw new Error('No active database connection!'); throw new ApplicationError('No active database connection');
} }
// DB ping // DB ping
await connection.query('SELECT 1'); await connection.query('SELECT 1');

View file

@ -1,7 +1,7 @@
import convict from 'convict'; import convict from 'convict';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { setGlobalState } from 'n8n-workflow'; import { ApplicationError, setGlobalState } from 'n8n-workflow';
import { inTest, inE2ETests } from '@/constants'; import { inTest, inE2ETests } from '@/constants';
if (inE2ETests) { if (inE2ETests) {
@ -53,7 +53,7 @@ if (!inE2ETests && !inTest) {
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
throw new Error(`The file "${fileName}" could not be found.`); throw new ApplicationError('File not found', { extra: { fileName } });
} }
throw error; throw error;
} }

View file

@ -1,8 +1,9 @@
import { NotStringArrayError } from '@/errors/not-string-array.error'; import { NotStringArrayError } from '@/errors/not-string-array.error';
import type { SchemaObj } from 'convict'; import type { SchemaObj } from 'convict';
import { ApplicationError } from 'n8n-workflow';
export const ensureStringArray = (values: string[], { env }: SchemaObj<string>) => { export const ensureStringArray = (values: string[], { env }: SchemaObj<string>) => {
if (!env) throw new Error(`Missing env: ${env}`); if (!env) throw new ApplicationError('Missing env', { extra: { env } });
if (!Array.isArray(values)) throw new NotStringArrayError(env); if (!Array.isArray(values)) throw new NotStringArrayError(env);

View file

@ -25,6 +25,7 @@ import { AuthError } from '@/errors/response-errors/auth.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
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 { ApplicationError } from 'n8n-workflow';
@Service() @Service()
@RestController() @RestController()
@ -44,8 +45,8 @@ export class AuthController {
@Post('/login') @Post('/login')
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> { async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
const { email, password, mfaToken, mfaRecoveryCode } = req.body; const { email, password, mfaToken, mfaRecoveryCode } = req.body;
if (!email) throw new Error('Email is required to log in'); if (!email) throw new ApplicationError('Email is required to log in');
if (!password) throw new Error('Password is required to log in'); if (!password) throw new ApplicationError('Password is required to log in');
let user: User | undefined; let user: User | undefined;

View file

@ -9,7 +9,7 @@ import omit from 'lodash/omit';
import set from 'lodash/set'; import set from 'lodash/set';
import split from 'lodash/split'; import split from 'lodash/split';
import type { OAuth2GrantType } from 'n8n-workflow'; import type { OAuth2GrantType } from 'n8n-workflow';
import { jsonParse, jsonStringify } from 'n8n-workflow'; import { ApplicationError, jsonParse, jsonStringify } from 'n8n-workflow';
import { Authorized, Get, RestController } from '@/decorators'; import { Authorized, Get, RestController } from '@/decorators';
import { OAuthRequest } from '@/requests'; import { OAuthRequest } from '@/requests';
import { AbstractOAuthController } from './abstractOAuth.controller'; import { AbstractOAuthController } from './abstractOAuth.controller';
@ -255,7 +255,7 @@ export class OAuth2CredentialController extends AbstractOAuthController {
errorMessage, errorMessage,
}); });
if (typeof decoded.cid !== 'string' || typeof decoded.token !== 'string') { if (typeof decoded.cid !== 'string' || typeof decoded.token !== 'string') {
throw new Error(errorMessage); throw new ApplicationError(errorMessage);
} }
return decoded; return decoded;
} }

View file

@ -2,6 +2,7 @@ import type { TableForeignKeyOptions, TableIndexOptions, QueryRunner } from 'typ
import { Table, TableColumn } from 'typeorm'; import { Table, TableColumn } from 'typeorm';
import LazyPromise from 'p-lazy'; import LazyPromise from 'p-lazy';
import { Column } from './Column'; import { Column } from './Column';
import { ApplicationError } from 'n8n-workflow';
abstract class TableOperation<R = void> extends LazyPromise<R> { abstract class TableOperation<R = void> extends LazyPromise<R> {
abstract execute(queryRunner: QueryRunner): Promise<R>; abstract execute(queryRunner: QueryRunner): Promise<R>;
@ -131,7 +132,7 @@ class ModifyNotNull extends TableOperation {
async execute(queryRunner: QueryRunner) { async execute(queryRunner: QueryRunner) {
const { tableName, prefix, columnName, isNullable } = this; const { tableName, prefix, columnName, isNullable } = this;
const table = await queryRunner.getTable(`${prefix}${tableName}`); const table = await queryRunner.getTable(`${prefix}${tableName}`);
if (!table) throw new Error(`No table found with the name ${tableName}`); if (!table) throw new ApplicationError('No table found', { extra: { tableName } });
const oldColumn = table.findColumnByName(columnName)!; const oldColumn = table.findColumnByName(columnName)!;
const newColumn = oldColumn.clone(); const newColumn = oldColumn.clone();
newColumn.isNullable = isNullable; newColumn.isNullable = isNullable;

View file

@ -1,4 +1,5 @@
import type { MigrationContext, ReversibleMigration } from '@db/types'; import type { MigrationContext, ReversibleMigration } from '@db/types';
import { ApplicationError } from 'n8n-workflow';
export class AddGlobalAdminRole1700571993961 implements ReversibleMigration { export class AddGlobalAdminRole1700571993961 implements ReversibleMigration {
async up({ escape, runQuery }: MigrationContext) { async up({ escape, runQuery }: MigrationContext) {
@ -39,7 +40,7 @@ export class AddGlobalAdminRole1700571993961 implements ReversibleMigration {
const memberRoleId = memberRoleIdResult[0]?.id; const memberRoleId = memberRoleIdResult[0]?.id;
if (!memberRoleId) { if (!memberRoleId) {
throw new Error('Could not find global member role!'); throw new ApplicationError('Could not find global member role!');
} }
await runQuery( await runQuery(

View file

@ -8,7 +8,12 @@ import type {
SelectQueryBuilder, SelectQueryBuilder,
} from 'typeorm'; } from 'typeorm';
import { parse, stringify } from 'flatted'; import { parse, stringify } from 'flatted';
import type { ExecutionStatus, IExecutionsSummary, IRunExecutionData } from 'n8n-workflow'; import {
ApplicationError,
type ExecutionStatus,
type IExecutionsSummary,
type IRunExecutionData,
} from 'n8n-workflow';
import { BinaryDataService } from 'n8n-core'; import { BinaryDataService } from 'n8n-core';
import type { import type {
ExecutionPayload, ExecutionPayload,
@ -381,7 +386,9 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
}, },
) { ) {
if (!deleteConditions?.deleteBefore && !deleteConditions?.ids) { if (!deleteConditions?.deleteBefore && !deleteConditions?.ids) {
throw new Error('Either "deleteBefore" or "ids" must be present in the request body'); throw new ApplicationError(
'Either "deleteBefore" or "ids" must be present in the request body',
);
} }
const query = this.createQueryBuilder('execution') const query = this.createQueryBuilder('execution')

View file

@ -3,7 +3,7 @@ import { readFileSync, rmSync } from 'fs';
import { InstanceSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import type { ObjectLiteral } from 'typeorm'; import type { ObjectLiteral } from 'typeorm';
import type { QueryRunner } from 'typeorm/query-runner/QueryRunner'; import type { QueryRunner } from 'typeorm/query-runner/QueryRunner';
import { jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import { inTest } from '@/constants'; import { inTest } from '@/constants';
import type { BaseMigration, Migration, MigrationContext, MigrationFn } from '@db/types'; import type { BaseMigration, Migration, MigrationContext, MigrationFn } from '@db/types';
@ -23,7 +23,7 @@ function loadSurveyFromDisk(): string | null {
const personalizationSurvey = JSON.parse(surveyFile) as object; const personalizationSurvey = JSON.parse(surveyFile) as object;
const kvPairs = Object.entries(personalizationSurvey); const kvPairs = Object.entries(personalizationSurvey);
if (!kvPairs.length) { if (!kvPairs.length) {
throw new Error('personalizationSurvey is empty'); throw new ApplicationError('personalizationSurvey is empty');
} else { } else {
const emptyKeys = kvPairs.reduce((acc, [, value]) => { const emptyKeys = kvPairs.reduce((acc, [, value]) => {
if (!value || (Array.isArray(value) && !value.length)) { if (!value || (Array.isArray(value) && !value.length)) {
@ -32,7 +32,7 @@ function loadSurveyFromDisk(): string | null {
return acc; return acc;
}, 0); }, 0);
if (emptyKeys === kvPairs.length) { if (emptyKeys === kvPairs.length) {
throw new Error('incomplete personalizationSurvey'); throw new ApplicationError('incomplete personalizationSurvey');
} }
} }
return surveyFile; return surveyFile;
@ -68,7 +68,8 @@ const runDisablingForeignKeys = async (
fn: MigrationFn, fn: MigrationFn,
) => { ) => {
const { dbType, queryRunner } = context; const { dbType, queryRunner } = context;
if (dbType !== 'sqlite') throw new Error('Disabling transactions only available in sqlite'); if (dbType !== 'sqlite')
throw new ApplicationError('Disabling transactions only available in sqlite');
await queryRunner.query('PRAGMA foreign_keys=OFF'); await queryRunner.query('PRAGMA foreign_keys=OFF');
await queryRunner.startTransaction(); await queryRunner.startTransaction();
try { try {

View file

@ -24,6 +24,7 @@ import type { BooleanLicenseFeature } from '@/Interfaces';
import Container from 'typedi'; 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';
export const createAuthMiddleware = export const createAuthMiddleware =
(authRole: AuthRole): RequestHandler => (authRole: AuthRole): RequestHandler =>
@ -87,7 +88,9 @@ export const registerController = (app: Application, config: Config, cObj: objec
| string | string
| undefined; | undefined;
if (!controllerBasePath) if (!controllerBasePath)
throw new Error(`${controllerClass.name} is missing the RestController decorator`); throw new ApplicationError('Controller is missing the RestController decorator', {
extra: { controllerName: controllerClass.name },
});
const authRoles = Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) as const authRoles = Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) as
| AuthRoleMetadata | AuthRoleMetadata

View file

@ -35,6 +35,7 @@ import { InternalHooks } from '@/InternalHooks';
import { TagRepository } from '@db/repositories/tag.repository'; import { TagRepository } from '@db/repositories/tag.repository';
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 { ApplicationError } from 'n8n-workflow';
@Service() @Service()
export class SourceControlService { export class SourceControlService {
@ -83,7 +84,7 @@ export class SourceControlService {
false, false,
); );
if (!foldersExisted) { if (!foldersExisted) {
throw new Error(); throw new ApplicationError('No folders exist');
} }
if (!this.gitService.git) { if (!this.gitService.git) {
await this.initGitService(); await this.initGitService();
@ -94,7 +95,7 @@ export class SourceControlService {
branches.current !== branches.current !==
this.sourceControlPreferencesService.sourceControlPreferences.branchName this.sourceControlPreferencesService.sourceControlPreferences.branchName
) { ) {
throw new Error(); throw new ApplicationError('Branch is not set up correctly');
} }
} catch (error) { } catch (error) {
throw new BadRequestError( throw new BadRequestError(
@ -195,7 +196,7 @@ export class SourceControlService {
await this.gitService.pull(); await this.gitService.pull();
} catch (error) { } catch (error) {
this.logger.error(`Failed to reset workfolder: ${(error as Error).message}`); this.logger.error(`Failed to reset workfolder: ${(error as Error).message}`);
throw new Error( throw new ApplicationError(
'Unable to fetch updates from git - your folder might be out of sync. Try reconnecting from the Source Control settings page.', 'Unable to fetch updates from git - your folder might be out of sync. Try reconnecting from the Source Control settings page.',
); );
} }

View file

@ -22,6 +22,7 @@ import { sourceControlFoldersExistCheck } from './sourceControlHelper.ee';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { getInstanceOwner } from '../../UserManagement/UserManagementHelper'; import { getInstanceOwner } from '../../UserManagement/UserManagementHelper';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { ApplicationError } from 'n8n-workflow';
@Service() @Service()
export class SourceControlGitService { export class SourceControlGitService {
@ -43,7 +44,7 @@ export class SourceControlGitService {
}); });
this.logger.debug(`Git binary found: ${gitResult.toString()}`); this.logger.debug(`Git binary found: ${gitResult.toString()}`);
} catch (error) { } catch (error) {
throw new Error(`Git binary not found: ${(error as Error).message}`); throw new ApplicationError('Git binary not found', { cause: error });
} }
try { try {
const sshResult = execSync('ssh -V', { const sshResult = execSync('ssh -V', {
@ -51,7 +52,7 @@ export class SourceControlGitService {
}); });
this.logger.debug(`SSH binary found: ${sshResult.toString()}`); this.logger.debug(`SSH binary found: ${sshResult.toString()}`);
} catch (error) { } catch (error) {
throw new Error(`SSH binary not found: ${(error as Error).message}`); throw new ApplicationError('SSH binary not found', { cause: error });
} }
return true; return true;
} }
@ -114,7 +115,7 @@ export class SourceControlGitService {
private async checkRepositorySetup(): Promise<boolean> { private async checkRepositorySetup(): Promise<boolean> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (async)'); throw new ApplicationError('Git is not initialized (async)');
} }
if (!(await this.git.checkIsRepo())) { if (!(await this.git.checkIsRepo())) {
return false; return false;
@ -129,7 +130,7 @@ export class SourceControlGitService {
private async hasRemote(remote: string): Promise<boolean> { private async hasRemote(remote: string): Promise<boolean> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (async)'); throw new ApplicationError('Git is not initialized (async)');
} }
try { try {
const remotes = await this.git.getRemotes(true); const remotes = await this.git.getRemotes(true);
@ -141,7 +142,7 @@ export class SourceControlGitService {
return true; return true;
} }
} catch (error) { } catch (error) {
throw new Error(`Git is not initialized ${(error as Error).message}`); throw new ApplicationError('Git is not initialized', { cause: error });
} }
this.logger.debug(`Git remote not found: ${remote}`); this.logger.debug(`Git remote not found: ${remote}`);
return false; return false;
@ -155,7 +156,7 @@ export class SourceControlGitService {
user: User, user: User,
): Promise<void> { ): Promise<void> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (Promise)'); throw new ApplicationError('Git is not initialized (Promise)');
} }
if (sourceControlPreferences.initRepo) { if (sourceControlPreferences.initRepo) {
try { try {
@ -193,7 +194,7 @@ export class SourceControlGitService {
async setGitUserDetails(name: string, email: string): Promise<void> { async setGitUserDetails(name: string, email: string): Promise<void> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (setGitUserDetails)'); throw new ApplicationError('Git is not initialized (setGitUserDetails)');
} }
await this.git.addConfig('user.email', email); await this.git.addConfig('user.email', email);
await this.git.addConfig('user.name', name); await this.git.addConfig('user.name', name);
@ -201,7 +202,7 @@ export class SourceControlGitService {
async getBranches(): Promise<{ branches: string[]; currentBranch: string }> { async getBranches(): Promise<{ branches: string[]; currentBranch: string }> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (getBranches)'); throw new ApplicationError('Git is not initialized (getBranches)');
} }
try { try {
@ -218,13 +219,13 @@ export class SourceControlGitService {
currentBranch: current, currentBranch: current,
}; };
} catch (error) { } catch (error) {
throw new Error(`Could not get remote branches from repository ${(error as Error).message}`); throw new ApplicationError('Could not get remote branches from repository', { cause: error });
} }
} }
async setBranch(branch: string): Promise<{ branches: string[]; currentBranch: string }> { async setBranch(branch: string): Promise<{ branches: string[]; currentBranch: string }> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (setBranch)'); throw new ApplicationError('Git is not initialized (setBranch)');
} }
await this.git.checkout(branch); await this.git.checkout(branch);
await this.git.branch([`--set-upstream-to=${SOURCE_CONTROL_ORIGIN}/${branch}`, branch]); await this.git.branch([`--set-upstream-to=${SOURCE_CONTROL_ORIGIN}/${branch}`, branch]);
@ -233,7 +234,7 @@ export class SourceControlGitService {
async getCurrentBranch(): Promise<{ current: string; remote: string }> { async getCurrentBranch(): Promise<{ current: string; remote: string }> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (getCurrentBranch)'); throw new ApplicationError('Git is not initialized (getCurrentBranch)');
} }
const currentBranch = (await this.git.branch()).current; const currentBranch = (await this.git.branch()).current;
return { return {
@ -244,7 +245,7 @@ export class SourceControlGitService {
async diffRemote(): Promise<DiffResult | undefined> { async diffRemote(): Promise<DiffResult | undefined> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (diffRemote)'); throw new ApplicationError('Git is not initialized (diffRemote)');
} }
const currentBranch = await this.getCurrentBranch(); const currentBranch = await this.getCurrentBranch();
if (currentBranch.remote) { if (currentBranch.remote) {
@ -256,7 +257,7 @@ export class SourceControlGitService {
async diffLocal(): Promise<DiffResult | undefined> { async diffLocal(): Promise<DiffResult | undefined> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (diffLocal)'); throw new ApplicationError('Git is not initialized (diffLocal)');
} }
const currentBranch = await this.getCurrentBranch(); const currentBranch = await this.getCurrentBranch();
if (currentBranch.remote) { if (currentBranch.remote) {
@ -268,14 +269,14 @@ export class SourceControlGitService {
async fetch(): Promise<FetchResult> { async fetch(): Promise<FetchResult> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (fetch)'); throw new ApplicationError('Git is not initialized (fetch)');
} }
return this.git.fetch(); return this.git.fetch();
} }
async pull(options: { ffOnly: boolean } = { ffOnly: true }): Promise<PullResult> { async pull(options: { ffOnly: boolean } = { ffOnly: true }): Promise<PullResult> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (pull)'); throw new ApplicationError('Git is not initialized (pull)');
} }
const params = {}; const params = {};
if (options.ffOnly) { if (options.ffOnly) {
@ -293,7 +294,7 @@ export class SourceControlGitService {
): Promise<PushResult> { ): Promise<PushResult> {
const { force, branch } = options; const { force, branch } = options;
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized ({)'); throw new ApplicationError('Git is not initialized ({)');
} }
if (force) { if (force) {
return this.git.push(SOURCE_CONTROL_ORIGIN, branch, ['-f']); return this.git.push(SOURCE_CONTROL_ORIGIN, branch, ['-f']);
@ -303,7 +304,7 @@ export class SourceControlGitService {
async stage(files: Set<string>, deletedFiles?: Set<string>): Promise<string> { async stage(files: Set<string>, deletedFiles?: Set<string>): Promise<string> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (stage)'); throw new ApplicationError('Git is not initialized (stage)');
} }
if (deletedFiles?.size) { if (deletedFiles?.size) {
try { try {
@ -319,7 +320,7 @@ export class SourceControlGitService {
options: { hard: boolean; target: string } = { hard: true, target: 'HEAD' }, options: { hard: boolean; target: string } = { hard: true, target: 'HEAD' },
): Promise<string> { ): Promise<string> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (Promise)'); throw new ApplicationError('Git is not initialized (Promise)');
} }
if (options?.hard) { if (options?.hard) {
return this.git.raw(['reset', '--hard', options.target]); return this.git.raw(['reset', '--hard', options.target]);
@ -331,14 +332,14 @@ export class SourceControlGitService {
async commit(message: string): Promise<CommitResult> { async commit(message: string): Promise<CommitResult> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (commit)'); throw new ApplicationError('Git is not initialized (commit)');
} }
return this.git.commit(message); return this.git.commit(message);
} }
async status(): Promise<StatusResult> { async status(): Promise<StatusResult> {
if (!this.git) { if (!this.git) {
throw new Error('Git is not initialized (status)'); throw new ApplicationError('Git is not initialized (status)');
} }
const statusResult = await this.git.status(); const statusResult = await this.git.status();
return statusResult; return statusResult;

View file

@ -8,7 +8,7 @@ import {
SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER,
} from './constants'; } from './constants';
import glob from 'fast-glob'; import glob from 'fast-glob';
import { jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
import { readFile as fsReadFile } from 'fs/promises'; import { readFile as fsReadFile } from 'fs/promises';
import { Credentials, InstanceSettings } from 'n8n-core'; import { Credentials, InstanceSettings } from 'n8n-core';
import type { IWorkflowToImport } from '@/Interfaces'; import type { IWorkflowToImport } from '@/Interfaces';
@ -63,7 +63,7 @@ export class SourceControlImportService {
const globalOwnerRole = await Container.get(RoleService).findGlobalOwnerRole(); const globalOwnerRole = await Container.get(RoleService).findGlobalOwnerRole();
if (!globalOwnerRole) { if (!globalOwnerRole) {
throw new Error(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
} }
return globalOwnerRole; return globalOwnerRole;
@ -73,7 +73,7 @@ export class SourceControlImportService {
const credentialOwnerRole = await Container.get(RoleService).findCredentialOwnerRole(); const credentialOwnerRole = await Container.get(RoleService).findCredentialOwnerRole();
if (!credentialOwnerRole) { if (!credentialOwnerRole) {
throw new Error(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
} }
return credentialOwnerRole; return credentialOwnerRole;
@ -83,7 +83,7 @@ export class SourceControlImportService {
const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole(); const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole();
if (!workflowOwnerRole) { if (!workflowOwnerRole) {
throw new Error(`Failed to find owner workflow role. ${UM_FIX_INSTRUCTION}`); throw new ApplicationError(`Failed to find owner workflow role. ${UM_FIX_INSTRUCTION}`);
} }
return workflowOwnerRole; return workflowOwnerRole;
@ -255,7 +255,9 @@ export class SourceControlImportService {
['id'], ['id'],
); );
if (upsertResult?.identifiers?.length !== 1) { if (upsertResult?.identifiers?.length !== 1) {
throw new Error(`Failed to upsert workflow ${importedWorkflow.id ?? 'new'}`); throw new ApplicationError('Failed to upsert workflow', {
extra: { workflowId: importedWorkflow.id ?? 'new' },
});
} }
// Update workflow owner to the user who exported the workflow, if that user exists // Update workflow owner to the user who exported the workflow, if that user exists
// in the instance, and the workflow doesn't already have an owner // in the instance, and the workflow doesn't already have an owner
@ -435,7 +437,7 @@ export class SourceControlImportService {
select: ['id'], select: ['id'],
}); });
if (findByName && findByName.id !== tag.id) { if (findByName && findByName.id !== tag.id) {
throw new Error( throw new ApplicationError(
`A tag with the name <strong>${tag.name}</strong> already exists locally.<br />Please either rename the local tag, or the remote one with the id <strong>${tag.id}</strong> in the tags.json file.`, `A tag with the name <strong>${tag.name}</strong> already exists locally.<br />Please either rename the local tag, or the remote one with the id <strong>${tag.id}</strong> in the tags.json file.`,
); );
} }

View file

@ -10,7 +10,7 @@ import {
sourceControlFoldersExistCheck, sourceControlFoldersExistCheck,
} from './sourceControlHelper.ee'; } from './sourceControlHelper.ee';
import { InstanceSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import { jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
import { import {
SOURCE_CONTROL_SSH_FOLDER, SOURCE_CONTROL_SSH_FOLDER,
SOURCE_CONTROL_GIT_FOLDER, SOURCE_CONTROL_GIT_FOLDER,
@ -150,7 +150,9 @@ export class SourceControlPreferencesService {
validationError: { target: false }, validationError: { target: false },
}); });
if (validationResult.length > 0) { if (validationResult.length > 0) {
throw new Error(`Invalid source control preferences: ${JSON.stringify(validationResult)}`); throw new ApplicationError('Invalid source control preferences', {
extra: { preferences: validationResult },
});
} }
return validationResult; return validationResult;
} }
@ -177,7 +179,7 @@ export class SourceControlPreferencesService {
loadOnStartup: true, loadOnStartup: true,
}); });
} catch (error) { } catch (error) {
throw new Error(`Failed to save source control preferences: ${(error as Error).message}`); throw new ApplicationError('Failed to save source control preferences', { cause: error });
} }
} }
return this.sourceControlPreferences; return this.sourceControlPreferences;

View file

@ -1,6 +1,6 @@
import { validate as jsonSchemaValidate } from 'jsonschema'; import { validate as jsonSchemaValidate } from 'jsonschema';
import type { IWorkflowBase, JsonObject, ExecutionStatus } from 'n8n-workflow'; import type { IWorkflowBase, JsonObject, ExecutionStatus } from 'n8n-workflow';
import { jsonParse, Workflow, WorkflowOperationError } from 'n8n-workflow'; import { ApplicationError, jsonParse, Workflow, WorkflowOperationError } from 'n8n-workflow';
import type { FindOperator } from 'typeorm'; import type { FindOperator } from 'typeorm';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { ActiveExecutions } from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
@ -234,7 +234,7 @@ export class ExecutionsService {
} }
if (execution.finished) { if (execution.finished) {
throw new Error('The execution succeeded, so it cannot be retried.'); throw new ApplicationError('The execution succeeded, so it cannot be retried.');
} }
const executionMode = 'retry'; const executionMode = 'retry';
@ -276,8 +276,9 @@ export class ExecutionsService {
})) as IWorkflowBase; })) as IWorkflowBase;
if (workflowData === undefined) { if (workflowData === undefined) {
throw new Error( throw new ApplicationError(
`The workflow with the ID "${workflowId}" could not be found and so the data not be loaded for the retry.`, 'Workflow could not be found and so the data not be loaded for the retry.',
{ extra: { workflowId } },
); );
} }
@ -324,7 +325,7 @@ export class ExecutionsService {
await Container.get(ActiveExecutions).getPostExecutePromise(retriedExecutionId); await Container.get(ActiveExecutions).getPostExecutePromise(retriedExecutionId);
if (!executionData) { if (!executionData) {
throw new Error('The retry did not start for an unknown reason.'); throw new ApplicationError('The retry did not start for an unknown reason.');
} }
return !!executionData.finished; return !!executionData.finished;

View file

@ -3,13 +3,13 @@
import { isObjectLiteral } from '@/utils'; import { isObjectLiteral } from '@/utils';
import { plainToInstance, instanceToPlain } from 'class-transformer'; import { plainToInstance, instanceToPlain } from 'class-transformer';
import { validate } from 'class-validator'; import { validate } from 'class-validator';
import { jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
export class BaseFilter { export class BaseFilter {
protected static async toFilter(rawFilter: string, Filter: typeof BaseFilter) { protected static async toFilter(rawFilter: string, Filter: typeof BaseFilter) {
const dto = jsonParse(rawFilter, { errorMessage: 'Failed to parse filter JSON' }); const dto = jsonParse(rawFilter, { errorMessage: 'Failed to parse filter JSON' });
if (!isObjectLiteral(dto)) throw new Error('Filter must be an object literal'); if (!isObjectLiteral(dto)) throw new ApplicationError('Filter must be an object literal');
const instance = plainToInstance(Filter, dto, { const instance = plainToInstance(Filter, dto, {
excludeExtraneousValues: true, // remove fields not in class excludeExtraneousValues: true, // remove fields not in class
@ -25,6 +25,6 @@ export class BaseFilter {
private async validate() { private async validate() {
const result = await validate(this); const result = await validate(this);
if (result.length > 0) throw new Error('Parsed filter does not fit the schema'); if (result.length > 0) throw new ApplicationError('Parsed filter does not fit the schema');
} }
} }

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { isStringArray } from '@/utils'; import { isStringArray } from '@/utils';
import { jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
export class BaseSelect { export class BaseSelect {
static selectableFields: Set<string>; static selectableFields: Set<string>;
@ -9,7 +9,7 @@ export class BaseSelect {
protected static toSelect(rawFilter: string, Select: typeof BaseSelect) { protected static toSelect(rawFilter: string, Select: typeof BaseSelect) {
const dto = jsonParse(rawFilter, { errorMessage: 'Failed to parse filter JSON' }); const dto = jsonParse(rawFilter, { errorMessage: 'Failed to parse filter JSON' });
if (!isStringArray(dto)) throw new Error('Parsed select is not a string array'); if (!isStringArray(dto)) throw new ApplicationError('Parsed select is not a string array');
return dto.reduce<Record<string, true>>((acc, field) => { return dto.reduce<Record<string, true>>((acc, field) => {
if (!Select.selectableFields.has(field)) return acc; if (!Select.selectableFields.has(field)) return acc;

View file

@ -1,13 +1,14 @@
import { isIntegerString } from '@/utils'; import { isIntegerString } from '@/utils';
import { ApplicationError } from 'n8n-workflow';
export class Pagination { export class Pagination {
static fromString(rawTake: string, rawSkip: string) { static fromString(rawTake: string, rawSkip: string) {
if (!isIntegerString(rawTake)) { if (!isIntegerString(rawTake)) {
throw new Error('Parameter take is not an integer string'); throw new ApplicationError('Parameter take is not an integer string');
} }
if (!isIntegerString(rawSkip)) { if (!isIntegerString(rawSkip)) {
throw new Error('Parameter skip is not an integer string'); throw new ApplicationError('Parameter skip is not an integer string');
} }
const [take, skip] = [rawTake, rawSkip].map((o) => parseInt(o, 10)); const [take, skip] = [rawTake, rawSkip].map((o) => parseInt(o, 10));

View file

@ -3,6 +3,7 @@ import * as ResponseHelper from '@/ResponseHelper';
import { Pagination } from './dtos/pagination.dto'; import { Pagination } from './dtos/pagination.dto';
import type { ListQuery } from '@/requests'; import type { ListQuery } from '@/requests';
import type { RequestHandler } from 'express'; import type { RequestHandler } from 'express';
import { ApplicationError } from 'n8n-workflow';
export const paginationListQueryMiddleware: RequestHandler = ( export const paginationListQueryMiddleware: RequestHandler = (
req: ListQuery.Request, req: ListQuery.Request,
@ -13,7 +14,7 @@ export const paginationListQueryMiddleware: RequestHandler = (
try { try {
if (!rawTake && req.query.skip) { if (!rawTake && req.query.skip) {
throw new Error('Please specify `take` when using `skip`'); throw new ApplicationError('Please specify `take` when using `skip`');
} }
if (!rawTake) return next(); if (!rawTake) return next();

View file

@ -3,7 +3,7 @@ import config from '@/config';
import { caching } from 'cache-manager'; import { caching } from 'cache-manager';
import type { MemoryCache } from 'cache-manager'; import type { MemoryCache } from 'cache-manager';
import type { RedisCache } from 'cache-manager-ioredis-yet'; import type { RedisCache } from 'cache-manager-ioredis-yet';
import { jsonStringify } from 'n8n-workflow'; import { ApplicationError, jsonStringify } from 'n8n-workflow';
import { getDefaultRedisClient, getRedisPrefix } from './redis/RedisServiceHelper'; import { getDefaultRedisClient, getRedisPrefix } from './redis/RedisServiceHelper';
import EventEmitter from 'events'; import EventEmitter from 'events';
@ -161,7 +161,9 @@ export class CacheService extends EventEmitter {
this.emit(this.metricsCounterEvents.cacheUpdate); this.emit(this.metricsCounterEvents.cacheUpdate);
const refreshValues: unknown[] = await options.refreshFunctionMany(keys); const refreshValues: unknown[] = await options.refreshFunctionMany(keys);
if (keys.length !== refreshValues.length) { if (keys.length !== refreshValues.length) {
throw new Error('refreshFunctionMany must return the same number of values as keys'); throw new ApplicationError(
'refreshFunctionMany must return the same number of values as keys',
);
} }
const newKV: Array<[string, unknown]> = []; const newKV: Array<[string, unknown]> = [];
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
@ -191,7 +193,7 @@ export class CacheService extends EventEmitter {
} }
if (this.isRedisCache()) { if (this.isRedisCache()) {
if (!(this.cache as RedisCache)?.store?.isCacheable(value)) { if (!(this.cache as RedisCache)?.store?.isCacheable(value)) {
throw new Error('Value is not cacheable'); throw new ApplicationError('Value is not cacheable');
} }
} }
await this.cache?.store.set(key, value, ttl); await this.cache?.store.set(key, value, ttl);
@ -215,7 +217,7 @@ export class CacheService extends EventEmitter {
if (this.isRedisCache()) { if (this.isRedisCache()) {
nonNullValues.forEach(([_key, value]) => { nonNullValues.forEach(([_key, value]) => {
if (!(this.cache as RedisCache)?.store?.isCacheable(value)) { if (!(this.cache as RedisCache)?.store?.isCacheable(value)) {
throw new Error('Value is not cacheable'); throw new ApplicationError('Value is not cacheable');
} }
}); });
} }
@ -301,7 +303,7 @@ export class CacheService extends EventEmitter {
} }
return map; return map;
} }
throw new Error( throw new ApplicationError(
'Keys and values do not match, this should not happen and appears to result from some cache corruption.', 'Keys and values do not match, this should not happen and appears to result from some cache corruption.',
); );
} }

View file

@ -5,7 +5,7 @@ import { Service } from 'typedi';
import { promisify } from 'util'; import { promisify } from 'util';
import axios from 'axios'; import axios from 'axios';
import type { PublicInstalledPackage } from 'n8n-workflow'; import { ApplicationError, type PublicInstalledPackage } from 'n8n-workflow';
import { InstanceSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import type { PackageDirectoryLoader } from 'n8n-core'; import type { PackageDirectoryLoader } from 'n8n-core';
@ -93,10 +93,10 @@ export class CommunityPackagesService {
} }
parseNpmPackageName(rawString?: string): CommunityPackages.ParsedPackageName { parseNpmPackageName(rawString?: string): CommunityPackages.ParsedPackageName {
if (!rawString) throw new Error(PACKAGE_NAME_NOT_PROVIDED); if (!rawString) throw new ApplicationError(PACKAGE_NAME_NOT_PROVIDED);
if (INVALID_OR_SUSPICIOUS_PACKAGE_NAME.test(rawString)) { if (INVALID_OR_SUSPICIOUS_PACKAGE_NAME.test(rawString)) {
throw new Error('Package name must be a single word'); throw new ApplicationError('Package name must be a single word');
} }
const scope = rawString.includes('/') ? rawString.split('/')[0] : undefined; const scope = rawString.includes('/') ? rawString.split('/')[0] : undefined;
@ -104,7 +104,7 @@ export class CommunityPackagesService {
const packageNameWithoutScope = scope ? rawString.replace(`${scope}/`, '') : rawString; const packageNameWithoutScope = scope ? rawString.replace(`${scope}/`, '') : rawString;
if (!packageNameWithoutScope.startsWith(NODE_PACKAGE_PREFIX)) { if (!packageNameWithoutScope.startsWith(NODE_PACKAGE_PREFIX)) {
throw new Error(`Package name must start with ${NODE_PACKAGE_PREFIX}`); throw new ApplicationError(`Package name must start with ${NODE_PACKAGE_PREFIX}`);
} }
const version = packageNameWithoutScope.includes('@') const version = packageNameWithoutScope.includes('@')
@ -155,12 +155,12 @@ export class CommunityPackagesService {
}; };
Object.entries(map).forEach(([npmMessage, n8nMessage]) => { Object.entries(map).forEach(([npmMessage, n8nMessage]) => {
if (errorMessage.includes(npmMessage)) throw new Error(n8nMessage); if (errorMessage.includes(npmMessage)) throw new ApplicationError(n8nMessage);
}); });
this.logger.warn('npm command failed', { errorMessage }); this.logger.warn('npm command failed', { errorMessage });
throw new Error(PACKAGE_FAILED_TO_INSTALL); throw new ApplicationError(PACKAGE_FAILED_TO_INSTALL);
} }
} }
@ -327,7 +327,7 @@ export class CommunityPackagesService {
await this.executeNpmCommand(command); await this.executeNpmCommand(command);
} catch (error) { } catch (error) {
if (error instanceof Error && error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) { if (error instanceof Error && error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
throw new Error(`The npm package "${packageName}" could not be found.`); throw new ApplicationError('npm package not found', { extra: { packageName } });
} }
throw error; throw error;
} }
@ -341,7 +341,7 @@ export class CommunityPackagesService {
try { try {
await this.executeNpmCommand(removeCommand); await this.executeNpmCommand(removeCommand);
} catch {} } catch {}
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error }); throw new ApplicationError(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error });
} }
if (loader.loadedNodes.length > 0) { if (loader.loadedNodes.length > 0) {
@ -354,7 +354,10 @@ export class CommunityPackagesService {
await this.loadNodesAndCredentials.postProcessLoaders(); await this.loadNodesAndCredentials.postProcessLoaders();
return installedPackage; return installedPackage;
} catch (error) { } catch (error) {
throw new Error(`Failed to save installed package: ${packageName}`, { cause: error }); throw new ApplicationError('Failed to save installed package', {
extra: { packageName },
cause: error,
});
} }
} else { } else {
// Remove this package since it contains no loadable nodes // Remove this package since it contains no loadable nodes
@ -363,7 +366,7 @@ export class CommunityPackagesService {
await this.executeNpmCommand(removeCommand); await this.executeNpmCommand(removeCommand);
} catch {} } catch {}
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES); throw new ApplicationError(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
} }
} }
} }

View file

@ -16,7 +16,7 @@ import type {
INodeParameters, INodeParameters,
INodeTypeNameVersion, INodeTypeNameVersion,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { Workflow, RoutingNode } from 'n8n-workflow'; import { Workflow, RoutingNode, ApplicationError } from 'n8n-workflow';
import { NodeExecuteFunctions } from 'n8n-core'; import { NodeExecuteFunctions } from 'n8n-core';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
@ -57,8 +57,9 @@ export class DynamicNodeParametersService {
// requiring a baseURL to be defined can at least not a random server be called. // requiring a baseURL to be defined can at least not a random server be called.
// In the future this code has to get improved that it does not use the request information from // In the future this code has to get improved that it does not use the request information from
// the request rather resolves it via the parameter-path and nodeType data. // the request rather resolves it via the parameter-path and nodeType data.
throw new Error( throw new ApplicationError(
`The node-type "${nodeType.description.name}" does not exist or does not have "requestDefaults.baseURL" defined!`, 'Node type does not exist or does not have "requestDefaults.baseURL" defined!',
{ tags: { nodeType: nodeType.description.name } },
); );
} }
@ -114,7 +115,7 @@ export class DynamicNodeParametersService {
} }
if (!Array.isArray(optionsData)) { if (!Array.isArray(optionsData)) {
throw new Error('The returned data is not an array!'); throw new ApplicationError('The returned data is not an array');
} }
return optionsData[0].map((item) => item.json) as unknown as INodePropertyOptions[]; return optionsData[0].map((item) => item.json) as unknown as INodePropertyOptions[];
@ -182,9 +183,10 @@ export class DynamicNodeParametersService {
) { ) {
const method = nodeType.methods?.[type]?.[methodName]; const method = nodeType.methods?.[type]?.[methodName];
if (typeof method !== 'function') { if (typeof method !== 'function') {
throw new Error( throw new ApplicationError('Node type does not have method defined', {
`The node-type "${nodeType.description.name}" does not have the method "${methodName}" defined!`, tags: { nodeType: nodeType.description.name },
); extra: { methodName },
});
} }
return method; return method;
} }

View file

@ -7,6 +7,7 @@ import { UserService } from './user.service';
import type { Credentials, ListQuery } from '@/requests'; import type { Credentials, ListQuery } from '@/requests';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { ApplicationError } from 'n8n-workflow';
@Service() @Service()
export class OwnershipService { export class OwnershipService {
@ -27,7 +28,7 @@ export class OwnershipService {
const workflowOwnerRole = await this.roleService.findWorkflowOwnerRole(); const workflowOwnerRole = await this.roleService.findWorkflowOwnerRole();
if (!workflowOwnerRole) throw new Error('Failed to find workflow owner role'); if (!workflowOwnerRole) throw new ApplicationError('Failed to find workflow owner role');
const sharedWorkflow = await this.sharedWorkflowRepository.findOneOrFail({ const sharedWorkflow = await this.sharedWorkflowRepository.findOneOrFail({
where: { workflowId, roleId: workflowOwnerRole.id }, where: { workflowId, roleId: workflowOwnerRole.id },

View file

@ -1,7 +1,7 @@
import type express from 'express'; import type express from 'express';
import Container, { Service } from 'typedi'; import Container, { Service } from 'typedi';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
import { getServiceProviderInstance } from './serviceProvider.ee'; import { getServiceProviderInstance } from './serviceProvider.ee';
import type { SamlUserAttributes } from './types/samlUserAttributes'; import type { SamlUserAttributes } from './types/samlUserAttributes';
import { isSsoJustInTimeProvisioningEnabled } from '../ssoHelpers'; import { isSsoJustInTimeProvisioningEnabled } from '../ssoHelpers';
@ -93,7 +93,7 @@ export class SamlService {
validate: async (response: string) => { validate: async (response: string) => {
const valid = await validateResponse(response); const valid = await validateResponse(response);
if (!valid) { if (!valid) {
throw new Error('Invalid SAML response'); throw new ApplicationError('Invalid SAML response');
} }
}, },
}); });
@ -101,7 +101,7 @@ export class SamlService {
getIdentityProviderInstance(forceRecreate = false): IdentityProviderInstance { getIdentityProviderInstance(forceRecreate = false): IdentityProviderInstance {
if (this.samlify === undefined) { if (this.samlify === undefined) {
throw new Error('Samlify is not initialized'); throw new ApplicationError('Samlify is not initialized');
} }
if (this.identityProviderInstance === undefined || forceRecreate) { if (this.identityProviderInstance === undefined || forceRecreate) {
this.identityProviderInstance = this.samlify.IdentityProvider({ this.identityProviderInstance = this.samlify.IdentityProvider({
@ -114,7 +114,7 @@ export class SamlService {
getServiceProviderInstance(): ServiceProviderInstance { getServiceProviderInstance(): ServiceProviderInstance {
if (this.samlify === undefined) { if (this.samlify === undefined) {
throw new Error('Samlify is not initialized'); throw new ApplicationError('Samlify is not initialized');
} }
return getServiceProviderInstance(this._samlPreferences, this.samlify); return getServiceProviderInstance(this._samlPreferences, this.samlify);
} }
@ -225,7 +225,7 @@ export class SamlService {
} else if (prefs.metadata) { } else if (prefs.metadata) {
const validationResult = await validateMetadata(prefs.metadata); const validationResult = await validateMetadata(prefs.metadata);
if (!validationResult) { if (!validationResult) {
throw new Error('Invalid SAML metadata'); throw new ApplicationError('Invalid SAML metadata');
} }
} }
this.getIdentityProviderInstance(true); this.getIdentityProviderInstance(true);

View file

@ -11,7 +11,7 @@ import type {
WorkflowWithSharingsAndCredentials, WorkflowWithSharingsAndCredentials,
} from './workflows.types'; } from './workflows.types';
import { CredentialsService } from '@/credentials/credentials.service'; import { CredentialsService } from '@/credentials/credentials.service';
import { NodeOperationError } from 'n8n-workflow'; import { ApplicationError, NodeOperationError } from 'n8n-workflow';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
import Container from 'typedi'; import Container from 'typedi';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
@ -161,7 +161,9 @@ export class EEWorkflowsService extends WorkflowsService {
if (credentialId === undefined) return; if (credentialId === undefined) return;
const matchedCredential = allowedCredentials.find(({ id }) => id === credentialId); const matchedCredential = allowedCredentials.find(({ id }) => id === credentialId);
if (!matchedCredential) { if (!matchedCredential) {
throw new Error('The workflow contains credentials that you do not have access to'); throw new ApplicationError(
'The workflow contains credentials that you do not have access to',
);
} }
}); });
}); });

View file

@ -51,10 +51,7 @@ describe('WorkflowCredentials', () => {
}); });
test('Should return an error if credentials cannot be found in the DB', async () => { test('Should return an error if credentials cannot be found in the DB', async () => {
const credentials = notFoundNode.credentials!.test; const expectedError = new Error('Could not find credential.');
const expectedError = new Error(
`Could not find credentials for type "test" with ID "${credentials.id}".`,
);
await expect(WorkflowCredentials([notFoundNode])).rejects.toEqual(expectedError); await expect(WorkflowCredentials([notFoundNode])).rejects.toEqual(expectedError);
expect(credentialsRepository.findOneBy).toHaveBeenCalledTimes(1); expect(credentialsRepository.findOneBy).toHaveBeenCalledTimes(1);
}); });