Merge branch 'master' into testing-framework

This commit is contained in:
Omar Ajoue 2021-04-28 16:07:16 +02:00
commit fdbbf9348d
618 changed files with 9389 additions and 4970 deletions

View file

@ -7,6 +7,7 @@
"build": "lerna exec npm run build", "build": "lerna exec npm run build",
"dev": "lerna exec npm run dev --parallel", "dev": "lerna exec npm run dev --parallel",
"clean:dist": "lerna exec -- rimraf ./dist", "clean:dist": "lerna exec -- rimraf ./dist",
"optimize-svg": "find ./packages -name '*.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
"start": "run-script-os", "start": "run-script-os",
"start:default": "cd packages/cli/bin && ./n8n", "start:default": "cd packages/cli/bin && ./n8n",
"start:windows": "cd packages/cli/bin && n8n", "start:windows": "cd packages/cli/bin && n8n",

View file

@ -2,6 +2,42 @@
This list shows all the versions which include breaking changes and how to upgrade. This list shows all the versions which include breaking changes and how to upgrade.
## 0.117.0
### What changed?
Removed the "Activation Trigger" node. This node was replaced by two other nodes.
The "Activation Trigger" node was added on version 0.113.0 but was not fully compliant to UX, so we decided to refactor and change it ASAP so it affects the least possible users.
The new nodes are "n8n Trigger" and "Workflow Trigger". Behavior-wise, the nodes do the same, we just split the functionality to make it more intuitive to users.
### When is action necessary?
If you use the "Activation Trigger" in any of your workflows, please replace it by the new nodes.
### How to upgrade:
Remove the previous node and add the new ones according to your workflows.
----------------------------
Changed the behavior for nodes that use Postgres Wire Protocol: Postgres, QuestDB, CrateDB and TimescaleDB.
All nodes have been standardized and now follow the same patterns. Behavior will be the same for most cases, but new added functionality can now be explored.
You can now also inform how you would like n8n to execute queries. Default mode is `Multiple queries` which translates to previous behavior, but you can now run them `Independently` or `Transaction`. Also, `Continue on Fail` now plays a major role for the new modes.
The node output for `insert` operations now rely on the new parameter `Return fields`, just like `update` operations did previously.
### When is action necessary?
If you rely on the output returned by `insert` operations for any of the mentioned nodes, we recommend you review your workflows.
By default, all `insert` operations will have `Return fields: *` as the default, setting, returning all information inserted.
Previously, the node would return all information it received, without taking into account what actually happened in the database.
## 0.113.0 ## 0.113.0
### What changed? ### What changed?

View file

@ -170,10 +170,11 @@ export class Execute extends Command {
this.log('===================================='); this.log('====================================');
this.log(JSON.stringify(data, null, 2)); this.log(JSON.stringify(data, null, 2));
// console.log(data.data.resultData.error); const { error } = data.data.resultData;
const error = new Error(data.data.resultData.error.message); throw {
error.stack = data.data.resultData.error.stack; ...error,
throw error; stack: error.stack,
};
} }
if (flags.rawOutput === undefined) { if (flags.rawOutput === undefined) {
this.log('Execution was successfull:'); this.log('Execution was successfull:');
@ -186,7 +187,6 @@ export class Execute extends Command {
console.error(e.message); console.error(e.message);
console.error(e.stack); console.error(e.stack);
this.exit(1); this.exit(1);
return;
} }
this.exit(); this.exit();

View file

@ -127,11 +127,22 @@ export class Worker extends Command {
staticData = workflowData.staticData; staticData = workflowData.staticData;
} }
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
if (currentExecutionDb.workflowData.settings && currentExecutionDb.workflowData.settings.executionTimeout) {
workflowTimeout = currentExecutionDb.workflowData.settings!.executionTimeout as number; // preference on workflow setting
}
let executionTimeoutTimestamp: number | undefined;
if (workflowTimeout > 0) {
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number);
executionTimeoutTimestamp = Date.now() + workflowTimeout * 1000;
}
const workflow = new Workflow({ id: currentExecutionDb.workflowData.id as string, name: currentExecutionDb.workflowData.name, nodes: currentExecutionDb.workflowData!.nodes, connections: currentExecutionDb.workflowData!.connections, active: currentExecutionDb.workflowData!.active, nodeTypes, staticData, settings: currentExecutionDb.workflowData!.settings }); const workflow = new Workflow({ id: currentExecutionDb.workflowData.id as string, name: currentExecutionDb.workflowData.name, nodes: currentExecutionDb.workflowData!.nodes, connections: currentExecutionDb.workflowData!.connections, active: currentExecutionDb.workflowData!.active, nodeTypes, staticData, settings: currentExecutionDb.workflowData!.settings });
const credentials = await WorkflowCredentials(currentExecutionDb.workflowData.nodes); const credentials = await WorkflowCredentials(currentExecutionDb.workflowData.nodes);
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials, undefined, executionTimeoutTimestamp);
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string }); additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string });
let workflowExecute: WorkflowExecute; let workflowExecute: WorkflowExecute;

View file

@ -446,6 +446,12 @@ const config = convict({
}, },
endpoints: { endpoints: {
payloadSizeMax: {
format: Number,
default: 16,
env: 'N8N_PAYLOAD_SIZE_MAX',
doc: 'Maximum payload size in MB.',
},
metrics: { metrics: {
enable: { enable: {
format: 'Boolean', format: 'Boolean',

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.114.0", "version": "0.117.0",
"description": "n8n Workflow Automation Tool", "description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -55,7 +55,6 @@
"devDependencies": { "devDependencies": {
"@oclif/dev-cli": "^1.22.2", "@oclif/dev-cli": "^1.22.2",
"@types/basic-auth": "^1.1.2", "@types/basic-auth": "^1.1.2",
"@types/bcryptjs": "^2.4.1",
"@types/bull": "^3.3.10", "@types/bull": "^3.3.10",
"@types/compression": "1.0.1", "@types/compression": "1.0.1",
"@types/connect-history-api-fallback": "^1.3.1", "@types/connect-history-api-fallback": "^1.3.1",
@ -80,12 +79,12 @@
"typescript": "~3.9.7" "typescript": "~3.9.7"
}, },
"dependencies": { "dependencies": {
"@node-rs/bcrypt": "^1.2.0",
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
"@oclif/errors": "^1.2.2", "@oclif/errors": "^1.2.2",
"@types/json-diff": "^0.5.1", "@types/json-diff": "^0.5.1",
"@types/jsonwebtoken": "^8.3.4", "@types/jsonwebtoken": "^8.3.4",
"basic-auth": "^2.0.1", "basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"body-parser-xml": "^1.1.0", "body-parser-xml": "^1.1.0",
"bull": "^3.19.0", "bull": "^3.19.0",
@ -105,11 +104,11 @@
"jwks-rsa": "~1.12.1", "jwks-rsa": "~1.12.1",
"localtunnel": "^2.0.0", "localtunnel": "^2.0.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mysql2": "~2.1.0", "mysql2": "~2.2.0",
"n8n-core": "~0.67.0", "n8n-core": "~0.68.0",
"n8n-editor-ui": "~0.84.0", "n8n-editor-ui": "~0.87.0",
"n8n-nodes-base": "~0.111.0", "n8n-nodes-base": "~0.114.0",
"n8n-workflow": "~0.55.0", "n8n-workflow": "~0.56.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"open": "^7.0.0", "open": "^7.0.0",
"pg": "^8.3.0", "pg": "^8.3.0",
@ -117,7 +116,7 @@
"request-promise-native": "^1.0.7", "request-promise-native": "^1.0.7",
"sqlite3": "^5.0.1", "sqlite3": "^5.0.1",
"sse-channel": "^3.1.1", "sse-channel": "^3.1.1",
"tslib": "1.11.2", "tslib": "1.13.0",
"typeorm": "^0.2.30" "typeorm": "^0.2.30"
}, },
"jest": { "jest": {

View file

@ -20,7 +20,6 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
IDataObject,
IExecuteData, IExecuteData,
IGetExecutePollFunctions, IGetExecutePollFunctions,
IGetExecuteTriggerFunctions, IGetExecuteTriggerFunctions,
@ -283,23 +282,11 @@ export class ActiveWorkflowRunner {
const node = workflow.getNode(webhookData.node) as INode; const node = workflow.getNode(webhookData.node) as INode;
node.name = webhookData.node; node.name = webhookData.node;
path = node.parameters.path as string; path = webhookData.path;
if (node.parameters.path === undefined) {
path = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['path'], mode) as string | undefined;
if (path === undefined) {
// TODO: Use a proper logger
console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflow.id}".`);
continue;
}
}
const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['isFullPath'], mode, false) as boolean;
const webhook = { const webhook = {
workflowId: webhookData.workflowId, workflowId: webhookData.workflowId,
webhookPath: NodeHelpers.getNodeWebhookPath(workflow.id as string, node, path, isFullPath), webhookPath: path,
node: node.name, node: node.name,
method: webhookData.httpMethod, method: webhookData.httpMethod,
} as IWebhookDb; } as IWebhookDb;
@ -317,7 +304,6 @@ export class ActiveWorkflowRunner {
} }
try { try {
await Db.collections.Webhook?.insert(webhook); await Db.collections.Webhook?.insert(webhook);
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, false); const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, false);

View file

@ -1,24 +1,24 @@
import { import {
ExecutionError,
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject,
ICredentialsDecrypted, ICredentialsDecrypted,
ICredentialsEncrypted, ICredentialsEncrypted,
ICredentialType, ICredentialType,
IDataObject, IDataObject,
IExecutionError,
IRun, IRun,
IRunData, IRunData,
IRunExecutionData, IRunExecutionData,
ITaskData, ITaskData,
IWorkflowBase as IWorkflowBaseWorkflow, IWorkflowBase as IWorkflowBaseWorkflow,
IWorkflowCredentials, IWorkflowCredentials,
Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
IDeferredPromise, IDeferredPromise, WorkflowExecute,
} from 'n8n-core'; } from 'n8n-core';
import * as PCancelable from 'p-cancelable'; import * as PCancelable from 'p-cancelable';
import { ObjectID, Repository } from 'typeorm'; import { ObjectID, Repository } from 'typeorm';
@ -374,10 +374,10 @@ export interface ITransferNodeTypes {
export interface IWorkflowErrorData { export interface IWorkflowErrorData {
[key: string]: IDataObject | string | number | IExecutionError; [key: string]: IDataObject | string | number | ExecutionError;
execution: { execution: {
id?: string; id?: string;
error: IExecutionError; error: ExecutionError;
lastNodeExecuted: string; lastNodeExecuted: string;
mode: WorkflowExecuteMode; mode: WorkflowExecuteMode;
}; };
@ -411,3 +411,9 @@ export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExe
executionId: string; executionId: string;
nodeTypeData: ITransferNodeTypes; nodeTypeData: ITransferNodeTypes;
} }
export interface IWorkflowExecuteProcess {
startedAt: Date;
workflow: Workflow;
workflowExecute: WorkflowExecute;
}

View file

@ -22,7 +22,7 @@ import { RequestOptions } from 'oauth-1.0a';
import * as csrf from 'csrf'; import * as csrf from 'csrf';
import * as requestPromise from 'request-promise-native'; import * as requestPromise from 'request-promise-native';
import { createHmac } from 'crypto'; import { createHmac } from 'crypto';
import { compare } from 'bcryptjs'; import { compare } from '@node-rs/bcrypt';
import * as promClient from 'prom-client'; import * as promClient from 'prom-client';
import { import {
@ -132,6 +132,7 @@ class App {
protocol: string; protocol: string;
sslKey: string; sslKey: string;
sslCert: string; sslCert: string;
payloadSizeMax: number;
presetCredentialsLoaded: boolean; presetCredentialsLoaded: boolean;
@ -145,6 +146,7 @@ class App {
this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean; this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean;
this.executionTimeout = config.get('executions.timeout') as number; this.executionTimeout = config.get('executions.timeout') as number;
this.maxExecutionTimeout = config.get('executions.maxTimeout') as number; this.maxExecutionTimeout = config.get('executions.maxTimeout') as number;
this.payloadSizeMax = config.get('endpoints.payloadSizeMax') as number;
this.timezone = config.get('generic.timezone') as string; this.timezone = config.get('generic.timezone') as string;
this.restEndpoint = config.get('endpoints.rest') as string; this.restEndpoint = config.get('endpoints.rest') as string;
@ -369,7 +371,7 @@ class App {
// Support application/json type post data // Support application/json type post data
this.app.use(bodyParser.json({ this.app.use(bodyParser.json({
limit: '16mb', verify: (req, res, buf) => { limit: this.payloadSizeMax + 'mb', verify: (req, res, buf) => {
// @ts-ignore // @ts-ignore
req.rawBody = buf; req.rawBody = buf;
}, },
@ -378,7 +380,7 @@ class App {
// Support application/xml type post data // Support application/xml type post data
// @ts-ignore // @ts-ignore
this.app.use(bodyParser.xml({ this.app.use(bodyParser.xml({
limit: '16mb', xmlParseOptions: { limit: this.payloadSizeMax + 'mb', xmlParseOptions: {
normalize: true, // Trim whitespace inside text nodes normalize: true, // Trim whitespace inside text nodes
normalizeTags: true, // Transform tags to lowercase normalizeTags: true, // Transform tags to lowercase
explicitArray: false, // Only put properties in array if length > 1 explicitArray: false, // Only put properties in array if length > 1
@ -386,7 +388,7 @@ class App {
})); }));
this.app.use(bodyParser.text({ this.app.use(bodyParser.text({
limit: '16mb', verify: (req, res, buf) => { limit: this.payloadSizeMax + 'mb', verify: (req, res, buf) => {
// @ts-ignore // @ts-ignore
req.rawBody = buf; req.rawBody = buf;
}, },

View file

@ -144,7 +144,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
try { try {
webhookResultData = await workflow.runWebhook(webhookData, workflowStartNode, additionalData, NodeExecuteFunctions, executionMode); webhookResultData = await workflow.runWebhook(webhookData, workflowStartNode, additionalData, NodeExecuteFunctions, executionMode);
} catch (e) { } catch (err) {
// Send error response to webhook caller // Send error response to webhook caller
const errorMessage = 'Workflow Webhook Error: Workflow could not be started!'; const errorMessage = 'Workflow Webhook Error: Workflow could not be started!';
responseCallback(new Error(errorMessage), {}); responseCallback(new Error(errorMessage), {});
@ -156,8 +156,9 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
runData: {}, runData: {},
lastNodeExecuted: workflowStartNode.name, lastNodeExecuted: workflowStartNode.name,
error: { error: {
message: e.message, ...err,
stack: e.stack, message: err.message,
stack: err.stack,
}, },
}, },
}; };

View file

@ -8,6 +8,7 @@ import {
IExecutionResponse, IExecutionResponse,
IPushDataExecutionFinished, IPushDataExecutionFinished,
IWorkflowBase, IWorkflowBase,
IWorkflowExecuteProcess,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
NodeTypes, NodeTypes,
Push, Push,
@ -569,7 +570,7 @@ export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promi
* @param {INodeExecutionData[]} [inputData] * @param {INodeExecutionData[]} [inputData]
* @returns {(Promise<Array<INodeExecutionData[] | null>>)} * @returns {(Promise<Array<INodeExecutionData[] | null>>)}
*/ */
export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[], parentExecutionId?: string, loadedWorkflowData?: IWorkflowBase, loadedRunData?: IWorkflowExecutionDataProcess): Promise<Array<INodeExecutionData[] | null> | IRun> { export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[], parentExecutionId?: string, loadedWorkflowData?: IWorkflowBase, loadedRunData?: IWorkflowExecutionDataProcess): Promise<Array<INodeExecutionData[] | null> | IWorkflowExecuteProcess> {
const externalHooks = ExternalHooks(); const externalHooks = ExternalHooks();
await externalHooks.init(); await externalHooks.init();
@ -605,10 +606,19 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
// This one already contains changes to talk to parent process // This one already contains changes to talk to parent process
// and get executionID from `activeExecutions` running on main process // and get executionID from `activeExecutions` running on main process
additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow; additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow;
additionalDataIntegrated.executionTimeoutTimestamp = additionalData.executionTimeoutTimestamp;
// Execute the workflow // Execute the workflow
const workflowExecute = new WorkflowExecute(additionalDataIntegrated, runData.executionMode, runExecutionData); const workflowExecute = new WorkflowExecute(additionalDataIntegrated, runData.executionMode, runExecutionData);
if (parentExecutionId !== undefined) {
// Must be changed to become typed
return {
startedAt: new Date(),
workflow,
workflowExecute,
};
}
const data = await workflowExecute.processRunExecutionData(workflow); const data = await workflowExecute.processRunExecutionData(workflow);
await externalHooks.run('workflow.postExecute', [data, workflowData]); await externalHooks.run('workflow.postExecute', [data, workflowData]);
@ -616,20 +626,17 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
if (data.finished === true) { if (data.finished === true) {
// Workflow did finish successfully // Workflow did finish successfully
if (parentExecutionId !== undefined) { await ActiveExecutions.getInstance().remove(executionId, data);
return data; const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
} else { return returnData!.data!.main;
await ActiveExecutions.getInstance().remove(executionId, data);
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
return returnData!.data!.main;
}
} else { } else {
await ActiveExecutions.getInstance().remove(executionId, data); await ActiveExecutions.getInstance().remove(executionId, data);
// Workflow did fail // Workflow did fail
const error = new Error(data.data.resultData.error!.message); const { error } = data.data.resultData;
error.stack = data.data.resultData.error!.stack; throw {
throw error; ...error,
stack: error!.stack,
};
} }
} }
@ -642,7 +649,7 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
* @param {INodeParameters} currentNodeParameters * @param {INodeParameters} currentNodeParameters
* @returns {Promise<IWorkflowExecuteAdditionalData>} * @returns {Promise<IWorkflowExecuteAdditionalData>}
*/ */
export async function getBase(credentials: IWorkflowCredentials, currentNodeParameters?: INodeParameters): Promise<IWorkflowExecuteAdditionalData> { export async function getBase(credentials: IWorkflowCredentials, currentNodeParameters?: INodeParameters, executionTimeoutTimestamp?: number): Promise<IWorkflowExecuteAdditionalData> {
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
const timezone = config.get('generic.timezone') as string; const timezone = config.get('generic.timezone') as string;
@ -664,6 +671,7 @@ export async function getBase(credentials: IWorkflowCredentials, currentNodePara
webhookBaseUrl, webhookBaseUrl,
webhookTestBaseUrl, webhookTestBaseUrl,
currentNodeParameters, currentNodeParameters,
executionTimeoutTimestamp,
}; };
} }

View file

@ -27,12 +27,12 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
IDataObject, ExecutionError,
IExecutionError,
IRun, IRun,
Workflow, Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
WorkflowHooks, WorkflowHooks,
WorkflowOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as config from '../config'; import * as config from '../config';
@ -78,13 +78,13 @@ export class WorkflowRunner {
/** /**
* The process did error * The process did error
* *
* @param {IExecutionError} error * @param {ExecutionError} error
* @param {Date} startedAt * @param {Date} startedAt
* @param {WorkflowExecuteMode} executionMode * @param {WorkflowExecuteMode} executionMode
* @param {string} executionId * @param {string} executionId
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
processError(error: IExecutionError, startedAt: Date, executionMode: WorkflowExecuteMode, executionId: string) { processError(error: ExecutionError, startedAt: Date, executionMode: WorkflowExecuteMode, executionId: string) {
const fullRunData: IRun = { const fullRunData: IRun = {
data: { data: {
resultData: { resultData: {
@ -158,8 +158,22 @@ export class WorkflowRunner {
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
// Soft timeout to stop workflow execution after current running node
// Changes were made by adding the `workflowTimeout` to the `additionalData`
// So that the timeout will also work for executions with nested workflows.
let executionTimeout: NodeJS.Timeout;
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
workflowTimeout = data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
}
if (workflowTimeout > 0) {
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number);
}
const workflow = new Workflow({ id: data.workflowData.id as string | undefined, name: data.workflowData.name, nodes: data.workflowData!.nodes, connections: data.workflowData!.connections, active: data.workflowData!.active, nodeTypes, staticData: data.workflowData!.staticData }); const workflow = new Workflow({ id: data.workflowData.id as string | undefined, name: data.workflowData.name, nodes: data.workflowData!.nodes, connections: data.workflowData!.connections, active: data.workflowData!.active, nodeTypes, staticData: data.workflowData!.staticData });
const additionalData = await WorkflowExecuteAdditionalData.getBase(data.credentials); const additionalData = await WorkflowExecuteAdditionalData.getBase(data.credentials, undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000);
// Register the active execution // Register the active execution
const executionId = await this.activeExecutions.add(data, undefined); const executionId = await this.activeExecutions.add(data, undefined);
@ -184,14 +198,7 @@ export class WorkflowRunner {
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution); this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
// Soft timeout to stop workflow execution after current running node if (workflowTimeout > 0) {
let executionTimeout: NodeJS.Timeout;
let workflowTimeout = config.get('executions.timeout') as number > 0 && config.get('executions.timeout') as number; // initialize with default
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
workflowTimeout = data.workflowData.settings!.executionTimeout as number > 0 && data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
}
if (workflowTimeout) {
const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
executionTimeout = setTimeout(() => { executionTimeout = setTimeout(() => {
this.activeExecutions.stopExecution(executionId, 'timeout'); this.activeExecutions.stopExecution(executionId, 'timeout');
@ -250,9 +257,7 @@ export class WorkflowRunner {
const fullRunData :IRun = { const fullRunData :IRun = {
data: { data: {
resultData: { resultData: {
error: { error: new WorkflowOperationError('Workflow has been canceled!'),
message: 'Workflow has been canceled!',
} as IExecutionError,
runData: {}, runData: {},
}, },
}, },
@ -282,7 +287,6 @@ export class WorkflowRunner {
* the database. * * the database. *
*************************************************/ *************************************************/
let watchDogInterval: NodeJS.Timeout | undefined; let watchDogInterval: NodeJS.Timeout | undefined;
let resolved = false;
const watchDog = new Promise((res) => { const watchDog = new Promise((res) => {
watchDogInterval = setInterval(async () => { watchDogInterval = setInterval(async () => {
@ -303,28 +307,9 @@ export class WorkflowRunner {
} }
}; };
await new Promise((res, rej) => { await Promise.race([jobData, watchDog]);
jobData.then((data) => { clearWatchdogInterval();
if (!resolved) {
resolved = true;
clearWatchdogInterval();
res(data);
}
}).catch((e) => {
if(!resolved) {
resolved = true;
clearWatchdogInterval();
rej(e);
}
});
watchDog.then((data) => {
if (!resolved) {
resolved = true;
clearWatchdogInterval();
res(data);
}
});
});
} else { } else {
await jobData; await jobData;
} }
@ -385,7 +370,7 @@ export class WorkflowRunner {
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> { async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
const startedAt = new Date(); let startedAt = new Date();
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js')); const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
if (loadStaticData === true && data.workflowData.id) { if (loadStaticData === true && data.workflowData.id) {
@ -428,7 +413,6 @@ export class WorkflowRunner {
} }
} }
(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId; (data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData; (data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = credentialsOverwrites; (data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = credentialsOverwrites;
@ -441,64 +425,93 @@ export class WorkflowRunner {
// Start timeout for the execution // Start timeout for the execution
let executionTimeout: NodeJS.Timeout; let executionTimeout: NodeJS.Timeout;
let workflowTimeout = config.get('executions.timeout') as number > 0 && config.get('executions.timeout') as number; // initialize with default let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) { if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
workflowTimeout = data.workflowData.settings!.executionTimeout as number > 0 && data.workflowData.settings!.executionTimeout as number; // preference on workflow setting workflowTimeout = data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
} }
if (workflowTimeout) { const processTimeoutFunction = (timeout: number) => {
const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds this.activeExecutions.stopExecution(executionId, 'timeout');
executionTimeout = setTimeout(() => { executionTimeout = setTimeout(() => subprocess.kill(), Math.max(timeout * 0.2, 5000)); // minimum 5 seconds
this.activeExecutions.stopExecution(executionId, 'timeout'); };
executionTimeout = setTimeout(() => subprocess.kill(), Math.max(timeout * 0.2, 5000)); // minimum 5 seconds if (workflowTimeout > 0) {
}, timeout); workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
// Start timeout already now but give process at least 5 seconds to start.
// Without it could would it be possible that the workflow executions times out before it even got started if
// the timeout time is very short as the process start time can be quite long.
executionTimeout = setTimeout(processTimeoutFunction, Math.max(5000, workflowTimeout), workflowTimeout);
} }
// Create a list of child spawned executions
// If after the child process exits we have
// outstanding executions, we remove them
const childExecutionIds: string[] = [];
// Listen to data from the subprocess // Listen to data from the subprocess
subprocess.on('message', async (message: IProcessMessage) => { subprocess.on('message', async (message: IProcessMessage) => {
if (message.type === 'end') { if (message.type === 'start') {
// Now that the execution actually started set the timeout again so that does not time out to early.
startedAt = new Date();
if (workflowTimeout > 0) {
clearTimeout(executionTimeout);
executionTimeout = setTimeout(processTimeoutFunction, workflowTimeout, workflowTimeout);
}
} else if (message.type === 'end') {
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
this.activeExecutions.remove(executionId!, message.data.runData); this.activeExecutions.remove(executionId!, message.data.runData);
} else if (message.type === 'processError') { } else if (message.type === 'processError') {
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
const executionError = message.data.executionError as IExecutionError; const executionError = message.data.executionError as ExecutionError;
this.processError(executionError, startedAt, data.executionMode, executionId); this.processError(executionError, startedAt, data.executionMode, executionId);
} else if (message.type === 'processHook') { } else if (message.type === 'processHook') {
this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook); this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook);
} else if (message.type === 'timeout') { } else if (message.type === 'timeout') {
// Execution timed out and its process has been terminated // Execution timed out and its process has been terminated
const timeoutError = { message: 'Workflow execution timed out!' } as IExecutionError; const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
this.processError(timeoutError, startedAt, data.executionMode, executionId); this.processError(timeoutError, startedAt, data.executionMode, executionId);
} else if (message.type === 'startExecution') { } else if (message.type === 'startExecution') {
const executionId = await this.activeExecutions.add(message.data.runData); const executionId = await this.activeExecutions.add(message.data.runData);
childExecutionIds.push(executionId);
subprocess.send({ type: 'executionId', data: {executionId} } as IProcessMessage); subprocess.send({ type: 'executionId', data: {executionId} } as IProcessMessage);
} else if (message.type === 'finishExecution') { } else if (message.type === 'finishExecution') {
const executionIdIndex = childExecutionIds.indexOf(message.data.executionId);
if (executionIdIndex !== -1) {
childExecutionIds.splice(executionIdIndex, 1);
}
await this.activeExecutions.remove(message.data.executionId, message.data.result); await this.activeExecutions.remove(message.data.executionId, message.data.result);
} }
}); });
// Also get informed when the processes does exit especially when it did crash or timed out // Also get informed when the processes does exit especially when it did crash or timed out
subprocess.on('exit', (code, signal) => { subprocess.on('exit', async (code, signal) => {
if (signal === 'SIGTERM'){ if (signal === 'SIGTERM'){
// Execution timed out and its process has been terminated // Execution timed out and its process has been terminated
const timeoutError = { const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
message: 'Workflow execution timed out!',
} as IExecutionError;
this.processError(timeoutError, startedAt, data.executionMode, executionId); this.processError(timeoutError, startedAt, data.executionMode, executionId);
} else if (code !== 0) { } else if (code !== 0) {
// Process did exit with error code, so something went wrong. // Process did exit with error code, so something went wrong.
const executionError = { const executionError = new WorkflowOperationError('Workflow execution process did crash for an unknown reason!');
message: 'Workflow execution process did crash for an unknown reason!',
} as IExecutionError;
this.processError(executionError, startedAt, data.executionMode, executionId); this.processError(executionError, startedAt, data.executionMode, executionId);
} }
for(const executionId of childExecutionIds) {
// When the child process exits, if we still have
// pending child executions, we mark them as finished
// They will display as unknown to the user
// Instead of pending forever as executing when it
// actually isn't anymore.
await this.activeExecutions.remove(executionId);
}
clearTimeout(executionTimeout); clearTimeout(executionTimeout);
}); });

View file

@ -4,6 +4,7 @@ import {
CredentialTypes, CredentialTypes,
Db, Db,
ExternalHooks, ExternalHooks,
IWorkflowExecuteProcess,
IWorkflowExecutionDataProcessWithExecution, IWorkflowExecutionDataProcessWithExecution,
NodeTypes, NodeTypes,
WorkflowExecuteAdditionalData, WorkflowExecuteAdditionalData,
@ -16,10 +17,9 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
ExecutionError,
IDataObject, IDataObject,
IExecuteData,
IExecuteWorkflowInfo, IExecuteWorkflowInfo,
IExecutionError,
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeData, INodeTypeData,
@ -30,6 +30,7 @@ import {
IWorkflowExecuteHooks, IWorkflowExecuteHooks,
Workflow, Workflow,
WorkflowHooks, WorkflowHooks,
WorkflowOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as config from '../config'; import * as config from '../config';
@ -40,6 +41,9 @@ export class WorkflowRunnerProcess {
workflow: Workflow | undefined; workflow: Workflow | undefined;
workflowExecute: WorkflowExecute | undefined; workflowExecute: WorkflowExecute | undefined;
executionIdCallback: (executionId: string) => void | undefined; executionIdCallback: (executionId: string) => void | undefined;
childExecutions: {
[key: string]: IWorkflowExecuteProcess,
} = {};
static async stopProcess() { static async stopProcess() {
setTimeout(() => { setTimeout(() => {
@ -107,8 +111,18 @@ export class WorkflowRunnerProcess {
await Db.init(); await Db.init();
} }
// Start timeout for the execution
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
if (this.data.workflowData.settings && this.data.workflowData.settings.executionTimeout) {
workflowTimeout = this.data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
}
if (workflowTimeout > 0) {
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number);
}
this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings }); this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings });
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials); const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials, undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000);
additionalData.hooks = this.getProcessForwardHooks(); additionalData.hooks = this.getProcessForwardHooks();
const executeWorkflowFunction = additionalData.executeWorkflow; const executeWorkflowFunction = additionalData.executeWorkflow;
@ -123,15 +137,21 @@ export class WorkflowRunnerProcess {
}); });
let result: IRun; let result: IRun;
try { try {
result = await executeWorkflowFunction(workflowInfo, additionalData, inputData, executionId, workflowData, runData); const executeWorkflowFunctionOutput = await executeWorkflowFunction(workflowInfo, additionalData, inputData, executionId, workflowData, runData) as {workflowExecute: WorkflowExecute, workflow: Workflow} as IWorkflowExecuteProcess;
const workflowExecute = executeWorkflowFunctionOutput.workflowExecute;
this.childExecutions[executionId] = executeWorkflowFunctionOutput;
const workflow = executeWorkflowFunctionOutput.workflow;
result = await workflowExecute.processRunExecutionData(workflow) as IRun;
await externalHooks.run('workflow.postExecute', [result, workflowData]);
await sendToParentProcess('finishExecution', { executionId, result });
delete this.childExecutions[executionId];
} catch (e) { } catch (e) {
await sendToParentProcess('finishExecution', { executionId }); await sendToParentProcess('finishExecution', { executionId });
delete this.childExecutions[executionId];
// Throw same error we had // Throw same error we had
throw e; throw e;
} }
await sendToParentProcess('finishExecution', { executionId, result });
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(result); const returnData = WorkflowHelpers.getDataLastExecutedNodeData(result);
return returnData!.data!.main; return returnData!.data!.main;
}; };
@ -254,6 +274,8 @@ const workflowRunner = new WorkflowRunnerProcess();
process.on('message', async (message: IProcessMessage) => { process.on('message', async (message: IProcessMessage) => {
try { try {
if (message.type === 'startWorkflow') { if (message.type === 'startWorkflow') {
await sendToParentProcess('start', {});
const runData = await workflowRunner.runWorkflow(message.data); const runData = await workflowRunner.runWorkflow(message.data);
await sendToParentProcess('end', { await sendToParentProcess('end', {
@ -267,10 +289,22 @@ process.on('message', async (message: IProcessMessage) => {
let runData: IRun; let runData: IRun;
if (workflowRunner.workflowExecute !== undefined) { if (workflowRunner.workflowExecute !== undefined) {
const executionIds = Object.keys(workflowRunner.childExecutions);
for (const executionId of executionIds) {
const childWorkflowExecute = workflowRunner.childExecutions[executionId];
runData = childWorkflowExecute.workflowExecute.getFullRunData(workflowRunner.childExecutions[executionId].startedAt);
const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : undefined;
// If there is any data send it to parent process, if execution timedout add the error
await childWorkflowExecute.workflowExecute.processSuccessExecution(workflowRunner.childExecutions[executionId].startedAt, childWorkflowExecute.workflow, timeOutError);
}
// Workflow started already executing // Workflow started already executing
runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt); runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt);
const timeOutError = message.type === 'timeout' ? { message: 'Workflow execution timed out!' } as IExecutionError : undefined; const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : undefined;
// If there is any data send it to parent process, if execution timedout add the error // If there is any data send it to parent process, if execution timedout add the error
await workflowRunner.workflowExecute.processSuccessExecution(workflowRunner.startedAt, workflowRunner.workflow!, timeOutError); await workflowRunner.workflowExecute.processSuccessExecution(workflowRunner.startedAt, workflowRunner.workflow!, timeOutError);
@ -301,11 +335,14 @@ process.on('message', async (message: IProcessMessage) => {
workflowRunner.executionIdCallback(message.data.executionId); workflowRunner.executionIdCallback(message.data.executionId);
} }
} catch (error) { } catch (error) {
// Catch all uncaught errors and forward them to parent process // Catch all uncaught errors and forward them to parent process
const executionError = { const executionError = {
message: error.message, ...error,
stack: error.stack, name: error!.name || 'Error',
} as IExecutionError; message: error!.message,
stack: error!.stack,
} as ExecutionError;
await sendToParentProcess('processError', { await sendToParentProcess('processError', {
executionError, executionError,

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-core", "name": "n8n-core",
"version": "0.67.0", "version": "0.68.0",
"description": "Core functionality of n8n", "description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -32,7 +32,7 @@
"@types/jest": "^26.0.13", "@types/jest": "^26.0.13",
"@types/lodash.get": "^4.4.6", "@types/lodash.get": "^4.4.6",
"@types/mime-types": "^2.1.0", "@types/mime-types": "^2.1.0",
"@types/node": "14.0.27", "@types/node": "^14.14.40",
"@types/request-promise-native": "~1.0.15", "@types/request-promise-native": "~1.0.15",
"jest": "^26.4.2", "jest": "^26.4.2",
"source-map-support": "^0.5.9", "source-map-support": "^0.5.9",
@ -47,7 +47,7 @@
"file-type": "^14.6.2", "file-type": "^14.6.2",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"n8n-workflow": "~0.55.0", "n8n-workflow": "~0.56.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"request": "^2.88.2", "request": "^2.88.2",

View file

@ -18,7 +18,6 @@ import {
IWorkflowSettings as IWorkflowSettingsWorkflow, IWorkflowSettings as IWorkflowSettingsWorkflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { OptionsWithUri, OptionsWithUrl } from 'request'; import { OptionsWithUri, OptionsWithUrl } from 'request';
import * as requestPromise from 'request-promise-native'; import * as requestPromise from 'request-promise-native';
@ -26,7 +25,6 @@ interface Constructable<T> {
new(): T; new(): T;
} }
export interface IProcessMessage { export interface IProcessMessage {
data?: any; // tslint:disable-line:no-any data?: any; // tslint:disable-line:no-any
type: string; type: string;

View file

@ -32,6 +32,7 @@ import {
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
IWorkflowMetadata, IWorkflowMetadata,
NodeHelpers, NodeHelpers,
NodeOperationError,
NodeParameterValue, NodeParameterValue,
Workflow, Workflow,
WorkflowActivateMode, WorkflowActivateMode,
@ -51,6 +52,9 @@ import { createHmac } from 'crypto';
import { fromBuffer } from 'file-type'; import { fromBuffer } from 'file-type';
import { lookup } from 'mime-types'; import { lookup } from 'mime-types';
const requestPromiseWithDefaults = requestPromise.defaults({
timeout: 300000, // 5 minutes
});
/** /**
* Takes a buffer and converts it into the format n8n uses. It encodes the binary data as * Takes a buffer and converts it into the format n8n uses. It encodes the binary data as
@ -309,16 +313,16 @@ export function getCredentials(workflow: Workflow, node: INode, type: string, ad
// Get the NodeType as it has the information if the credentials are required // Get the NodeType as it has the information if the credentials are required
const nodeType = workflow.nodeTypes.getByName(node.type); const nodeType = workflow.nodeTypes.getByName(node.type);
if (nodeType === undefined) { if (nodeType === undefined) {
throw new Error(`Node type "${node.type}" is not known so can not get credentials!`); throw new NodeOperationError(node, `Node type "${node.type}" is not known so can not get credentials!`);
} }
if (nodeType.description.credentials === undefined) { if (nodeType.description.credentials === undefined) {
throw new Error(`Node type "${node.type}" does not have any credentials defined!`); throw new NodeOperationError(node, `Node type "${node.type}" does not have any credentials defined!`);
} }
const nodeCredentialDescription = nodeType.description.credentials.find((credentialTypeDescription) => credentialTypeDescription.name === type); const nodeCredentialDescription = nodeType.description.credentials.find((credentialTypeDescription) => credentialTypeDescription.name === type);
if (nodeCredentialDescription === undefined) { if (nodeCredentialDescription === undefined) {
throw new Error(`Node type "${node.type}" does not have any credentials of type "${type}" defined!`); throw new NodeOperationError(node, `Node type "${node.type}" does not have any credentials of type "${type}" defined!`);
} }
if (NodeHelpers.displayParameter(additionalData.currentNodeParameters || node.parameters, nodeCredentialDescription, node.parameters) === false) { if (NodeHelpers.displayParameter(additionalData.currentNodeParameters || node.parameters, nodeCredentialDescription, node.parameters) === false) {
@ -333,10 +337,10 @@ export function getCredentials(workflow: Workflow, node: INode, type: string, ad
if (nodeCredentialDescription.required === true) { if (nodeCredentialDescription.required === true) {
// Credentials are required so error // Credentials are required so error
if (!node.credentials) { if (!node.credentials) {
throw new Error('Node does not have any credentials set!'); throw new NodeOperationError(node,'Node does not have any credentials set!');
} }
if (!node.credentials[type]) { if (!node.credentials[type]) {
throw new Error(`Node does not have any credentials set for "${type}"!`); throw new NodeOperationError(node,`Node does not have any credentials set for "${type}"!`);
} }
} else { } else {
// Credentials are not required so resolve with undefined // Credentials are not required so resolve with undefined
@ -576,7 +580,7 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio
}, },
helpers: { helpers: {
prepareBinaryData, prepareBinaryData,
request: requestPromise, request: requestPromiseWithDefaults,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options);
}, },
@ -642,7 +646,7 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi
}, },
helpers: { helpers: {
prepareBinaryData, prepareBinaryData,
request: requestPromise, request: requestPromiseWithDefaults,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options);
}, },
@ -738,7 +742,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
prepareOutputData: NodeHelpers.prepareOutputData, prepareOutputData: NodeHelpers.prepareOutputData,
helpers: { helpers: {
prepareBinaryData, prepareBinaryData,
request: requestPromise, request: requestPromiseWithDefaults,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options);
}, },
@ -836,7 +840,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
}, },
helpers: { helpers: {
prepareBinaryData, prepareBinaryData,
request: requestPromise, request: requestPromiseWithDefaults,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options);
}, },
@ -892,7 +896,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio
return additionalData.restApiUrl; return additionalData.restApiUrl;
}, },
helpers: { helpers: {
request: requestPromise, request: requestPromiseWithDefaults,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options);
}, },
@ -962,7 +966,7 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio
return workflow.getStaticData(type, node); return workflow.getStaticData(type, node);
}, },
helpers: { helpers: {
request: requestPromise, request: requestPromiseWithDefaults,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options);
}, },
@ -1062,7 +1066,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi
prepareOutputData: NodeHelpers.prepareOutputData, prepareOutputData: NodeHelpers.prepareOutputData,
helpers: { helpers: {
prepareBinaryData, prepareBinaryData,
request: requestPromise, request: requestPromiseWithDefaults,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options);
}, },

View file

@ -1,10 +1,10 @@
import * as PCancelable from 'p-cancelable'; import * as PCancelable from 'p-cancelable';
import { import {
ExecutionError,
IConnection, IConnection,
IDataObject, IDataObject,
IExecuteData, IExecuteData,
IExecutionError,
INode, INode,
INodeConnections, INodeConnections,
INodeExecutionData, INodeExecutionData,
@ -17,6 +17,7 @@ import {
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
Workflow, Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
WorkflowOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
NodeExecuteFunctions, NodeExecuteFunctions,
@ -490,7 +491,7 @@ export class WorkflowExecute {
// Variables which hold temporary data for each node-execution // Variables which hold temporary data for each node-execution
let executionData: IExecuteData; let executionData: IExecuteData;
let executionError: IExecutionError | undefined; let executionError: ExecutionError | undefined;
let executionNode: INode; let executionNode: INode;
let nodeSuccessData: INodeExecutionData[][] | null | undefined; let nodeSuccessData: INodeExecutionData[][] | null | undefined;
let runIndex: number; let runIndex: number;
@ -517,8 +518,10 @@ export class WorkflowExecute {
try { try {
await this.executeHook('workflowExecuteBefore', [workflow]); await this.executeHook('workflowExecuteBefore', [workflow]);
} catch (error) { } catch (error) {
// Set the error that it can be saved correctly // Set the error that it can be saved correctly
executionError = { executionError = {
...error,
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
}; };
@ -547,6 +550,10 @@ export class WorkflowExecute {
executionLoop: executionLoop:
while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) { while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) {
if (this.additionalData.executionTimeoutTimestamp !== undefined && Date.now() >= this.additionalData.executionTimeoutTimestamp) {
gotCancel = true;
}
// @ts-ignore // @ts-ignore
if (gotCancel === true) { if (gotCancel === true) {
return Promise.resolve(); return Promise.resolve();
@ -683,9 +690,11 @@ export class WorkflowExecute {
break; break;
} catch (error) { } catch (error) {
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name; this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
executionError = { executionError = {
...error,
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
}; };
@ -784,7 +793,7 @@ export class WorkflowExecute {
})() })()
.then(async () => { .then(async () => {
if (gotCancel && executionError === undefined) { if (gotCancel && executionError === undefined) {
return this.processSuccessExecution(startedAt, workflow, { message: 'Workflow has been canceled!' } as IExecutionError); return this.processSuccessExecution(startedAt, workflow, new WorkflowOperationError('Workflow has been canceled!'));
} }
return this.processSuccessExecution(startedAt, workflow, executionError); return this.processSuccessExecution(startedAt, workflow, executionError);
}) })
@ -792,6 +801,7 @@ export class WorkflowExecute {
const fullRunData = this.getFullRunData(startedAt); const fullRunData = this.getFullRunData(startedAt);
fullRunData.data.resultData.error = { fullRunData.data.resultData.error = {
...error,
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
}; };
@ -815,7 +825,7 @@ export class WorkflowExecute {
// @ts-ignore // @ts-ignore
async processSuccessExecution(startedAt: Date, workflow: Workflow, executionError?: IExecutionError): PCancelable<IRun> { async processSuccessExecution(startedAt: Date, workflow: Workflow, executionError?: ExecutionError): PCancelable<IRun> {
const fullRunData = this.getFullRunData(startedAt); const fullRunData = this.getFullRunData(startedAt);
if (executionError !== undefined) { if (executionError !== undefined) {

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-editor-ui", "name": "n8n-editor-ui",
"version": "0.84.0", "version": "0.87.0",
"description": "Workflow Editor UI for n8n", "description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -36,7 +36,7 @@
"@types/jest": "^26.0.13", "@types/jest": "^26.0.13",
"@types/lodash.get": "^4.4.6", "@types/lodash.get": "^4.4.6",
"@types/lodash.set": "^4.3.6", "@types/lodash.set": "^4.3.6",
"@types/node": "14.0.27", "@types/node": "^14.14.40",
"@types/quill": "^2.0.1", "@types/quill": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^2.13.0", "@typescript-eslint/eslint-plugin": "^2.13.0",
"@typescript-eslint/parser": "^2.13.0", "@typescript-eslint/parser": "^2.13.0",
@ -65,7 +65,7 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"n8n-workflow": "~0.55.0", "n8n-workflow": "~0.56.0",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"normalize-wheel": "^1.0.1", "normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1", "prismjs": "^1.17.1",

View file

@ -428,3 +428,20 @@ export interface ITimeoutHMS {
} }
export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR'; export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR';
export type MenuItemType = 'link';
export type MenuItemPosition = 'top' | 'bottom';
export interface IMenuItem {
id: string;
type: MenuItemType;
position: MenuItemPosition;
properties: ILinkMenuItemProperties;
}
export interface ILinkMenuItemProperties {
title: string;
icon: string;
href: string;
newWindow?: boolean;
}

View file

@ -30,7 +30,7 @@
Credential type: Credential type:
</el-col> </el-col>
<el-col :span="18"> <el-col :span="18">
<el-select v-model="credentialType" filterable placeholder="Select Type" size="small"> <el-select v-model="credentialType" filterable placeholder="Select Type" size="small" ref="credentialsDropdown">
<el-option <el-option
v-for="item in credentialTypes" v-for="item in credentialTypes"
:key="item.name" :key="item.name"
@ -198,6 +198,9 @@ export default mixins(
this.credentialData = currentCredentials; this.credentialData = currentCredentials;
} else { } else {
Vue.nextTick(() => {
(this.$refs.credentialsDropdown as HTMLDivElement).focus();
});
if (this.credentialType || this.setCredentialType) { if (this.credentialType || this.setCredentialType) {
const credentialType = this.$store.getters.credentialType(this.credentialType || this.setCredentialType); const credentialType = this.$store.getters.credentialType(this.credentialType || this.setCredentialType);
if (credentialType === null) { if (credentialType === null) {

View file

@ -0,0 +1,177 @@
<template>
<div>
<div class="error-header">
<div class="error-message">ERROR: {{error.message}}</div>
<div class="error-description" v-if="error.description">{{error.description}}</div>
</div>
<details>
<summary class="error-details__summary">
<font-awesome-icon class="error-details__icon" icon="angle-right" /> Details
</summary>
<div class="error-details__content">
<div v-if="error.timestamp">
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix box-card__title">
<span>Time</span>
</div>
<div>
{{new Date(error.timestamp).toLocaleString()}}
</div>
</el-card>
</div>
<div v-if="error.httpCode">
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix box-card__title">
<span>HTTP-Code</span>
</div>
<div>
{{error.httpCode}}
</div>
</el-card>
</div>
<div v-if="error.cause">
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix box-card__title">
<span>Cause</span>
<br>
<span class="box-card__subtitle">Data below may contain sensitive information. Proceed with caution when sharing.</span>
</div>
<div>
<el-button class="copy-button" @click="copyCause" circle type="text" title="Copy to clipboard">
<font-awesome-icon icon="copy" />
</el-button>
<vue-json-pretty
:data="error.cause"
:deep="3"
:showLength="true"
selectableType="single"
path="error"
class="json-data"
/>
</div>
</el-card>
</div>
<div v-if="error.stack">
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix box-card__title">
<span>Stack</span>
</div>
<div>
<pre><code>{{error.stack}}</code></pre>
</div>
</el-card>
</div>
</div>
</details>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
//@ts-ignore
import VueJsonPretty from 'vue-json-pretty';
import { copyPaste } from '@/components/mixins/copyPaste';
import { showMessage } from '@/components/mixins/showMessage';
import mixins from 'vue-typed-mixins';
export default mixins(
copyPaste,
showMessage,
).extend({
name: 'NodeErrorView',
props: [
'error',
],
components: {
VueJsonPretty,
},
methods: {
copyCause() {
this.copyToClipboard(JSON.stringify(this.error.cause));
this.copySuccess();
},
copySuccess() {
this.$showMessage({
title: 'Copied to clipboard',
message: '',
type: 'info',
});
},
},
});
</script>
<style lang="scss">
.error-header {
margin-bottom: 10px;
}
.error-message {
color: #ff0000;
font-weight: bold;
font-size: 1.1rem;
}
.error-description {
margin-top: 10px;
font-size: 1rem;
}
.error-details__summary {
font-weight: 600;
font-size: 16px;
cursor: pointer;
outline:none;
}
.error-details__icon {
margin-right: 4px;
}
details > summary {
list-style-type: none;
}
details > summary::-webkit-details-marker {
display: none;
}
details[open] {
.error-details__icon {
transform: rotate(90deg);
}
}
.error-details__content {
margin-top: 15px;
}
.el-divider__text {
background-color: #f9f9f9;
}
.box-card {
margin-top: 1em;
overflow: auto;
}
.box-card__title {
font-weight: 400;
}
.box-card__subtitle {
font-weight: 200;
font-style: italic;
font-size: 0.7rem;
}
.copy-button {
position: absolute;
font-size: 1.1rem;
right: 50px;
z-index: 1000;
}
</style>

View file

@ -21,6 +21,22 @@
</a> </a>
</el-menu-item> </el-menu-item>
<el-menu-item
v-for="item in sidebarMenuTopItems"
:key="item.id"
:index="item.id"
>
<a
v-if="item.type === 'link'"
:href="item.properties.href"
:target="item.properties.newWindow ? '_blank' : '_self'"
class="primary-item"
>
<font-awesome-icon :icon="item.properties.icon" />
<span slot="title" class="item-title-root">{{ item.properties.title }}</span>
</a>
</el-menu-item>
<el-submenu index="workflow" title="Workflow"> <el-submenu index="workflow" title="Workflow">
<template slot="title"> <template slot="title">
<font-awesome-icon icon="network-wired"/>&nbsp; <font-awesome-icon icon="network-wired"/>&nbsp;
@ -152,6 +168,22 @@
</el-menu-item> </el-menu-item>
</el-submenu> </el-submenu>
<el-menu-item
v-for="item in sidebarMenuBottomItems"
:key="item.id"
:index="item.id"
>
<a
v-if="item.type === 'link'"
:href="item.properties.href"
:target="item.properties.newWindow ? '_blank' : '_self'"
class="primary-item"
>
<font-awesome-icon :icon="item.properties.icon" />
<span slot="title" class="item-title-root">{{ item.properties.title }}</span>
</a>
</el-menu-item>
</el-menu> </el-menu>
</div> </div>
@ -167,6 +199,7 @@ import {
IExecutionResponse, IExecutionResponse,
IExecutionsStopData, IExecutionsStopData,
IWorkflowDataUpdate, IWorkflowDataUpdate,
IMenuItem,
} from '../Interface'; } from '../Interface';
import About from '@/components/About.vue'; import About from '@/components/About.vue';
@ -266,6 +299,12 @@ export default mixins(
workflowRunning (): boolean { workflowRunning (): boolean {
return this.$store.getters.isActionActive('workflowRunning'); return this.$store.getters.isActionActive('workflowRunning');
}, },
sidebarMenuTopItems(): IMenuItem[] {
return this.$store.getters.sidebarMenuItems.filter((item: IMenuItem) => item.position === 'top');
},
sidebarMenuBottomItems(): IMenuItem[] {
return this.$store.getters.sidebarMenuItems.filter((item: IMenuItem) => item.position === 'bottom');
},
}, },
methods: { methods: {
clearExecutionData () { clearExecutionData () {
@ -533,6 +572,11 @@ export default mixins(
.el-menu-item { .el-menu-item {
a { a {
color: #666; color: #666;
&.primary-item {
color: $--color-primary;
vertical-align: baseline;
}
} }
&.logo-item { &.logo-item {

View file

@ -81,8 +81,7 @@
<div class="data-display-content"> <div class="data-display-content">
<span v-if="node && workflowRunData !== null && workflowRunData.hasOwnProperty(node.name)"> <span v-if="node && workflowRunData !== null && workflowRunData.hasOwnProperty(node.name)">
<div v-if="workflowRunData[node.name][runIndex].error" class="error-display"> <div v-if="workflowRunData[node.name][runIndex].error" class="error-display">
<div class="error-message">ERROR: {{workflowRunData[node.name][runIndex].error.message}}</div> <NodeErrorView :error="workflowRunData[node.name][runIndex].error" />
<pre><code>{{workflowRunData[node.name][runIndex].error.stack}}</code></pre>
</div> </div>
<span v-else> <span v-else>
<div v-if="showData === false" class="to-much-data"> <div v-if="showData === false" class="to-much-data">
@ -226,6 +225,7 @@ import {
} from '@/constants'; } from '@/constants';
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue'; import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
import NodeErrorView from '@/components/Error/NodeViewError.vue';
import { copyPaste } from '@/components/mixins/copyPaste'; import { copyPaste } from '@/components/mixins/copyPaste';
import { genericHelpers } from '@/components/mixins/genericHelpers'; import { genericHelpers } from '@/components/mixins/genericHelpers';
@ -247,6 +247,7 @@ export default mixins(
name: 'RunData', name: 'RunData',
components: { components: {
BinaryDataDisplay, BinaryDataDisplay,
NodeErrorView,
VueJsonPretty, VueJsonPretty,
}, },
data () { data () {
@ -739,13 +740,6 @@ export default mixins(
} }
} }
.error-display {
.error-message {
color: #ff0000;
font-weight: bold;
}
}
table { table {
border-collapse: collapse; border-collapse: collapse;
text-align: left; text-align: left;

View file

@ -207,8 +207,19 @@ export const pushConnection = mixins(
if (runDataExecuted.finished !== true) { if (runDataExecuted.finished !== true) {
// There was a problem with executing the workflow // There was a problem with executing the workflow
let errorMessage = 'There was a problem executing the workflow!'; let errorMessage = 'There was a problem executing the workflow!';
if (runDataExecuted.data.resultData.error && runDataExecuted.data.resultData.error.message) { if (runDataExecuted.data.resultData.error && runDataExecuted.data.resultData.error.message) {
errorMessage = `There was a problem executing the workflow:<br /><strong>"${runDataExecuted.data.resultData.error.message}"</strong>`; let nodeName: string | undefined;
if (runDataExecuted.data.resultData.error.node) {
nodeName = typeof runDataExecuted.data.resultData.error.node === 'string'
? runDataExecuted.data.resultData.error.node
: runDataExecuted.data.resultData.error.node.name;
}
const receivedError = nodeName
? `${nodeName}: ${runDataExecuted.data.resultData.error.message}`
: runDataExecuted.data.resultData.error.message;
errorMessage = `There was a problem executing the workflow:<br /><strong>"${receivedError}"</strong>`;
} }
this.$titleSet(workflow.name, 'ERROR'); this.$titleSet(workflow.name, 'ERROR');
this.$showMessage({ this.$showMessage({

View file

@ -56,6 +56,7 @@ import {
faFilePdf, faFilePdf,
faFolderOpen, faFolderOpen,
faHdd, faHdd,
faHome,
faHourglass, faHourglass,
faImage, faImage,
faInbox, faInbox,
@ -136,6 +137,7 @@ library.add(faFileImport);
library.add(faFilePdf); library.add(faFilePdf);
library.add(faFolderOpen); library.add(faFolderOpen);
library.add(faHdd); library.add(faHdd);
library.add(faHome);
library.add(faHourglass); library.add(faHourglass);
library.add(faImage); library.add(faImage);
library.add(faInbox); library.add(faInbox);

View file

@ -459,6 +459,10 @@ h1, h2, h3, h4, h5, h6 {
border: none; border: none;
} }
.el-notification__content {
text-align: left;
}
// Custom scrollbar // Custom scrollbar
::-webkit-scrollbar { ::-webkit-scrollbar {

View file

@ -21,12 +21,13 @@ import {
ICredentialsResponse, ICredentialsResponse,
IExecutionResponse, IExecutionResponse,
IExecutionsCurrentSummaryExtended, IExecutionsCurrentSummaryExtended,
IPushDataExecutionFinished, IMenuItem,
IPushDataNodeExecuteAfter,
IWorkflowDb,
INodeUi, INodeUi,
INodeUpdatePropertiesInformation, INodeUpdatePropertiesInformation,
IPushDataExecutionFinished,
IPushDataNodeExecuteAfter,
IUpdateInformation, IUpdateInformation,
IWorkflowDb,
XYPositon, XYPositon,
} from './Interface'; } from './Interface';
@ -79,6 +80,7 @@ export const store = new Vuex.Store({
nodes: [] as INodeUi[], nodes: [] as INodeUi[],
settings: {} as IWorkflowSettings, settings: {} as IWorkflowSettings,
} as IWorkflowDb, } as IWorkflowDb,
sidebarMenuItems: [] as IMenuItem[],
}, },
mutations: { mutations: {
// Active Actions // Active Actions
@ -597,6 +599,11 @@ export const store = new Vuex.Store({
Vue.set(state, 'nodeTypes', updatedNodes); Vue.set(state, 'nodeTypes', updatedNodes);
state.nodeTypes = updatedNodes; state.nodeTypes = updatedNodes;
}, },
addSidebarMenuItems (state, menuItems: IMenuItem[]) {
const updated = state.sidebarMenuItems.concat(menuItems);
Vue.set(state, 'sidebarMenuItems', updated);
},
}, },
getters: { getters: {
@ -834,6 +841,9 @@ export const store = new Vuex.Store({
return workflowRunData[nodeName]; return workflowRunData[nodeName];
}, },
sidebarMenuItems: (state): IMenuItem[] => {
return state.sidebarMenuItems;
},
}, },
}); });

View file

@ -55,12 +55,12 @@
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
"@oclif/errors": "^1.2.2", "@oclif/errors": "^1.2.2",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/node": "14.0.27", "@types/node": "^14.14.40",
"change-case": "^4.1.1", "change-case": "^4.1.1",
"copyfiles": "^2.1.1", "copyfiles": "^2.1.1",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"n8n-core": "^0.48.0", "n8n-core": "^0.67.0",
"n8n-workflow": "^0.42.0", "n8n-workflow": "^0.55.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0", "replace-in-file": "^6.0.0",
"request": "^2.88.2", "request": "^2.88.2",

View file

@ -0,0 +1,25 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'https://www.googleapis.com/auth/bigquery',
];
export class GoogleBigQueryOAuth2Api implements ICredentialType {
name = 'googleBigQueryOAuth2Api';
extends = [
'googleOAuth2Api',
];
displayName = 'Google BigQuery OAuth2 API';
documentationUrl = 'google';
properties = [
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(' '),
},
];
}

View file

@ -0,0 +1,18 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MailcheckApi implements ICredentialType {
name = 'mailcheckApi';
displayName = 'Mailcheck API';
documentationUrl = 'mailcheck';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -0,0 +1,52 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'boards:write',
'boards:read',
];
export class MondayComOAuth2Api implements ICredentialType {
name = 'mondayComOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Monday.com OAuth2 API';
documentationUrl = 'monday';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://auth.monday.com/oauth2/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://auth.monday.com/oauth2/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(' '),
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'body',
},
];
}

View file

@ -49,5 +49,62 @@ export class MySql implements ICredentialType {
default: 10000, default: 10000,
description: 'The milliseconds before a timeout occurs during the initial connection to the MySQL server.', description: 'The milliseconds before a timeout occurs during the initial connection to the MySQL server.',
}, },
{
displayName: 'SSL',
name: 'ssl',
type: 'boolean' as NodePropertyTypes,
default: false,
},
{
displayName: 'CA Certificate',
name: 'caCertificate',
typeOptions: {
alwaysOpenEditWindow: true,
password: true,
},
displayOptions: {
show: {
ssl: [
true,
],
},
},
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Client Private Key',
name: 'clientPrivateKey',
typeOptions: {
alwaysOpenEditWindow: true,
password: true,
},
displayOptions: {
show: {
ssl: [
true,
],
},
},
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Client Certificate',
name: 'clientCertificate',
typeOptions: {
alwaysOpenEditWindow: true,
password: true,
},
displayOptions: {
show: {
ssl: [
true,
],
},
},
type: 'string' as NodePropertyTypes,
default: '',
},
]; ];
} }

View file

@ -1,5 +1,6 @@
import { import {
ICredentialType, ICredentialType,
IDisplayOptions,
NodePropertyTypes, NodePropertyTypes,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -51,8 +52,22 @@ export class RabbitMQ implements ICredentialType {
default: false, default: false,
}, },
{ {
displayName: 'Client Certificate', displayName: 'Passwordless',
name: 'cert', name: 'passwordless',
type: 'boolean' as NodePropertyTypes,
displayOptions: {
show: {
ssl: [
true,
],
},
},
default: true,
description: 'Passwordless connection with certificates (SASL mechanism EXTERNAL)',
},
{
displayName: 'CA Certificates',
name: 'ca',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
typeOptions: { typeOptions: {
password: true, password: true,
@ -65,6 +80,26 @@ export class RabbitMQ implements ICredentialType {
}, },
}, },
default: '', default: '',
description: 'SSL CA Certificates to use.',
},
{
displayName: 'Client Certificate',
name: 'cert',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
displayOptions: {
show: {
ssl: [
true,
],
passwordless: [
true,
],
},
} as IDisplayOptions,
default: '',
description: 'SSL Client Certificate to use.', description: 'SSL Client Certificate to use.',
}, },
{ {
@ -79,6 +114,9 @@ export class RabbitMQ implements ICredentialType {
ssl: [ ssl: [
true, true,
], ],
passwordless: [
true,
],
}, },
}, },
default: '', default: '',
@ -96,31 +134,13 @@ export class RabbitMQ implements ICredentialType {
ssl: [ ssl: [
true, true,
], ],
}, passwordless: [
},
default: '',
description: 'SSL passphrase to use.',
},
{
displayName: 'CA Certificates',
name: 'ca',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
// typeOptions: {
// multipleValues: true,
// multipleValueButtonText: 'Add Certificate',
// },
displayOptions: {
show: {
ssl: [
true, true,
], ],
}, },
}, },
default: '', default: '',
description: 'SSL CA Certificates to use.', description: 'SSL passphrase to use.',
}, },
// { // {
// displayName: 'Client ID', // displayName: 'Client ID',

View file

@ -5,7 +5,7 @@ import {
export class SentryIoServerApi implements ICredentialType { export class SentryIoServerApi implements ICredentialType {
name = 'sentryIoServerApi'; name = 'sentryIoServerApi';
displayName = 'Sentry.io API'; displayName = 'Sentry.io Server API';
documentationUrl = 'sentryIo'; documentationUrl = 'sentryIo';
properties = [ properties = [
{ {

View file

@ -3,7 +3,6 @@ import {
NodePropertyTypes, NodePropertyTypes,
} from 'n8n-workflow'; } from 'n8n-workflow';
export class WebflowOAuth2Api implements ICredentialType { export class WebflowOAuth2Api implements ICredentialType {
name = 'webflowOAuth2Api'; name = 'webflowOAuth2Api';
extends = [ extends = [

View file

@ -0,0 +1,15 @@
{
"node": "n8n-nodes-base.activationTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Core Nodes"
],
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.activationTrigger/"
}
]
}
}

View file

@ -1,73 +0,0 @@
import { ITriggerFunctions } from 'n8n-core';
import {
INodeType,
INodeTypeDescription,
ITriggerResponse,
} from 'n8n-workflow';
export class ActivationTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Activation Trigger',
name: 'activationTrigger',
icon: 'fa:play-circle',
group: ['trigger'],
version: 1,
description: 'Executes whenever the workflow becomes active.',
defaults: {
name: 'Activation Trigger',
color: '#00e000',
},
inputs: [],
outputs: ['main'],
properties: [
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
required: true,
default: [],
description: 'Specifies under which conditions an execution should happen:<br />' +
'- <b>Activation</b>: Workflow gets activated<br />' +
'- <b>Update</b>: Workflow gets saved while active<br>' +
'- <b>Start</b>: n8n starts or restarts',
options: [
{
name: 'Activation',
value: 'activate',
description: 'Run when workflow gets activated',
},
{
name: 'Start',
value: 'init',
description: 'Run when n8n starts or restarts',
},
{
name: 'Update',
value: 'update',
description: 'Run when workflow gets saved while it is active',
},
],
},
],
};
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
const events = this.getNodeParameter('events', []) as string[];
const activationMode = this.getActivationMode();
if (events.includes(activationMode)) {
this.emit([this.helpers.returnJsonArray([{ activation: activationMode }])]);
}
const self = this;
async function manualTriggerFunction() {
self.emit([self.helpers.returnJsonArray([{ activation: 'manual' }])]);
}
return {
manualTriggerFunction,
};
}
}

View file

@ -2,6 +2,7 @@
"node": "n8n-nodes-base.activeCampaign", "node": "n8n-nodes-base.activeCampaign",
"nodeVersion": "1.0", "nodeVersion": "1.0",
"codexVersion": "1.0", "codexVersion": "1.0",
"details": "ActiveCampaign is a cloud software platform that allows customer experience automation, which combines email marketing, marketing automation, sales automation, and CRM categories. Use this node when you want to interact with your ActiveCampaign account.",
"categories": [ "categories": [
"Marketing & Content" "Marketing & Content"
], ],

View file

@ -9,6 +9,7 @@ import {
INodePropertyOptions, INodePropertyOptions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -431,7 +432,7 @@ export class ActiveCampaign implements INodeType {
addAdditionalFields(body.contact as IDataObject, updateFields); addAdditionalFields(body.contact as IDataObject, updateFields);
} else { } else {
throw new Error(`The operation "${operation}" is not known`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known`);
} }
} else if (resource === 'account') { } else if (resource === 'account') {
if (operation === 'create') { if (operation === 'create') {
@ -512,7 +513,7 @@ export class ActiveCampaign implements INodeType {
addAdditionalFields(body.account as IDataObject, updateFields); addAdditionalFields(body.account as IDataObject, updateFields);
} else { } else {
throw new Error(`The operation "${operation}" is not known`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known`);
} }
} else if (resource === 'accountContact') { } else if (resource === 'accountContact') {
if (operation === 'create') { if (operation === 'create') {
@ -562,7 +563,7 @@ export class ActiveCampaign implements INodeType {
endpoint = `/api/3/accountContacts/${accountContactId}`; endpoint = `/api/3/accountContacts/${accountContactId}`;
} else { } else {
throw new Error(`The operation "${operation}" is not known`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known`);
} }
} else if (resource === 'contactTag') { } else if (resource === 'contactTag') {
if (operation === 'add') { if (operation === 'add') {
@ -592,7 +593,7 @@ export class ActiveCampaign implements INodeType {
endpoint = `/api/3/contactTags/${contactTagId}`; endpoint = `/api/3/contactTags/${contactTagId}`;
} else { } else {
throw new Error(`The operation "${operation}" is not known`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known`);
} }
} else if (resource === 'contactList') { } else if (resource === 'contactList') {
if (operation === 'add') { if (operation === 'add') {
@ -630,7 +631,7 @@ export class ActiveCampaign implements INodeType {
dataKey = 'contacts'; dataKey = 'contacts';
} else { } else {
throw new Error(`The operation "${operation}" is not known`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known`);
} }
} else if (resource === 'list') { } else if (resource === 'list') {
if (operation === 'getAll') { if (operation === 'getAll') {
@ -732,7 +733,7 @@ export class ActiveCampaign implements INodeType {
addAdditionalFields(body.tag as IDataObject, updateFields); addAdditionalFields(body.tag as IDataObject, updateFields);
} else { } else {
throw new Error(`The operation "${operation}" is not known`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known`);
} }
} else if (resource === 'deal') { } else if (resource === 'deal') {
if (operation === 'create') { if (operation === 'create') {
@ -851,7 +852,7 @@ export class ActiveCampaign implements INodeType {
endpoint = `/api/3/deals/${dealId}/notes/${dealNoteId}`; endpoint = `/api/3/deals/${dealId}/notes/${dealNoteId}`;
} else { } else {
throw new Error(`The operation "${operation}" is not known`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known`);
} }
} else if (resource === 'connection') { } else if (resource === 'connection') {
if (operation === 'create') { if (operation === 'create') {
@ -926,7 +927,7 @@ export class ActiveCampaign implements INodeType {
endpoint = `/api/3/connections`; endpoint = `/api/3/connections`;
} else { } else {
throw new Error(`The operation "${operation}" is not known`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known`);
} }
} else if (resource === 'ecommerceOrder') { } else if (resource === 'ecommerceOrder') {
if (operation === 'create') { if (operation === 'create') {
@ -1024,7 +1025,7 @@ export class ActiveCampaign implements INodeType {
endpoint = `/api/3/ecomOrders`; endpoint = `/api/3/ecomOrders`;
} else { } else {
throw new Error(`The operation "${operation}" is not known`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known`);
} }
} else if (resource === 'ecommerceCustomer') { } else if (resource === 'ecommerceCustomer') {
if (operation === 'create') { if (operation === 'create') {
@ -1114,7 +1115,7 @@ export class ActiveCampaign implements INodeType {
endpoint = `/api/3/ecomCustomers`; endpoint = `/api/3/ecomCustomers`;
} else { } else {
throw new Error(`The operation "${operation}" is not known`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known`);
} }
} else if (resource === 'ecommerceOrderProducts') { } else if (resource === 'ecommerceOrderProducts') {
if (operation === 'getByProductId') { if (operation === 'getByProductId') {
@ -1160,11 +1161,11 @@ export class ActiveCampaign implements INodeType {
endpoint = `/api/3/ecomOrderProducts`; endpoint = `/api/3/ecomOrderProducts`;
} else { } else {
throw new Error(`The operation "${operation}" is not known`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known`);
} }
} else { } else {
throw new Error(`The resource "${resource}" is not known!`); throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known!`);
} }
let responseData; let responseData;

View file

@ -116,7 +116,7 @@ export class ActiveCampaignTrigger implements INodeType {
const endpoint = `/api/3/webhooks/${webhookData.webhookId}`; const endpoint = `/api/3/webhooks/${webhookData.webhookId}`;
try { try {
await activeCampaignApiRequest.call(this, 'GET', endpoint, {}); await activeCampaignApiRequest.call(this, 'GET', endpoint, {});
} catch (e) { } catch (error) {
return false; return false;
} }
return true; return true;

View file

@ -4,7 +4,7 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
IDataObject, ILoadOptionsFunctions, INodeProperties, IDataObject, ILoadOptionsFunctions, INodeProperties, NodeApiError, NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { OptionsWithUri } from 'request'; import { OptionsWithUri } from 'request';
@ -28,7 +28,7 @@ export interface IProduct {
export async function activeCampaignApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject, dataKey?: string): Promise<any> { // tslint:disable-line:no-any export async function activeCampaignApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject, dataKey?: string): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('activeCampaignApi'); const credentials = this.getCredentials('activeCampaignApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
if (query === undefined) { if (query === undefined) {
@ -53,7 +53,7 @@ export async function activeCampaignApiRequest(this: IHookFunctions | IExecuteFu
const responseData = await this.helpers.request!(options); const responseData = await this.helpers.request!(options);
if (responseData.success === false) { if (responseData.success === false) {
throw new Error(`ActiveCampaign error response: ${responseData.error} (${responseData.error_info})`); throw new NodeApiError(this.getNode(), responseData);
} }
if (dataKey === undefined) { if (dataKey === undefined) {
@ -63,13 +63,7 @@ export async function activeCampaignApiRequest(this: IHookFunctions | IExecuteFu
} }
} catch (error) { } catch (error) {
if (error.statusCode === 403) { throw new NodeApiError(this.getNode(), error);
// Return a clear error
throw new Error('The ActiveCampaign credentials are not valid!');
}
// If that data does not exist for some reason return the actual error
throw error;
} }
} }

View file

@ -6,7 +6,7 @@ import {
ILoadOptionsFunctions, ILoadOptionsFunctions,
IWebhookFunctions, IWebhookFunctions,
} from 'n8n-core'; } from 'n8n-core';
import { IDataObject } from 'n8n-workflow'; import { IDataObject, NodeApiError, NodeOperationError, } from 'n8n-workflow';
export async function acuitySchedulingApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any export async function acuitySchedulingApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const authenticationMethod = this.getNodeParameter('authentication', 0); const authenticationMethod = this.getNodeParameter('authentication', 0);
@ -27,7 +27,7 @@ export async function acuitySchedulingApiRequest(this: IHookFunctions | IExecute
if (authenticationMethod === 'apiKey') { if (authenticationMethod === 'apiKey') {
const credentials = this.getCredentials('acuitySchedulingApi'); const credentials = this.getCredentials('acuitySchedulingApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
options.auth = { options.auth = {
@ -42,6 +42,6 @@ export async function acuitySchedulingApiRequest(this: IHookFunctions | IExecute
return await this.helpers.requestOAuth2!.call(this, 'acuitySchedulingOAuth2Api', options, true); return await this.helpers.requestOAuth2!.call(this, 'acuitySchedulingOAuth2Api', options, true);
} }
} catch (error) { } catch (error) {
throw new Error('Acuity Scheduling Error: ' + error.message); throw new NodeApiError(this.getNode(), error);
} }
} }

View file

@ -8,6 +8,7 @@ import {
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
IWebhookResponseData, IWebhookResponseData,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -187,7 +188,7 @@ export class AffinityTrigger implements INodeType {
const webhookUrl = this.getNodeWebhookUrl('default') as string; const webhookUrl = this.getNodeWebhookUrl('default') as string;
if (webhookUrl.includes('%20')) { if (webhookUrl.includes('%20')) {
throw new Error('The name of the Affinity Trigger Node is not allowed to contain any spaces!'); throw new NodeOperationError(this.getNode(), 'The name of the Affinity Trigger Node is not allowed to contain any spaces!');
} }
const events = this.getNodeParameter('events') as string[]; const events = this.getNodeParameter('events') as string[];

View file

@ -12,6 +12,8 @@ import {
IDataObject, IDataObject,
IHookFunctions, IHookFunctions,
IWebhookFunctions, IWebhookFunctions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
export async function affinityApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any export async function affinityApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
@ -19,7 +21,7 @@ export async function affinityApiRequest(this: IExecuteFunctions | IWebhookFunct
const credentials = this.getCredentials('affinityApi'); const credentials = this.getCredentials('affinityApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
const apiKey = `:${credentials.apiKey}`; const apiKey = `:${credentials.apiKey}`;
@ -47,11 +49,7 @@ export async function affinityApiRequest(this: IExecuteFunctions | IWebhookFunct
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
if (error.response) { throw new NodeApiError(this.getNode(), error);
const errorMessage = error.response.body.message || error.response.body.description || error.message;
throw new Error(`Affinity error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;
} }
} }

View file

@ -3,7 +3,8 @@ import {
IDataObject, IDataObject,
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -149,7 +150,7 @@ export class AgileCrm implements INodeType {
Object.assign(body, JSON.parse(additionalFieldsJson)); Object.assign(body, JSON.parse(additionalFieldsJson));
} else { } else {
throw new Error('Additional fields must be a valid JSON'); throw new NodeOperationError(this.getNode(), 'Additional fields must be a valid JSON');
} }
} }
@ -305,7 +306,7 @@ export class AgileCrm implements INodeType {
Object.assign(body, JSON.parse(additionalFieldsJson)); Object.assign(body, JSON.parse(additionalFieldsJson));
} else { } else {
throw new Error('Additional fields must be a valid JSON'); throw new NodeOperationError(this.getNode(), 'Additional fields must be a valid JSON');
} }
} }
} else { } else {
@ -483,7 +484,7 @@ export class AgileCrm implements INodeType {
if (validateJSON(additionalFieldsJson) !== undefined) { if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson)); Object.assign(body, JSON.parse(additionalFieldsJson));
} else { } else {
throw new Error('Additional fields must be a valid JSON'); throw new NodeOperationError(this.getNode(), 'Additional fields must be a valid JSON');
} }
} }
@ -525,7 +526,7 @@ export class AgileCrm implements INodeType {
Object.assign(body, JSON.parse(additionalFieldsJson)); Object.assign(body, JSON.parse(additionalFieldsJson));
} else { } else {
throw new Error('Additional fields must be valid JSON'); throw new NodeOperationError(this.getNode(), 'Additional fields must be valid JSON');
} }
} }

View file

@ -10,7 +10,7 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
IDataObject, IDataObject, NodeApiError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { IContactUpdate } from './ContactInterface'; import { IContactUpdate } from './ContactInterface';
@ -39,7 +39,7 @@ export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunction
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
throw new Error(`AgileCRM error response: ${error.message}`); throw new NodeApiError(this.getNode(), error);
} }
} }
@ -114,9 +114,9 @@ export async function agileCrmApiRequestUpdate(this: IHookFunctions | IExecuteFu
} catch (error) { } catch (error) {
if (successfulUpdates.length === 0) { if (successfulUpdates.length === 0) {
throw new Error(`AgileCRM error response: ${error.message}`); throw new NodeApiError(this.getNode(), error);
} else { } else {
throw new Error(`Not all properties updated. Updated properties: ${successfulUpdates.join(', ')} \n \nAgileCRM error response: ${error.message}`); throw new NodeApiError(this.getNode(), error, { message: `Not all properties updated. Updated properties: ${successfulUpdates.join(', ')}`, description: error.message, httpCode: error.statusCode });
} }
} }

View file

@ -17,9 +17,50 @@
} }
], ],
"generic": [ "generic": [
{
"label": "2021 Goals: Level Up Your Vocabulary With Vonage and n8n",
"icon": "🎯",
"url": "https://n8n.io/blog/2021-goals-level-up-your-vocabulary-with-vonage-and-n8n/"
},
{
"label": "2021: The Year to Automate the New You with n8n",
"icon": "☀️",
"url": "https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/"
},
{
"label": "Building an expense tracking app in 10 minutes",
"icon": "📱",
"url": "https://n8n.io/blog/building-an-expense-tracking-app-in-10-minutes/"
},
{
"label": "Why this Product Manager loves workflow automation with n8n",
"icon": "🧠",
"url": "https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/"
},
{
"label": "Learn to Build Powerful API Endpoints Using Webhooks",
"icon": "🧰",
"url": "https://n8n.io/blog/learn-to-build-powerful-api-endpoints-using-webhooks/"
},
{
"label": "Sending SMS the Low-Code Way with Airtable, Twilio Programmable SMS, and n8n",
"icon": "📱",
"url": "https://n8n.io/blog/sending-sms-the-low-code-way-with-airtable-twilio-programmable-sms-and-n8n/"
},
{ {
"label": "Automating Conference Organization Processes with n8n", "label": "Automating Conference Organization Processes with n8n",
"url": "https://medium.com/n8n-io/automating-conference-organization-processes-with-n8n-ab8f64a7a520" "icon": "🙋‍♀️",
"url": "https://n8n.io/blog/automating-conference-organization-processes-with-n8n/"
},
{
"label": "Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin",
"icon": "🎖",
"url": "https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/"
},
{
"label": "How Goomer automated their operations with over 200 n8n workflows",
"icon": "🛵",
"url": "https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/"
} }
] ]
} }

View file

@ -7,6 +7,7 @@ import {
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -636,7 +637,7 @@ export class Airtable implements INodeType {
} }
} else { } else {
throw new Error(`The operation "${operation}" is not known!`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`);
} }
return [this.helpers.returnJsonArray(returnData)]; return [this.helpers.returnJsonArray(returnData)];

View file

@ -7,6 +7,7 @@ import {
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -170,7 +171,7 @@ export class AirtableTrigger implements INodeType {
if (Array.isArray(records) && records.length) { if (Array.isArray(records) && records.length) {
if (this.getMode() === 'manual' && records[0].fields[triggerField] === undefined) { if (this.getMode() === 'manual' && records[0].fields[triggerField] === undefined) {
throw new Error(`The Field "${triggerField}" does not exist.`); throw new NodeOperationError(this.getNode(), `The Field "${triggerField}" does not exist.`);
} }
if (downloadAttachments === true) { if (downloadAttachments === true) {

View file

@ -13,6 +13,8 @@ import {
IDataObject, IDataObject,
INodeExecutionData, INodeExecutionData,
IPollFunctions, IPollFunctions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -41,7 +43,7 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa
const credentials = this.getCredentials('airtableApi'); const credentials = this.getCredentials('airtableApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
query = query || {}; query = query || {};
@ -73,23 +75,7 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
if (error.statusCode === 401) { throw new NodeApiError(this.getNode(), error);
// Return a clear error
throw new Error('The Airtable credentials are not valid!');
}
if (error.response && error.response.body && error.response.body.error) {
// Try to return the error prettier
const airtableError = error.response.body.error;
if (airtableError.type && airtableError.message) {
throw new Error(`Airtable error response [${airtableError.type}]: ${airtableError.message}`);
}
}
// Expected error data did not get returned so rhow the actual error
throw error;
} }
} }

View file

@ -1,21 +1 @@
<?xml version="1.0" encoding="utf-8"?> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 170"><path d="M89 4.8L16.2 34.9c-4.1 1.7-4 7.4.1 9.1l73.2 29c6.4 2.6 13.6 2.6 20 0l73.2-29c4.1-1.6 4.1-7.4.1-9.1l-73-30.1C103.2 2 95.7 2 89 4.8" fill="#fcb400"/><path d="M105.9 88.9v72.5c0 3.4 3.5 5.8 6.7 4.5l81.6-31.7c1.9-.7 3.1-2.5 3.1-4.5V57.2c0-3.4-3.5-5.8-6.7-4.5L109 84.3c-1.9.8-3.1 2.6-3.1 4.6" fill="#18bfff"/><path d="M86.9 92.6l-24.2 11.7-2.5 1.2L9.1 130c-3.2 1.6-7.4-.8-7.4-4.4V57.5c0-1.3.7-2.4 1.6-3.3.4-.4.8-.7 1.2-.9 1.2-.7 3-.9 4.4-.3l77.5 30.7c4 1.5 4.3 7.1.5 8.9" fill="#f82b60"/><path d="M86.9 92.6l-24.2 11.7-59.4-50c.4-.4.8-.7 1.2-.9 1.2-.7 3-.9 4.4-.3l77.5 30.7c4 1.4 4.3 7 .5 8.8" fill="#ba1e45"/></svg>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 200 170" style="enable-background:new 0 0 200 170;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FCB400;}
.st1{fill:#18BFFF;}
.st2{fill:#F82B60;}
.st3{fill:#BA1E45;}
</style>
<g>
<path class="st0" d="M89,4.8L16.2,34.9c-4.1,1.7-4,7.4,0.1,9.1l73.2,29c6.4,2.6,13.6,2.6,20,0l73.2-29c4.1-1.6,4.1-7.4,0.1-9.1
L109.8,4.8C103.2,2,95.7,2,89,4.8"/>
<path class="st1" d="M105.9,88.9v72.5c0,3.4,3.5,5.8,6.7,4.5l81.6-31.7c1.9-0.7,3.1-2.5,3.1-4.5V57.2c0-3.4-3.5-5.8-6.7-4.5
L109,84.3C107.1,85.1,105.9,86.9,105.9,88.9"/>
<path class="st2" d="M86.9,92.6l-24.2,11.7l-2.5,1.2L9.1,130c-3.2,1.6-7.4-0.8-7.4-4.4V57.5c0-1.3,0.7-2.4,1.6-3.3
c0.4-0.4,0.8-0.7,1.2-0.9c1.2-0.7,3-0.9,4.4-0.3l77.5,30.7C90.4,85.2,90.7,90.8,86.9,92.6"/>
<path class="st3" d="M86.9,92.6l-24.2,11.7L3.3,54.3c0.4-0.4,0.8-0.7,1.2-0.9c1.2-0.7,3-0.9,4.4-0.3l77.5,30.7
C90.4,85.2,90.7,90.8,86.9,92.6"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 682 B

View file

@ -19,9 +19,9 @@
], ],
"generic": [ "generic": [
{ {
"label": "Smart Factory Automation using IoT and Sensor Data with n8n", "label": "Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide",
"icon": "🏭", "icon": "🏭",
"url": "https://medium.com/n8n-io/smart-factory-automation-using-iot-and-sensor-data-with-n8n-27675de9943b" "url": "https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/"
} }
] ]
} }

View file

@ -11,6 +11,7 @@ import {
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
export class Amqp implements INodeType { export class Amqp implements INodeType {
@ -98,7 +99,7 @@ export class Amqp implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const credentials = this.getCredentials('amqp'); const credentials = this.getCredentials('amqp');
if (!credentials) { if (!credentials) {
throw new Error('Credentials are mandatory!'); throw new NodeOperationError(this.getNode(), 'Credentials are mandatory!');
} }
const sink = this.getNodeParameter('sink', 0, '') as string; const sink = this.getNodeParameter('sink', 0, '') as string;
@ -116,7 +117,7 @@ export class Amqp implements INodeType {
} }
if (sink === '') { if (sink === '') {
throw new Error('Queue or Topic required!'); throw new NodeOperationError(this.getNode(), 'Queue or Topic required!');
} }
const container = create_container(); const container = create_container();

View file

@ -19,9 +19,9 @@
], ],
"generic": [ "generic": [
{ {
"label": "Smart Factory Automation using IoT and Sensor Data with n8n", "label": "Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide",
"icon": "🏭", "icon": "🏭",
"url": "https://medium.com/n8n-io/smart-factory-automation-using-iot-and-sensor-data-with-n8n-27675de9943b" "url": "https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/"
} }
] ]
} }

View file

@ -12,6 +12,7 @@ import {
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
ITriggerResponse, ITriggerResponse,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -133,7 +134,7 @@ export class AmqpTrigger implements INodeType {
const credentials = this.getCredentials('amqp'); const credentials = this.getCredentials('amqp');
if (!credentials) { if (!credentials) {
throw new Error('Credentials are mandatory!'); throw new NodeOperationError(this.getNode(), 'Credentials are mandatory!');
} }
const sink = this.getNodeParameter('sink', '') as string; const sink = this.getNodeParameter('sink', '') as string;
@ -146,7 +147,7 @@ export class AmqpTrigger implements INodeType {
const containerReconnectLimit = options.reconnectLimit as number || 50; const containerReconnectLimit = options.reconnectLimit as number || 50;
if (sink === '') { if (sink === '') {
throw new Error('Queue or Topic required!'); throw new NodeOperationError(this.getNode(), 'Queue or Topic required!');
} }
let durable = false; let durable = false;

View file

@ -9,6 +9,7 @@ import {
INodePropertyOptions, INodePropertyOptions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -466,7 +467,7 @@ export class ApiTemplateIo implements INodeType {
if (overrideJson !== '') { if (overrideJson !== '') {
const data = validateJSON(overrideJson); const data = validateJSON(overrideJson);
if (data === undefined) { if (data === undefined) {
throw new Error('A valid JSON must be provided.'); throw new NodeOperationError(this.getNode(), 'A valid JSON must be provided.');
} }
body.overrides = data; body.overrides = data;
} }
@ -523,14 +524,14 @@ export class ApiTemplateIo implements INodeType {
if (jsonParameters === false) { if (jsonParameters === false) {
const properties = (this.getNodeParameter('propertiesUi', i) as IDataObject || {}).propertyValues as IDataObject[] || []; const properties = (this.getNodeParameter('propertiesUi', i) as IDataObject || {}).propertyValues as IDataObject[] || [];
if (properties.length === 0) { if (properties.length === 0) {
throw new Error('The parameter properties cannot be empty'); throw new NodeOperationError(this.getNode(), 'The parameter properties cannot be empty');
} }
data = properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}); data = properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {});
} else { } else {
const propertiesJson = this.getNodeParameter('propertiesJson', i) as string; const propertiesJson = this.getNodeParameter('propertiesJson', i) as string;
data = validateJSON(propertiesJson); data = validateJSON(propertiesJson);
if (data === undefined) { if (data === undefined) {
throw new Error('A valid JSON must be provided.'); throw new NodeOperationError(this.getNode(), 'A valid JSON must be provided.');
} }
} }

View file

@ -6,6 +6,7 @@ import {
IExecuteFunctions, IExecuteFunctions,
ILoadOptionsFunctions, ILoadOptionsFunctions,
} from 'n8n-core'; } from 'n8n-core';
import { NodeApiError } from 'n8n-workflow';
export async function apiTemplateIoApiRequest( export async function apiTemplateIoApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions, this: IExecuteFunctions | ILoadOptionsFunctions,
@ -42,14 +43,11 @@ export async function apiTemplateIoApiRequest(
try { try {
const response = await this.helpers.request!(options); const response = await this.helpers.request!(options);
if (response.status === 'error') { if (response.status === 'error') {
throw new Error(response.message); throw new NodeApiError(this.getNode(), response.message);
} }
return response; return response;
} catch (error) { } catch (error) {
if (error?.response?.body?.message) { throw new NodeApiError(this.getNode(), error);
throw new Error(`APITemplate.io error response [${error.statusCode}]: ${error.response.body.message}`);
}
throw error;
} }
} }

View file

@ -1,102 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 23.167 21.167"><path fill="#91bce5" d="M18.611.504L3.473 11.24l4.597 2.213 1.066 3.883 3.171-2.226 2.802.951z"/><path fill="#1f212b" d="M9.172 17.597a.264.264 0 01-.29-.192l-1.035-3.767-4.488-2.16a.265.265 0 01-.039-.454L18.458.288a.264.264 0 01.41.274l-3.5 15.556a.265.265 0 01-.344.193l-2.674-.908-3.061 2.15a.273.273 0 01-.117.044zm-5.18-6.401l4.193 2.017c.069.034.12.095.14.169l.967 3.52 2.863-2.01a.265.265 0 01.237-.033l2.524.856L18.2 1.119z"/><path fill="#3a84c1" d="M9.339 17.079l.946-2.913L18.642.728 8.226 13.235z"/><path fill="#1f212b" d="M9.356 17.21a.132.132 0 01-.143-.172l.946-2.913a.16.16 0 01.014-.03l7.393-11.888-9.419 11.156a.133.133 0 01-.202-.171L18.54.642a.132.132 0 01.214.156l-8.348 13.424-.941 2.898a.132.132 0 01-.109.09zm-3.215 1.045c-1.168.029-2.303-.373-3.067-1.287-.485-.58-.683-1.574.275-1.705.859-.119 1.229.485 1.48 1.196.804 2.026.16 2.938-1.68 4.272-.144.09.015.296.159.206 1.218-1 1.829-1.466 2.1-2.448.134-1.36-.402-3.091-1.384-3.436-1.12-.382-2.063.516-1.499 1.585.699 1.324 2.23 1.914 3.652 1.878.17-.004.133-.266-.036-.261z"/><path fill="#1f212b" d="M7.235 18.14a14.95 14.95 0 01-.759.09c-.17.017-.133.279.036.262.253-.025.507-.052.758-.09.169-.026.133-.288-.035-.262zm1.194-.504a8.46 8.46 0 01-.865.344c-.162.054-.057.297.104.243a8.37 8.37 0 00.865-.344c.154-.071.05-.315-.104-.243z"/><path d="M12.599 15.22a.131.131 0 01-.07-.01l-2.168-.924a.133.133 0 01.103-.243l2.169.923a.133.133 0 01-.034.253z"/></svg>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="80"
height="80"
viewBox="0 0 23.166659 21.166671"
version="1.1"
id="svg8"
inkscape:version="1.0.1 (1.0.1+r75)"
sodipodi:docname="favicon.svg"
inkscape:export-filename="/mnt/shared/bktan81@gmail.com/Companies/AlphaCloud/APITemplate.io/export/favicon-color.png"
inkscape:export-xdpi="614.40002"
inkscape:export-ydpi="614.40002">
<defs
id="defs2">
<rect
x="5.5194178"
y="6.7918406"
width="82.495544"
height="30.566183"
id="rect835" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.6962788"
inkscape:cx="84.04094"
inkscape:cy="31.651389"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1014"
inkscape:window-x="0"
inkscape:window-y="36"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
style="fill:#000000"
id="g944"
transform="matrix(0.26072249,-0.0450346,0.0450346,0.26072249,-4.4090493,0.80238047)">
<path
fill="#91bce5"
d="m 85.926965,13.698648 -63.285045,30.243807 15.697805,11.196666 1.472218,15.148608 13.242212,-6.250661 9.822444,5.345054 z"
id="path918" />
<path
fill="#1f212b"
d="m 39.778008,71.288154 c -0.158903,-0.0056 -0.31648,-0.04907 -0.45874,-0.130089 -0.283589,-0.159995 -0.470626,-0.448698 -0.501329,-0.772967 L 37.38898,55.688275 22.06215,44.756717 c -0.288015,-0.205176 -0.446151,-0.547904 -0.415808,-0.901061 0.03035,-0.353155 0.245283,-0.662843 0.564816,-0.815784 L 85.495203,12.796029 c 0.373477,-0.179075 0.817321,-0.109542 1.119534,0.176179 0.301247,0.284686 0.393931,0.72519 0.235443,1.108892 L 63.800813,69.764609 c -0.108224,0.262384 -0.32346,0.465996 -0.589929,0.560754 -0.268434,0.09369 -0.562801,0.0704 -0.811249,-0.06435 l -9.376677,-5.101349 -12.782391,6.034577 c -0.147425,0.0669 -0.305655,0.09939 -0.462559,0.09391 z M 24.604162,44.11304 38.920495,54.324893 c 0.235269,0.168314 0.386237,0.429742 0.414198,0.717894 l 1.33547,13.734974 11.957261,-5.644136 c 0.287923,-0.136035 0.624788,-0.126272 0.904658,0.02559 l 8.84643,4.812668 21.627569,-52.247035 z"
id="path920" />
<path
fill="#3a84c1"
d="M 40.73124,69.46431 46.129963,59.222484 85.897126,14.553126 39.059778,54.433821 Z"
id="path922" />
<path
fill="#1f212b"
d="m 40.71279,69.963971 c -0.07296,-0.0026 -0.146351,-0.02112 -0.215122,-0.05754 -0.244661,-0.128616 -0.338162,-0.431064 -0.208546,-0.675689 l 5.398723,-10.241827 c 0.01925,-0.03535 0.04242,-0.06856 0.06852,-0.09967 L 80.940068,19.368061 38.684781,54.856992 c -0.211297,0.176733 -0.526545,0.149715 -0.705278,-0.06165 -0.177698,-0.212332 -0.149715,-0.526547 0.06165,-0.705279 L 85.574314,14.169627 c 0.201987,-0.168053 0.497667,-0.153725 0.680201,0.03476 0.182533,0.188486 0.189157,0.485899 0.01525,0.680947 L 46.544181,59.510112 41.173361,69.69788 c -0.09216,0.17489 -0.27468,0.272579 -0.460568,0.266088 z"
id="path924" />
<path
fill="#1f212b"
d="m 28.066921,71.78593 c -4.37019,-0.64391 -8.339996,-2.867809 -10.598292,-6.766997 -1.432549,-2.473502 -1.528128,-6.298167 2.122716,-6.172679 3.274075,0.112327 4.26409,2.596776 4.740567,5.406801 1.694711,8.06304 -1.290339,11.045322 -9.002137,14.829352 -0.594821,0.242389 -0.134492,1.113471 0.457295,0.871978 5.181377,-2.944492 7.754842,-4.282847 9.399081,-7.76557 1.370916,-4.981188 0.489139,-11.773963 -2.947089,-13.689368 -3.921594,-2.143168 -8.01567,0.59562 -6.601411,4.939623 1.751362,5.378399 7.074496,8.562112 12.394405,9.345251 0.635099,0.09422 0.665001,-0.905343 0.03486,-0.998391 z"
id="path926"
sodipodi:nodetypes="cccccccccccc" />
<path
fill="#1f212b"
d="m 32.211855,72.060754 c -0.960938,-0.01857 -1.924304,-0.08221 -2.882497,-0.15069 -0.64277,-0.04646 -0.676671,0.952965 -0.0349,0.999392 0.958193,0.06848 1.921524,0.133135 2.882497,0.150689 0.642958,0.01242 0.679856,-0.986875 0.0349,-0.999391 z m 4.771615,-1.109149 c -1.135251,0.302564 -2.280575,0.549744 -3.442445,0.726303 -0.635768,0.09687 -0.401574,1.069637 0.232195,0.972696 1.16187,-0.176559 2.307194,-0.423738 3.442444,-0.726302 0.620118,-0.164459 0.391991,-1.139015 -0.232194,-0.972697 z"
id="path928" />
<path
fill="#1f212b"
d="m 54.069217,64.636113 c -0.08695,-0.003 -0.175091,-0.02913 -0.253366,-0.07989 l -7.481853,-4.834056 c -0.231899,-0.150184 -0.298138,-0.459685 -0.148952,-0.69162 0.150185,-0.231899 0.460683,-0.298103 0.691619,-0.148953 l 7.481852,4.834058 c 0.231899,0.150184 0.298139,0.459686 0.148953,0.69162 -0.09943,0.153622 -0.268357,0.234778 -0.438253,0.228845 z"
id="path926-0"
sodipodi:nodetypes="sccccccs"
style="fill:#000000" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -15,6 +15,13 @@
{ {
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.asana/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.asana/"
} }
],
"generic": [
{
"label": "How a digital strategist uses n8n for online marketing",
"icon": "💻",
"url": "https://n8n.io/blog/how-a-digital-strategist-uses-n8n-for-online-marketing/"
}
] ]
} }
} }

View file

@ -9,6 +9,8 @@ import {
INodePropertyOptions, INodePropertyOptions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -1639,7 +1641,7 @@ export class Asana implements INodeType {
const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {}); const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {});
if (responseData.data === undefined) { if (responseData.data === undefined) {
throw new Error('No data got returned'); throw new NodeApiError(this.getNode(), responseData, { message: 'No data got returned' });
} }
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
@ -1674,7 +1676,7 @@ export class Asana implements INodeType {
const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {}); const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {});
if (responseData.data === undefined) { if (responseData.data === undefined) {
throw new Error('No data got returned'); throw new NodeApiError(this.getNode(), responseData, { message: 'No data got returned' });
} }
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
@ -1711,7 +1713,7 @@ export class Asana implements INodeType {
// to retrieve the teams from an organization just work with workspaces that are an organization // to retrieve the teams from an organization just work with workspaces that are an organization
if (workspace.is_organization === false) { if (workspace.is_organization === false) {
throw Error('To filter by team, the workspace selected has to be an organization'); throw new NodeOperationError(this.getNode(), 'To filter by team, the workspace selected has to be an organization');
} }
const endpoint = `/organizations/${workspaceId}/teams`; const endpoint = `/organizations/${workspaceId}/teams`;
@ -1750,15 +1752,15 @@ export class Asana implements INodeType {
let taskData; let taskData;
try { try {
taskData = await asanaApiRequest.call(this, 'GET', `/tasks/${taskId}`, {}); taskData = await asanaApiRequest.call(this, 'GET', `/tasks/${taskId}`, {});
} catch (e) { } catch (error) {
throw new Error(`Could not find task with id "${taskId}" so tags could not be loaded.`); throw new NodeApiError(this.getNode(), error, { message: `Could not find task with id "${taskId}" so tags could not be loaded.` });
} }
const workspace = taskData.data.workspace.gid; const workspace = taskData.data.workspace.gid;
const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {}, { workspace }); const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {}, { workspace });
if (responseData.data === undefined) { if (responseData.data === undefined) {
throw new Error('No data got returned'); throw new NodeApiError(this.getNode(), responseData, { message: 'No data got returned' });
} }
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
@ -1790,7 +1792,7 @@ export class Asana implements INodeType {
const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {}); const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {});
if (responseData.data === undefined) { if (responseData.data === undefined) {
throw new Error('No data got returned'); throw new NodeApiError(this.getNode(), responseData, { message: 'No data got returned' });
} }
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];

View file

@ -10,6 +10,7 @@ import {
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
IWebhookResponseData, IWebhookResponseData,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -155,7 +156,7 @@ export class AsanaTrigger implements INodeType {
const webhookUrl = this.getNodeWebhookUrl('default') as string; const webhookUrl = this.getNodeWebhookUrl('default') as string;
if (webhookUrl.includes('%20')) { if (webhookUrl.includes('%20')) {
throw new Error('The name of the Asana Trigger Node is not allowed to contain any spaces!'); throw new NodeOperationError(this.getNode(), 'The name of the Asana Trigger Node is not allowed to contain any spaces!');
} }
const resource = this.getNodeParameter('resource') as string; const resource = this.getNodeParameter('resource') as string;
@ -189,7 +190,7 @@ export class AsanaTrigger implements INodeType {
try { try {
await asanaApiRequest.call(this, 'DELETE', endpoint, body); await asanaApiRequest.call(this, 'DELETE', endpoint, body);
} catch (e) { } catch (error) {
return false; return false;
} }

View file

@ -11,6 +11,8 @@ import {
import { import {
IDataObject, IDataObject,
INodePropertyOptions, INodePropertyOptions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -43,7 +45,7 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions |
const credentials = this.getCredentials('asanaApi'); const credentials = this.getCredentials('asanaApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
options.headers!['Authorization'] = `Bearer ${credentials.accessToken}`; options.headers!['Authorization'] = `Bearer ${credentials.accessToken}`;
@ -54,25 +56,7 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions |
return await this.helpers.requestOAuth2.call(this, 'asanaOAuth2Api', options); return await this.helpers.requestOAuth2.call(this, 'asanaOAuth2Api', options);
} }
} catch (error) { } catch (error) {
if (error.statusCode === 401) { throw new NodeApiError(this.getNode(), error);
// Return a clear error
throw new Error('The Asana credentials are not valid!');
}
if (error.statusCode === 403) {
throw error;
}
if (error.response && error.response.body && error.response.body.errors) {
// Try to return the error prettier
const errorMessages = error.response.body.errors.map((errorData: { message: string }) => {
return errorData.message;
});
throw new Error(`Asana error response [${error.statusCode}]: ${errorMessages.join(' | ')}`);
}
// If that data does not exist for some reason return the actual error
throw error;
} }
} }

View file

@ -1,18 +1 @@
<?xml version="1.0" encoding="UTF-8"?> <svg width="60" height="60" xmlns="http://www.w3.org/2000/svg"><defs><radialGradient cx="50%" cy="55%" fx="50%" fy="55%" r="72.507%" gradientTransform="matrix(.92404 0 0 1 .038 0)" id="a"><stop stop-color="#FFB900" offset="0%"/><stop stop-color="#F95D8F" offset="60%"/><stop stop-color="#F95353" offset="99.91%"/></radialGradient></defs><path d="M45.594 28.5c-6.994.003-12.664 5.673-12.667 12.667.003 6.995 5.673 12.664 12.667 12.668 6.995-.004 12.664-5.673 12.667-12.668-.003-6.994-5.672-12.664-12.667-12.667zm-32.927.001C5.673 28.505.003 34.174 0 41.17c.003 6.994 5.673 12.664 12.667 12.667 6.995-.003 12.664-5.673 12.668-12.667-.004-6.995-5.673-12.664-12.668-12.668zM41.79 12.667c-.002 6.995-5.671 12.665-12.666 12.67-6.995-.004-12.664-5.674-12.667-12.67C16.46 5.673 22.13.003 29.123 0c6.994.004 12.663 5.673 12.666 12.667z" transform="translate(.732 2.732)" fill="url(#a)" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg width="60px" height="60px" viewBox="0 0 60 60" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>asana_node_icon</title>
<defs>
<radialGradient cx="50%" cy="55%" fx="50%" fy="55%" r="72.5074481%" gradientTransform="translate(0.500000,0.550000),scale(0.924043,1.000000),translate(-0.500000,-0.550000)" id="radialGradient-1">
<stop stop-color="#FFB900" offset="0%"></stop>
<stop stop-color="#F95D8F" offset="60%"></stop>
<stop stop-color="#F95353" offset="99.91%"></stop>
</radialGradient>
</defs>
<g id="asana_node_icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="asana" transform="translate(0.000000, 2.000000)" fill="url(#radialGradient-1)">
<g id="Group" transform="translate(0.731707, 0.731707)">
<path d="M45.5941463,28.5 C38.5995187,28.50323 32.9300593,34.1726894 32.9268293,41.1673171 C32.9300593,48.1619448 38.5995187,53.8314041 45.5941463,53.8346341 C52.588774,53.8314041 58.2582334,48.1619448 58.2614634,41.1673171 C58.2582334,34.1726894 52.588774,28.50323 45.5941463,28.5 L45.5941463,28.5 Z M12.6673171,28.5014634 C5.67268939,28.5046934 0.00323002946,34.1741528 -1.40700459e-15,41.1687805 C0.00323002946,48.1634082 5.67268939,53.8328675 12.6673171,53.8360976 C19.6619448,53.8328675 25.3314041,48.1634082 25.3346341,41.1687805 C25.3314041,34.1741528 19.6619448,28.5046934 12.6673171,28.5014634 L12.6673171,28.5014634 Z M41.7892683,12.6673171 C41.7868464,19.6619451 36.118042,25.3320595 29.1234146,25.3360976 C22.1282158,25.3328669 16.4585201,19.6625162 16.4560976,12.6673171 C16.4593276,5.67268939 22.128787,0.00323002946 29.1234146,-1.40700459e-15 C36.1174708,0.0040373938 41.7860389,5.67326048 41.7892683,12.6673171 L41.7892683,12.6673171 Z" id="Shape"></path>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 951 B

View file

@ -9,7 +9,7 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
IDataObject, IDataObject, NodeApiError,
} from 'n8n-workflow'; } from 'n8n-workflow';
export async function automizyApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, option = {}): Promise<any> { // tslint:disable-line:no-any export async function automizyApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, option = {}): Promise<any> { // tslint:disable-line:no-any
@ -40,14 +40,7 @@ export async function automizyApiRequest(this: IExecuteFunctions | IExecuteSingl
//@ts-ignore //@ts-ignore
return await this.helpers.request.call(this, options); return await this.helpers.request.call(this, options);
} catch (error) { } catch (error) {
if (error.response && error.response.body) { throw new NodeApiError(this.getNode(), error);
throw new Error(
`Automizy error response [${error.statusCode}]: ${error.response.body.title}`,
);
}
throw error;
} }
} }

View file

@ -0,0 +1,20 @@
{
"node": "n8n-nodes-base.autopilot",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Marketing & Content"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/autopilot"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.autopilot/"
}
]
}
}

View file

@ -0,0 +1,20 @@
{
"node": "n8n-nodes-base.autopilotTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Marketing & Content"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/autopilot"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.autopilotTrigger/"
}
]
}
}

View file

@ -11,6 +11,7 @@ import {
IDataObject, IDataObject,
IHookFunctions, IHookFunctions,
IWebhookFunctions, IWebhookFunctions,
NodeApiError,
} from 'n8n-workflow'; } from 'n8n-workflow';
export async function autopilotApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any export async function autopilotApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
@ -42,11 +43,7 @@ export async function autopilotApiRequest(this: IExecuteFunctions | IWebhookFunc
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
if (error.response) { throw new NodeApiError(this.getNode(), error);
const errorMessage = error.response.body.message || error.response.body.description || error.message;
throw new Error(`Autopilot error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;
} }
} }

View file

@ -1,8 +1 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="38 26 35 35"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="38 26 35 35"><circle cx="50" cy="50" r="40" stroke="#18d4b2" stroke-width="3" fill="#18d4b2"/><path fill="#fff" d="M45.4 42.6h19.9l3.4-4.8H42l3.4 4.8zm3.1 8.3h13.1l3.4-4.8H45.4l3.1 4.8zm54-.7"/></svg>
<circle cx="50" cy="50" r="40" stroke="#18d4b2" stroke-width="3" fill="#18d4b2" />
<g>
<g>
<path fill="#ffffff" d="M45.4,42.6h19.9l3.4-4.8H42L45.4,42.6z M48.5,50.9h13.1l3.4-4.8H45.4L48.5,50.9z M102.5,50.2"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 249 B

View file

@ -6,6 +6,8 @@ import {
INodePropertyOptions, INodePropertyOptions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { awsApiRequestREST } from './GenericFunctions'; import { awsApiRequestREST } from './GenericFunctions';
@ -130,13 +132,7 @@ export class AwsLambda implements INodeType {
loadOptions: { loadOptions: {
async getFunctions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getFunctions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
const data = await awsApiRequestREST.call(this, 'lambda', 'GET', '/2015-03-31/functions/');
let data;
try {
data = await awsApiRequestREST.call(this, 'lambda', 'GET', '/2015-03-31/functions/');
} catch (err) {
throw new Error(`AWS Error: ${err}`);
}
for (const func of data.Functions!) { for (const func of data.Functions!) {
returnData.push({ returnData.push({
@ -162,22 +158,17 @@ export class AwsLambda implements INodeType {
Qualifier: this.getNodeParameter('qualifier', i) as string, Qualifier: this.getNodeParameter('qualifier', i) as string,
}; };
let responseData; const responseData = await awsApiRequestREST.call(
try { this,
responseData = await awsApiRequestREST.call( 'lambda',
this, 'POST',
'lambda', `/2015-03-31/functions/${params.FunctionName}/invocations?Qualifier=${params.Qualifier}`,
'POST', params.Payload,
`/2015-03-31/functions/${params.FunctionName}/invocations?Qualifier=${params.Qualifier}`, {
params.Payload, 'X-Amz-Invocation-Type': params.InvocationType,
{ 'Content-Type': 'application/x-amz-json-1.0',
'X-Amz-Invocation-Type': params.InvocationType, },
'Content-Type': 'application/x-amz-json-1.0', );
},
);
} catch (err) {
throw new Error(`AWS Error: ${err}`);
}
if (responseData !== null && responseData.errorMessage !== undefined) { if (responseData !== null && responseData.errorMessage !== undefined) {
let errorMessage = responseData.errorMessage; let errorMessage = responseData.errorMessage;
@ -186,7 +177,7 @@ export class AwsLambda implements INodeType {
errorMessage += `\n\nStack trace:\n${responseData.stackTrace}`; errorMessage += `\n\nStack trace:\n${responseData.stackTrace}`;
} }
throw new Error(errorMessage); throw new NodeApiError(this.getNode(), responseData);
} else { } else {
returnData.push({ returnData.push({
result: responseData, result: responseData,

View file

@ -1,14 +0,0 @@
{
"node": "n8n-nodes-base.awsSes",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "Development"],
"resources": {
"credentialDocumentation": [
{ "url": "https://docs.n8n.io/credentials/aws" }
],
"primaryDocumentation": [
{ "url": "https://docs.n8n.io/nodes/n8n-nodes-base.awsSes/" }
]
}
}

View file

@ -6,6 +6,8 @@ import {
INodePropertyOptions, INodePropertyOptions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { awsApiRequestSOAP } from './GenericFunctions'; import { awsApiRequestSOAP } from './GenericFunctions';
@ -107,12 +109,7 @@ export class AwsSns implements INodeType {
// select them easily // select them easily
async getTopics(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getTopics(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
let data; const data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics');
try {
data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics');
} catch (err) {
throw new Error(`AWS Error: ${err}`);
}
let topics = data.ListTopicsResponse.ListTopicsResult.Topics.member; let topics = data.ListTopicsResponse.ListTopicsResult.Topics.member;
@ -149,12 +146,8 @@ export class AwsSns implements INodeType {
'Message=' + this.getNodeParameter('message', i) as string, 'Message=' + this.getNodeParameter('message', i) as string,
]; ];
let responseData;
try { const responseData = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=Publish&' + params.join('&'));
responseData = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=Publish&' + params.join('&'));
} catch (err) {
throw new Error(`AWS Error: ${err}`);
}
returnData.push({MessageId: responseData.PublishResponse.PublishResult.MessageId} as IDataObject); returnData.push({MessageId: responseData.PublishResponse.PublishResult.MessageId} as IDataObject);
} }

View file

@ -9,6 +9,8 @@ import {
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
IWebhookResponseData, IWebhookResponseData,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -68,12 +70,7 @@ export class AwsSnsTrigger implements INodeType {
// select them easily // select them easily
async getTopics(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getTopics(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
let data; const data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics');
try {
data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics');
} catch (err) {
throw new Error(`AWS Error: ${err}`);
}
let topics = data.ListTopicsResponse.ListTopicsResult.Topics.member; let topics = data.ListTopicsResponse.ListTopicsResult.Topics.member;
@ -134,7 +131,7 @@ export class AwsSnsTrigger implements INodeType {
const topic = this.getNodeParameter('topic') as string; const topic = this.getNodeParameter('topic') as string;
if (webhookUrl.includes('%20')) { if (webhookUrl.includes('%20')) {
throw new Error('The name of the SNS Trigger Node is not allowed to contain any spaces!'); throw new NodeOperationError(this.getNode(), 'The name of the SNS Trigger Node is not allowed to contain any spaces!');
} }
const params = [ const params = [

View file

@ -22,7 +22,7 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
function getEndpointForService(service: string, credentials: ICredentialDataDecryptedObject): string { function getEndpointForService(service: string, credentials: ICredentialDataDecryptedObject): string {
@ -40,7 +40,7 @@ function getEndpointForService(service: string, credentials: ICredentialDataDecr
export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('aws'); const credentials = this.getCredentials('aws');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
// Concatenate path and instantiate URL object so it parses correctly query strings // Concatenate path and instantiate URL object so it parses correctly query strings
@ -61,17 +61,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message; throw new NodeApiError(this.getNode(), error); // no XML parsing needed
if (error.statusCode === 403) {
if (errorMessage === 'The security token included in the request is invalid.') {
throw new Error('The AWS credentials are not valid!');
} else if (errorMessage.startsWith('The request signature we calculated does not match the signature you provided')) {
throw new Error('The AWS credentials are not valid!');
}
}
throw new Error(`AWS error response [${error.statusCode}]: ${errorMessage}`);
} }
} }
@ -79,7 +69,7 @@ export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions
const response = await awsApiRequest.call(this, service, method, path, body, headers); const response = await awsApiRequest.call(this, service, method, path, body, headers);
try { try {
return JSON.parse(response); return JSON.parse(response);
} catch (e) { } catch (error) {
return response; return response;
} }
} }
@ -95,7 +85,7 @@ export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions
resolve(data); resolve(data);
}); });
}); });
} catch (e) { } catch (error) {
return response; return response;
} }
} }

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 75 75"><defs><style>.cls-1{fill:url(#TurquoiseGradient);}.cls-2{fill:#fff;}</style><linearGradient id="TurquoiseGradient" x1="617.46" y1="-674.53" x2="723.53" y2="-568.46" gradientTransform="translate(659 708) rotate(-90)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#055f4e"/><stop offset="1" stop-color="#56c0a7"/></linearGradient></defs><title>Amazon-Comprehend</title><g id="Reference"><rect id="Turquoise_Gradient" data-name="Turquoise Gradient" class="cls-1" width="75" height="75"/><g id="Icon_Test" data-name="Icon Test"><path class="cls-2" d="M44.5,34.2V24.5a1,1,0,0,0-.29-.71l-11-11a1,1,0,0,0-.71-.29h-19a1,1,0,0,0-1,1v43a1,1,0,0,0,1,1h30a1,1,0,0,0,1-1V52.06a11.8,11.8,0,0,1-2-2.3V55.5h-28v-41h17v10a1,1,0,0,0,1,1h10v11A11.56,11.56,0,0,1,44.5,34.2Zm-11-10.7V15.91l7.59,7.59Zm-10,8h-6v-2h6Zm16,0h-14v-2h14Zm0,6h-22v-2h22Zm15.44,25H50.06a1,1,0,0,1-.93-.62l-1.21-3a1,1,0,0,1,.09-.94,1,1,0,0,1,.83-.44h7.32a1,1,0,0,1,.83.44,1,1,0,0,1,.09.94l-1.21,3A1,1,0,0,1,54.94,62.5Zm-4.21-2h3.54l.4-1H50.33Zm11.64-19A10,10,0,0,0,42.5,43.12a10,10,0,0,0,4.28,8.2,3.88,3.88,0,0,1,.72.59V55.5a1,1,0,0,0,1,1h8a1,1,0,0,0,1-1V51.9a4.33,4.33,0,0,1,.71-.57,9.92,9.92,0,0,0,4.29-8.2A10.19,10.19,0,0,0,62.37,41.48ZM57.05,49.7c-.58.4-1.55,1.07-1.55,2.1v2.7h-2v-7h2v-2h-6v2h2v7h-2V51.82c0-1-1-1.73-1.58-2.14A8,8,0,1,1,58,37.32a7.89,7.89,0,0,1,2.39,4.47A8,8,0,0,1,57.05,49.7ZM28.5,25.5h-11v-2h11Zm1,18h-12v-2h12Zm10,0h-8v-2h8Zm-9,6h-13v-2h13Z"/></g></g></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75 75"><defs><linearGradient id="a" x1="617.46" y1="-674.53" x2="723.53" y2="-568.46" gradientTransform="rotate(-90 683.5 24.5)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#055f4e"/><stop offset="1" stop-color="#56c0a7"/></linearGradient></defs><path data-name="Turquoise Gradient" d="M0 0h75v75H0z" fill="url(#a)"/><path d="M44.5 34.2v-9.7a1 1 0 00-.29-.71l-11-11a1 1 0 00-.71-.29h-19a1 1 0 00-1 1v43a1 1 0 001 1h30a1 1 0 001-1v-4.44a11.8 11.8 0 01-2-2.3v5.74h-28v-41h17v10a1 1 0 001 1h10v11a11.56 11.56 0 012-2.3zm-11-10.7v-7.59l7.59 7.59zm-10 8h-6v-2h6zm16 0h-14v-2h14zm0 6h-22v-2h22zm15.44 25h-4.88a1 1 0 01-.93-.62l-1.21-3a1 1 0 01.09-.94 1 1 0 01.83-.44h7.32a1 1 0 01.83.44 1 1 0 01.09.94l-1.21 3a1 1 0 01-.93.62zm-4.21-2h3.54l.4-1h-4.34zm11.64-19a10 10 0 00-19.87 1.62 10 10 0 004.28 8.2 3.88 3.88 0 01.72.59v3.59a1 1 0 001 1h8a1 1 0 001-1v-3.6a4.33 4.33 0 01.71-.57 9.92 9.92 0 004.29-8.2 10.19 10.19 0 00-.13-1.65zm-5.32 8.2c-.58.4-1.55 1.07-1.55 2.1v2.7h-2v-7h2v-2h-6v2h2v7h-2v-2.68c0-1-1-1.73-1.58-2.14A8 8 0 1158 37.32a7.89 7.89 0 012.39 4.47 8 8 0 01-3.34 7.91zM28.5 25.5h-11v-2h11zm1 18h-12v-2h12zm10 0h-8v-2h8zm-9 6h-13v-2h13z" data-name="Icon Test" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,7 +1,7 @@
import { URL } from 'url'; import { URL } from 'url';
import { sign } from 'aws4'; import { sign } from 'aws4';
import { OptionsWithUri } from 'request'; import { OptionsWithUri } from 'request';
import { parseString } from 'xml2js'; import { parseString as parseXml } from 'xml2js';
import { import {
IExecuteFunctions, IExecuteFunctions,
@ -11,7 +11,7 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
function getEndpointForService(service: string, credentials: ICredentialDataDecryptedObject): string { function getEndpointForService(service: string, credentials: ICredentialDataDecryptedObject): string {
@ -31,7 +31,7 @@ function getEndpointForService(service: string, credentials: ICredentialDataDecr
export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('aws'); const credentials = this.getCredentials('aws');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
// Concatenate path and instantiate URL object so it parses correctly query strings // Concatenate path and instantiate URL object so it parses correctly query strings
@ -52,17 +52,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message; throw new NodeApiError(this.getNode(), error, { parseXml: true });
if (error.statusCode === 403) {
if (errorMessage === 'The security token included in the request is invalid.') {
throw new Error('The AWS credentials are not valid!');
} else if (errorMessage.startsWith('The request signature we calculated does not match the signature you provided')) {
throw new Error('The AWS credentials are not valid!');
}
}
throw new Error(`AWS error response [${error.statusCode}]: ${errorMessage}`);
} }
} }
@ -70,7 +60,7 @@ export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions
const response = await awsApiRequest.call(this, service, method, path, body, headers); const response = await awsApiRequest.call(this, service, method, path, body, headers);
try { try {
return JSON.parse(response); return JSON.parse(response);
} catch (e) { } catch (error) {
return response; return response;
} }
} }
@ -79,14 +69,14 @@ export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions
const response = await awsApiRequest.call(this, service, method, path, body, headers); const response = await awsApiRequest.call(this, service, method, path, body, headers);
try { try {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
parseString(response, { explicitArray: false }, (err, data) => { parseXml(response, { explicitArray: false }, (err, data) => {
if (err) { if (err) {
return reject(err); return reject(err);
} }
resolve(data); resolve(data);
}); });
}); });
} catch (e) { } catch (error) {
return response; return response;
} }
} }

View file

@ -8,6 +8,8 @@ import {
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -459,11 +461,11 @@ export class AwsRekognition implements INodeType {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string; const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string;
if (items[i].binary === undefined) { if (items[i].binary === undefined) {
throw new Error('No binary data exists on item!'); throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
} }
if ((items[i].binary as IBinaryKeyData)[binaryPropertyName] === undefined) { if ((items[i].binary as IBinaryKeyData)[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); throw new NodeOperationError(this.getNode(), `No binary data property "${binaryPropertyName}" does not exists on item!`);
} }
const binaryPropertyData = (items[i].binary as IBinaryKeyData)[binaryPropertyName]; const binaryPropertyData = (items[i].binary as IBinaryKeyData)[binaryPropertyName];
@ -494,7 +496,9 @@ export class AwsRekognition implements INodeType {
body.Image.S3Object.Version = additionalFields.version as string; body.Image.S3Object.Version = additionalFields.version as string;
} }
} }
responseData = await awsApiRequestREST.call(this, 'rekognition', 'POST', '', JSON.stringify(body), {}, { 'X-Amz-Target': action, 'Content-Type': 'application/x-amz-json-1.1' }); responseData = await awsApiRequestREST.call(this, 'rekognition', 'POST', '', JSON.stringify(body), {}, { 'X-Amz-Target': action, 'Content-Type': 'application/x-amz-json-1.1' });
} }
} }
} }

View file

@ -27,6 +27,8 @@ import {
import { import {
IDataObject, IDataObject,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -36,7 +38,7 @@ import {
export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string | Buffer | IDataObject, query: IDataObject = {}, headers?: object, option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string | Buffer | IDataObject, query: IDataObject = {}, headers?: object, option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('aws'); const credentials = this.getCredentials('aws');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
const endpoint = new URL(((credentials.rekognitionEndpoint as string || '').replace('{region}', credentials.region as string) || `https://${service}.${credentials.region}.amazonaws.com`) + path); const endpoint = new URL(((credentials.rekognitionEndpoint as string || '').replace('{region}', credentials.region as string) || `https://${service}.${credentials.region}.amazonaws.com`) + path);
@ -59,17 +61,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message; throw new NodeApiError(this.getNode(), error);
if (error.statusCode === 403) {
if (errorMessage === 'The security token included in the request is invalid.') {
throw new Error('The AWS credentials are not valid!');
} else if (errorMessage.startsWith('The request signature we calculated does not match the signature you provided')) {
throw new Error('The AWS credentials are not valid!');
}
}
throw new Error(`AWS error response [${error.statusCode}]: ${errorMessage}`);
} }
} }
@ -77,7 +69,7 @@ export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions
const response = await awsApiRequest.call(this, service, method, path, body, query, headers, options, region); const response = await awsApiRequest.call(this, service, method, path, body, query, headers, options, region);
try { try {
return JSON.parse(response); return JSON.parse(response);
} catch (e) { } catch (error) {
return response; return response;
} }
} }
@ -93,8 +85,8 @@ export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions
resolve(data); resolve(data);
}); });
}); });
} catch (e) { } catch (error) {
return e; return error;
} }
} }

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 74.375 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x="2.188" y="2.5"/><symbol id="A" overflow="visible"><g stroke="none"><path d="M2.886 52.8L16.8 51.268V28.732L2.886 27.2v25.6z" fill="#5294cf"/><g fill="#19486f"><path d="M67.207 28.898l-6.462 2.43-36.237-5.346L34.99 0l32.217 28.898z"/><path d="M3.504 28.966L35 12.234 45.543 26 16.81 30.224l-13.305-1.26z"/></g><g fill="#205b99"><path d="M35 24L0 30.624V16.556L35 0l17.016 18.478L35 24z"/><path d="M7.008 16.478L0 19.395v44.05l7.008 3.307V16.478z"/></g><path d="M70 16.566L35 0v24l35 6.624v-14.06z" fill="#5294cf"/><g fill="#99bce3"><path d="M1.154 51.26L34.99 80l10.554-26-28.734-4.224L1.154 51.26z"/><path d="M67.64 51.142l-6.493-2.527-36.64 5.405 10.48 25.22 32.65-28.097z"/></g><path d="M67.207 51.103l-13.965-1.327v-19.55L67.207 28.9v22.205zM35 56l15.13-16L35 24 16.356 40 35 56z" fill="#205b99"/><path d="M53.624 40L35 56V24l18.634 16z" fill="#5294cf"/><path d="M0 49.376L35 56l19.21 7.873L35 80 0 63.444V49.376z" fill="#205b99"/><g fill="#5294cf"><path d="M70 63.435L35 80V56l35-6.624v14.06z"/><path d="M62.97 66.75L70 63.434V16.566l-7.03-3.327V66.75z"/></g></g></symbol></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 74.375 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#a" x="2.188" y="2.5"/><symbol id="a" overflow="visible"><g stroke="none"><path d="M2.886 52.8L16.8 51.268V28.732L2.886 27.2v25.6z" fill="#5294cf"/><g fill="#19486f"><path d="M67.207 28.898l-6.462 2.43-36.237-5.346L34.99 0l32.217 28.898z"/><path d="M3.504 28.966L35 12.234 45.543 26 16.81 30.224l-13.305-1.26z"/></g><g fill="#205b99"><path d="M35 24L0 30.624V16.556L35 0l17.016 18.478L35 24z"/><path d="M7.008 16.478L0 19.395v44.05l7.008 3.307V16.478z"/></g><path d="M70 16.566L35 0v24l35 6.624v-14.06z" fill="#5294cf"/><g fill="#99bce3"><path d="M1.154 51.26L34.99 80l10.554-26-28.734-4.224L1.154 51.26z"/><path d="M67.64 51.142l-6.493-2.527-36.64 5.405 10.48 25.22 32.65-28.097z"/></g><path d="M67.207 51.103l-13.965-1.327v-19.55L67.207 28.9v22.205zM35 56l15.13-16L35 24 16.356 40 35 56z" fill="#205b99"/><path d="M53.624 40L35 56V24l18.634 16z" fill="#5294cf"/><path d="M0 49.376L35 56l19.21 7.873L35 80 0 63.444V49.376z" fill="#205b99"/><g fill="#5294cf"><path d="M70 63.435L35 80V56l35-6.624v14.06z"/><path d="M62.97 66.75L70 63.434V16.566l-7.03-3.327V66.75z"/></g></g></symbol></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -19,9 +19,9 @@
], ],
"generic": [ "generic": [
{ {
"label": "Why this CEO loves n8n", "label": "Why business process automation with n8n can change your daily life",
"icon": "🧬", "icon": "🧬",
"url": "https://medium.com/n8n-io/why-this-ceo-loves-n8n-ee7d102b0948" "url": "https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/"
} }
] ]
} }

View file

@ -23,6 +23,7 @@ import {
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -422,7 +423,7 @@ export class AwsS3 implements INodeType {
const fileName = fileKey.split('/')[fileKey.split('/').length - 1]; const fileName = fileKey.split('/')[fileKey.split('/').length - 1];
if (fileKey.substring(fileKey.length - 1) === '/') { if (fileKey.substring(fileKey.length - 1) === '/') {
throw new Error('Downloding a whole directory is not yet supported, please provide a file key'); throw new NodeOperationError(this.getNode(), 'Downloding a whole directory is not yet supported, please provide a file key');
} }
let region = await awsApiRequestSOAP.call(this, `${bucketName}.s3`, 'GET', '', '', { location: '' }); let region = await awsApiRequestSOAP.call(this, `${bucketName}.s3`, 'GET', '', '', { location: '' });
@ -588,11 +589,11 @@ export class AwsS3 implements INodeType {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string; const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string;
if (items[i].binary === undefined) { if (items[i].binary === undefined) {
throw new Error('No binary data exists on item!'); throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
} }
if ((items[i].binary as IBinaryKeyData)[binaryPropertyName] === undefined) { if ((items[i].binary as IBinaryKeyData)[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); throw new NodeOperationError(this.getNode(), `No binary data property "${binaryPropertyName}" does not exists on item!`);
} }
const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName]; const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName];

View file

@ -26,13 +26,13 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
IDataObject, IDataObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string | Buffer, query: IDataObject = {}, headers?: object, option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string | Buffer, query: IDataObject = {}, headers?: object, option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('aws'); const credentials = this.getCredentials('aws');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
const endpoint = new URL(((credentials.s3Endpoint as string || '').replace('{region}', credentials.region as string) || `https://${service}.${credentials.region}.amazonaws.com`) + path); const endpoint = new URL(((credentials.s3Endpoint as string || '').replace('{region}', credentials.region as string) || `https://${service}.${credentials.region}.amazonaws.com`) + path);
@ -57,17 +57,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message; throw new NodeApiError(this.getNode(), error, { parseXml: true });
if (error.statusCode === 403) {
if (errorMessage === 'The security token included in the request is invalid.') {
throw new Error('The AWS credentials are not valid!');
} else if (errorMessage.startsWith('The request signature we calculated does not match the signature you provided')) {
throw new Error('The AWS credentials are not valid!');
}
}
throw new Error(`AWS error response [${error.statusCode}]: ${errorMessage}`);
} }
} }
@ -75,7 +65,7 @@ export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions
const response = await awsApiRequest.call(this, service, method, path, body, query, headers, options, region); const response = await awsApiRequest.call(this, service, method, path, body, query, headers, options, region);
try { try {
return JSON.parse(response); return JSON.parse(response);
} catch (e) { } catch (error) {
return response; return response;
} }
} }
@ -91,8 +81,8 @@ export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions
resolve(data); resolve(data);
}); });
}); });
} catch (e) { } catch (error) {
return e; return error;
} }
} }

View file

@ -19,9 +19,9 @@
], ],
"generic": [ "generic": [
{ {
"label": "Why this CEO loves n8n", "label": "Why business process automation with n8n can change your daily life",
"icon": "🧬", "icon": "🧬",
"url": "https://medium.com/n8n-io/why-this-ceo-loves-n8n-ee7d102b0948" "url": "https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/"
} }
] ]
} }

View file

@ -9,6 +9,7 @@ import {
INodePropertyOptions, INodePropertyOptions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -1098,7 +1099,7 @@ export class AwsSes implements INodeType {
if (toAddresses.length) { if (toAddresses.length) {
setParameter(params, 'Destination.ToAddresses.member', toAddresses); setParameter(params, 'Destination.ToAddresses.member', toAddresses);
} else { } else {
throw new Error('At least one "To Address" has to be added!'); throw new NodeOperationError(this.getNode(), 'At least one "To Address" has to be added!');
} }
if (additionalFields.configurationSetName) { if (additionalFields.configurationSetName) {
@ -1151,7 +1152,7 @@ export class AwsSes implements INodeType {
if (toAddresses.length) { if (toAddresses.length) {
setParameter(params, 'Destination.ToAddresses.member', toAddresses); setParameter(params, 'Destination.ToAddresses.member', toAddresses);
} else { } else {
throw new Error('At least one "To Address" has to be added!'); throw new NodeOperationError(this.getNode(), 'At least one "To Address" has to be added!');
} }
if (additionalFields.configurationSetName) { if (additionalFields.configurationSetName) {

View file

@ -22,7 +22,7 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
IDataObject, IDataObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -32,7 +32,7 @@ import {
export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('aws'); const credentials = this.getCredentials('aws');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
const endpoint = new URL(((credentials.sesEndpoint as string || '').replace('{region}', credentials.region as string) || `https://${service}.${credentials.region}.amazonaws.com`) + path); const endpoint = new URL(((credentials.sesEndpoint as string || '').replace('{region}', credentials.region as string) || `https://${service}.${credentials.region}.amazonaws.com`) + path);
@ -52,17 +52,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message; throw new NodeApiError(this.getNode(), error, { parseXml: true });
if (error.statusCode === 403) {
if (errorMessage === 'The security token included in the request is invalid.') {
throw new Error('The AWS credentials are not valid!');
} else if (errorMessage.startsWith('The request signature we calculated does not match the signature you provided')) {
throw new Error('The AWS credentials are not valid!');
}
}
throw new Error(`AWS error response [${error.statusCode}]: ${errorMessage}`);
} }
} }
@ -70,7 +60,7 @@ export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions
const response = await awsApiRequest.call(this, service, method, path, body, headers); const response = await awsApiRequest.call(this, service, method, path, body, headers);
try { try {
return JSON.parse(response); return JSON.parse(response);
} catch (e) { } catch (error) {
return response; return response;
} }
} }
@ -86,7 +76,7 @@ export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions
resolve(data); resolve(data);
}); });
}); });
} catch (e) { } catch (error) {
return response; return response;
} }
} }

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 74.375 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x="2.188" y="2.5"/><symbol id="A" overflow="visible"><g stroke="none"><path d="M16.558 12.75L0 38.591l16.558 25.841 13.227-3.324.654-44.869-13.881-3.489z" fill="#876929"/><path d="M35.049 59.786l-18.491 4.645V12.75l18.491 4.645v42.391z" fill="#d9a741"/><g fill="#876929"><path d="M32.849 21.614L35.05 80 70 62.867l-.01-43.615-8.914 1.448-28.228.913z"/><path d="M46.184 33.149l10.906-.632 10.778-19.164L40.612 0 30.439 4.364l15.745 28.785z"/></g><path d="M40.612 0l27.256 13.353-10.778 19.164L40.612 0z" fill="#d9a741"/><path d="M35.049 5.539L57.09 44.742l3.788 22.595L35.049 80l-10.46-5.131V9.64l10.46-4.101z" fill="#876929"/><path d="M69.991 19.251L70 62.867 35.05 80V5.539l22.041 39.203 12.899-25.491z" fill="#d9a741"/></g></symbol></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 74.375 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#a" x="2.188" y="2.5"/><symbol id="a" overflow="visible"><g stroke="none"><path d="M16.558 12.75L0 38.591l16.558 25.841 13.227-3.324.654-44.869-13.881-3.489z" fill="#876929"/><path d="M35.049 59.786l-18.491 4.645V12.75l18.491 4.645v42.391z" fill="#d9a741"/><g fill="#876929"><path d="M32.849 21.614L35.05 80 70 62.867l-.01-43.615-8.914 1.448-28.228.913z"/><path d="M46.184 33.149l10.906-.632 10.778-19.164L40.612 0 30.439 4.364l15.745 28.785z"/></g><path d="M40.612 0l27.256 13.353L57.09 32.517 40.612 0z" fill="#d9a741"/><path d="M35.049 5.539L57.09 44.742l3.788 22.595L35.049 80l-10.46-5.131V9.64l10.46-4.101z" fill="#876929"/><path d="M69.991 19.251L70 62.867 35.05 80V5.539l22.041 39.203L69.99 19.251z" fill="#d9a741"/></g></symbol></svg>

Before

Width:  |  Height:  |  Size: 961 B

After

Width:  |  Height:  |  Size: 959 B

View file

@ -1,5 +1,4 @@
import { import {
BINARY_ENCODING,
IExecuteFunctions, IExecuteFunctions,
} from 'n8n-core'; } from 'n8n-core';
@ -11,6 +10,8 @@ import {
INodePropertyOptions, INodePropertyOptions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -21,6 +22,10 @@ import {
awsApiRequestSOAP, awsApiRequestSOAP,
} from '../GenericFunctions'; } from '../GenericFunctions';
import {
pascalCase,
} from 'change-case';
export class AwsSqs implements INodeType { export class AwsSqs implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'AWS SQS', displayName: 'AWS SQS',
@ -266,12 +271,17 @@ export class AwsSqs implements INodeType {
loadOptions: { loadOptions: {
// Get all the available queues to display them to user so that it can be selected easily // Get all the available queues to display them to user so that it can be selected easily
async getQueues(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getQueues(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const params = [
'Version=2012-11-05',
`Action=ListQueues`,
];
let data; let data;
try { try {
// loads first 1000 queues from SQS // loads first 1000 queues from SQS
data = await awsApiRequestSOAP.call(this, 'sqs', 'GET', `?Action=ListQueues`); data = await awsApiRequestSOAP.call(this, 'sqs', 'GET', `?${params.join('&')}`);
} catch (err) { } catch (error) {
throw new Error(`AWS Error: ${err}`); throw new NodeApiError(this.getNode(), error);
} }
let queues = data.ListQueuesResponse.ListQueuesResult.QueueUrl; let queues = data.ListQueuesResponse.ListQueuesResult.QueueUrl;
@ -308,7 +318,11 @@ export class AwsSqs implements INodeType {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const queueUrl = this.getNodeParameter('queue', i) as string; const queueUrl = this.getNodeParameter('queue', i) as string;
const queuePath = new URL(queueUrl).pathname; const queuePath = new URL(queueUrl).pathname;
const params = [];
const params = [
'Version=2012-11-05',
`Action=${pascalCase(operation)}`,
];
const options = this.getNodeParameter('options', i, {}) as IDataObject; const options = this.getNodeParameter('options', i, {}) as IDataObject;
const sendInputData = this.getNodeParameter('sendInputData', i) as boolean; const sendInputData = this.getNodeParameter('sendInputData', i) as boolean;
@ -349,11 +363,11 @@ export class AwsSqs implements INodeType {
const item = items[i]; const item = items[i];
if (item.binary === undefined) { if (item.binary === undefined) {
throw new Error('No binary data set. So message attribute cannot be added!'); throw new NodeOperationError(this.getNode(), 'No binary data set. So message attribute cannot be added!');
} }
if (item.binary[dataPropertyName] === undefined) { if (item.binary[dataPropertyName] === undefined) {
throw new Error(`The binary property "${dataPropertyName}" does not exist. So message attribute cannot be added!`); throw new NodeOperationError(this.getNode(), `The binary property "${dataPropertyName}" does not exist. So message attribute cannot be added!`);
} }
const binaryData = item.binary[dataPropertyName].data; const binaryData = item.binary[dataPropertyName].data;
@ -373,9 +387,9 @@ export class AwsSqs implements INodeType {
let responseData; let responseData;
try { try {
responseData = await awsApiRequestSOAP.call(this, 'sqs', 'GET', `${queuePath}/?Action=${operation}&` + params.join('&')); responseData = await awsApiRequestSOAP.call(this, 'sqs', 'GET', `${queuePath}?${params.join('&')}`);
} catch (err) { } catch (error) {
throw new Error(`AWS Error: ${err}`); throw new NodeApiError(this.getNode(), error);
} }
const result = responseData.SendMessageResponse.SendMessageResult; const result = responseData.SendMessageResponse.SendMessageResult;

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-5 0 85 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x="2.188" y="2.5"/><symbol id="A" overflow="visible"><g fill="#876929" stroke="none"><path d="M0 25.938L.021 63.44 35 80l34.98-16.56v-9.368l-29.745-3.508.02-21.127L70 25.948V16.57L35 0 0 16.56v9.378z"/><path d="M.021 54.062l34.98 9.942V80L.021 63.44v-9.378z"/><path d="M4.465 65.549L0 63.431V16.56l4.475-2.109-.01 51.098z"/></g><path d="M40.255 50.564l-35.79 4.762.01-30.661 35.78 4.772v21.127zM70 25.948l-35-9.951V0l35 16.57v9.378zm-.02 28.124L35 64.004V80l34.98-16.56v-9.368z" stroke="none" fill="#d9a741"/><path d="M22.109 48.581l12.892 1.526V29.815L22.109 31.36v17.221zM9.125 47.065l8.365.982V31.924l-8.365 1.001v14.14z" stroke="none" fill="#876929"/><path d="M4.475 24.665L35 15.996l35 9.951-29.745 3.489-35.78-4.772z" fill="#624a1e" stroke="none"/><path d="M4.465 55.326L35 64.004l34.98-9.932-29.724-3.508-35.79 4.762z" fill="#fad791" stroke="none"/><path d="M69.98 45.918L35 50.107V29.815l34.98 4.218v11.885z" fill="#d9a741" stroke="none"/></symbol></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-5 0 85 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#a" x="2.188" y="2.5"/><symbol id="a" overflow="visible"><g fill="#876929" stroke="none"><path d="M0 25.938L.021 63.44 35 80l34.98-16.56v-9.368l-29.745-3.508.02-21.127L70 25.948V16.57L35 0 0 16.56v9.378z"/><path d="M.021 54.062l34.98 9.942V80L.021 63.44v-9.378z"/><path d="M4.465 65.549L0 63.431V16.56l4.475-2.109-.01 51.098z"/></g><path d="M40.255 50.564l-35.79 4.762.01-30.661 35.78 4.772v21.127zM70 25.948l-35-9.951V0l35 16.57v9.378zm-.02 28.124L35 64.004V80l34.98-16.56v-9.368z" stroke="none" fill="#d9a741"/><path d="M22.109 48.581l12.892 1.526V29.815L22.109 31.36v17.221zM9.125 47.065l8.365.982V31.924l-8.365 1.001v14.14z" stroke="none" fill="#876929"/><path d="M4.475 24.665L35 15.996l35 9.951-29.745 3.489-35.78-4.772z" fill="#624a1e" stroke="none"/><path d="M4.465 55.326L35 64.004l34.98-9.932-29.724-3.508-35.79 4.762z" fill="#fad791" stroke="none"/><path d="M69.98 45.918L35 50.107V29.815l34.98 4.218v11.885z" fill="#d9a741" stroke="none"/></symbol></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -19,7 +19,18 @@
"generic": [ "generic": [
{ {
"label": "Automate Designs with Bannerbear and n8n", "label": "Automate Designs with Bannerbear and n8n",
"url": "https://medium.com/n8n-io/automate-designs-with-bannerbear-and-n8n-2b64c94b54db" "icon": "🎨",
"url": "https://n8n.io/blog/automate-designs-with-bannerbear-and-n8n/"
},
{
"label": "Automating Conference Organization Processes with n8n",
"icon": "🙋‍♀️",
"url": "https://n8n.io/blog/automating-conference-organization-processes-with-n8n/"
},
{
"label": "Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin",
"icon": "🎖",
"url": "https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/"
} }
] ]
} }

View file

@ -11,6 +11,8 @@ import {
IDataObject, IDataObject,
IHookFunctions, IHookFunctions,
IWebhookFunctions, IWebhookFunctions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -22,7 +24,7 @@ export async function bannerbearApiRequest(this: IExecuteFunctions | IWebhookFun
const credentials = this.getCredentials('bannerbearApi'); const credentials = this.getCredentials('bannerbearApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
const options: OptionsWithUri = { const options: OptionsWithUri = {
@ -46,12 +48,7 @@ export async function bannerbearApiRequest(this: IExecuteFunctions | IWebhookFun
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
if (error.response && error.response.body && error.response.body.message) { throw new NodeApiError(this.getNode(), error);
// Try to return the error prettier
//@ts-ignore
throw new Error(`Bannerbear error response [${error.statusCode}]: ${error.response.body.message}`);
}
throw error;
} }
} }

View file

@ -7,6 +7,7 @@ import {
IDataObject, IDataObject,
IHookFunctions, IHookFunctions,
IWebhookFunctions, IWebhookFunctions,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -18,7 +19,7 @@ export async function createDatapoint(this: IExecuteFunctions | IWebhookFunction
const credentials = this.getCredentials('beeminderApi'); const credentials = this.getCredentials('beeminderApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints.json`; const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints.json`;
@ -30,7 +31,7 @@ export async function getAllDatapoints(this: IExecuteFunctions | IHookFunctions
const credentials = this.getCredentials('beeminderApi'); const credentials = this.getCredentials('beeminderApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints.json`; const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints.json`;
@ -46,7 +47,7 @@ export async function updateDatapoint(this: IExecuteFunctions | IWebhookFunction
const credentials = this.getCredentials('beeminderApi'); const credentials = this.getCredentials('beeminderApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints/${data.datapointId}.json`; const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints/${data.datapointId}.json`;
@ -58,7 +59,7 @@ export async function deleteDatapoint(this: IExecuteFunctions | IWebhookFunction
const credentials = this.getCredentials('beeminderApi'); const credentials = this.getCredentials('beeminderApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints/${data.datapointId}.json`; const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints/${data.datapointId}.json`;

View file

@ -10,6 +10,7 @@ import {
INodePropertyOptions, INodePropertyOptions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -308,7 +309,7 @@ export class Beeminder implements INodeType {
const credentials = this.getCredentials('beeminderApi'); const credentials = this.getCredentials('beeminderApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
const endpoint = `/users/${credentials.user}/goals.json`; const endpoint = `/users/${credentials.user}/goals.json`;

View file

@ -11,6 +11,7 @@ import {
IDataObject, IDataObject,
IHookFunctions, IHookFunctions,
IWebhookFunctions, IWebhookFunctions,
NodeApiError,
} from 'n8n-workflow'; } from 'n8n-workflow';
const BEEMINDER_URI = 'https://www.beeminder.com/api/v1'; const BEEMINDER_URI = 'https://www.beeminder.com/api/v1';
@ -40,10 +41,7 @@ export async function beeminderApiRequest(this: IExecuteFunctions | IWebhookFunc
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
if (error?.message) { throw new NodeApiError(this.getNode(), error);
throw new Error(`Beeminder error response [${error.statusCode}]: ${error.message}`);
}
throw error;
} }
} }

View file

@ -280,7 +280,7 @@ export class BitbucketTrigger implements INodeType {
} }
try { try {
await bitbucketApiRequest.call(this, 'GET', endpoint); await bitbucketApiRequest.call(this, 'GET', endpoint);
} catch (e) { } catch (error) {
return false; return false;
} }
return true; return true;

View file

@ -5,12 +5,12 @@ import {
IHookFunctions, IHookFunctions,
ILoadOptionsFunctions, ILoadOptionsFunctions,
} from 'n8n-core'; } from 'n8n-core';
import { IDataObject } from 'n8n-workflow'; import { IDataObject, NodeApiError, NodeOperationError, } from 'n8n-workflow';
export async function bitbucketApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any export async function bitbucketApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('bitbucketApi'); const credentials = this.getCredentials('bitbucketApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
let options: OptionsWithUri = { let options: OptionsWithUri = {
method, method,
@ -30,8 +30,8 @@ export async function bitbucketApiRequest(this: IHookFunctions | IExecuteFunctio
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (err) { } catch (error) {
throw new Error('Bitbucket Error: ' + err.message); throw new NodeApiError(this.getNode(), error);
} }
} }

View file

@ -10,7 +10,7 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
IDataObject, IDataObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
export async function bitlyApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any export async function bitlyApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
@ -32,7 +32,7 @@ export async function bitlyApiRequest(this: IHookFunctions | IExecuteFunctions |
if (authenticationMethod === 'accessToken') { if (authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('bitlyApi'); const credentials = this.getCredentials('bitlyApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
} }
options.headers = { Authorization: `Bearer ${credentials.accessToken}`}; options.headers = { Authorization: `Bearer ${credentials.accessToken}`};
@ -42,15 +42,7 @@ export async function bitlyApiRequest(this: IHookFunctions | IExecuteFunctions |
return await this.helpers.requestOAuth2!.call(this, 'bitlyOAuth2Api', options, { tokenType: 'Bearer' }); return await this.helpers.requestOAuth2!.call(this, 'bitlyOAuth2Api', options, { tokenType: 'Bearer' });
} }
} catch(error) { } catch(error) {
throw new NodeApiError(this.getNode(), error);
if (error.response && error.response.body && error.response.body.message) {
// Try to return the error prettier
const errorBody = error.response.body;
throw new Error(`Bitly error response [${error.statusCode}]: ${errorBody.message}`);
}
// Expected error data did not get returned so throw the actual error
throw error;
} }
} }

View file

@ -0,0 +1,20 @@
{
"node": "n8n-nodes-base.bitwarden",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Data & Storage"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/bitwarden"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.bitwarden/"
}
]
}
}

View file

@ -8,6 +8,7 @@ import {
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -177,7 +178,7 @@ export class Bitwarden implements INodeType {
const updateFields = this.getNodeParameter('updateFields', i) as CollectionUpdateFields; const updateFields = this.getNodeParameter('updateFields', i) as CollectionUpdateFields;
if (isEmpty(updateFields)) { if (isEmpty(updateFields)) {
throw new Error(`Please enter at least one field to update for the ${resource}.`); throw new NodeOperationError(this.getNode(), `Please enter at least one field to update for the ${resource}.`);
} }
const { groups, externalId } = updateFields; const { groups, externalId } = updateFields;
@ -308,7 +309,7 @@ export class Bitwarden implements INodeType {
const updateFields = this.getNodeParameter('updateFields', i) as GroupUpdateFields; const updateFields = this.getNodeParameter('updateFields', i) as GroupUpdateFields;
if (isEmpty(updateFields)) { if (isEmpty(updateFields)) {
throw new Error(`Please enter at least one field to update for the ${resource}.`); throw new NodeOperationError(this.getNode(), `Please enter at least one field to update for the ${resource}.`);
} }
// set defaults for `name` and `accessAll`, required by Bitwarden but optional in n8n // set defaults for `name` and `accessAll`, required by Bitwarden but optional in n8n
@ -452,7 +453,7 @@ export class Bitwarden implements INodeType {
const updateFields = this.getNodeParameter('updateFields', i) as MemberUpdateFields; const updateFields = this.getNodeParameter('updateFields', i) as MemberUpdateFields;
if (isEmpty(updateFields)) { if (isEmpty(updateFields)) {
throw new Error(`Please enter at least one field to update for the ${resource}.`); throw new NodeOperationError(this.getNode(), `Please enter at least one field to update for the ${resource}.`);
} }
const { accessAll, collections, externalId, type } = updateFields; const { accessAll, collections, externalId, type } = updateFields;

View file

@ -6,6 +6,7 @@ import {
IDataObject, IDataObject,
ILoadOptionsFunctions, ILoadOptionsFunctions,
INodePropertyOptions, INodePropertyOptions,
NodeApiError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -48,17 +49,7 @@ export async function bitwardenApiRequest(
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
throw new NodeApiError(this.getNode(), error);
if (error.statusCode === 404) {
throw new Error('Bitwarden error response [404]: Not found');
}
if (error?.response?.body?.Message) {
const message = error?.response?.body?.Message;
throw new Error(`Bitwarden error response [${error.statusCode}]: ${message}`);
}
//TODO handle Errors array
throw error;
} }
} }
@ -93,7 +84,7 @@ export async function getAccessToken(
const { access_token } = await this.helpers.request!(options); const { access_token } = await this.helpers.request!(options);
return access_token; return access_token;
} catch (error) { } catch (error) {
throw error; throw new NodeApiError(this.getNode(), error);
} }
} }

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 55 66" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x=".5" y=".5"/><symbol id="A" overflow="visible"><path d="M53.333 2.667v32c0 2.388-.465 4.756-1.396 7.103s-2.084 4.43-3.458 6.25-3.015 3.59-4.917 5.312-3.66 3.153-5.272 4.292l-5.04 3.23-3.73 2.062-1.77.834c-.333.166-.695.25-1.083.25a2.4 2.4 0 0 1-1.083-.25l-1.77-.834-3.73-2.062-5.042-3.23c-1.61-1.14-3.368-2.57-5.27-4.292s-3.54-3.492-4.916-5.312-2.528-3.903-3.46-6.25S0 37.055 0 34.667v-32A2.56 2.56 0 0 1 .791.792 2.56 2.56 0 0 1 2.666 0h48c.72 0 1.346.264 1.874.792a2.56 2.56 0 0 1 .792 1.875m-8 32V8H26.666v47.375c3.305-1.75 6.264-3.653 8.875-5.708 6.527-5.11 9.79-10.11 9.79-15" stroke="none" fill="#3c8dbc" fill-rule="nonzero"/></symbol></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 55 66" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#a" x=".5" y=".5"/><symbol id="a" overflow="visible"><path d="M53.333 2.667v32c0 2.388-.465 4.756-1.396 7.103s-2.084 4.43-3.458 6.25-3.015 3.59-4.917 5.312-3.66 3.153-5.272 4.292l-5.04 3.23-3.73 2.062-1.77.834c-.333.166-.695.25-1.083.25a2.4 2.4 0 01-1.083-.25l-1.77-.834-3.73-2.062-5.042-3.23c-1.61-1.14-3.368-2.57-5.27-4.292s-3.54-3.492-4.916-5.312-2.528-3.903-3.46-6.25S0 37.055 0 34.667v-32A2.56 2.56 0 01.791.792 2.56 2.56 0 012.666 0h48c.72 0 1.346.264 1.874.792a2.56 2.56 0 01.792 1.875m-8 32V8H26.666v47.375c3.305-1.75 6.264-3.653 8.875-5.708 6.527-5.11 9.79-10.11 9.79-15" stroke="none" fill="#3c8dbc" fill-rule="nonzero"/></symbol></svg>

Before

Width:  |  Height:  |  Size: 866 B

After

Width:  |  Height:  |  Size: 859 B

Some files were not shown because too many files have changed in this diff Show more