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",
"dev": "lerna exec npm run dev --parallel",
"clean:dist": "lerna exec -- rimraf ./dist",
"optimize-svg": "find ./packages -name '*.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
"start": "run-script-os",
"start:default": "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.
## 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
### What changed?

View file

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

View file

@ -127,11 +127,22 @@ export class Worker extends Command {
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 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 });
let workflowExecute: WorkflowExecute;

View file

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

View file

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

View file

@ -20,7 +20,6 @@ import {
} from 'n8n-core';
import {
IDataObject,
IExecuteData,
IGetExecutePollFunctions,
IGetExecuteTriggerFunctions,
@ -283,23 +282,11 @@ export class ActiveWorkflowRunner {
const node = workflow.getNode(webhookData.node) as INode;
node.name = webhookData.node;
path = node.parameters.path as string;
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;
path = webhookData.path;
const webhook = {
workflowId: webhookData.workflowId,
webhookPath: NodeHelpers.getNodeWebhookPath(workflow.id as string, node, path, isFullPath),
webhookPath: path,
node: node.name,
method: webhookData.httpMethod,
} as IWebhookDb;
@ -317,7 +304,6 @@ export class ActiveWorkflowRunner {
}
try {
await Db.collections.Webhook?.insert(webhook);
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, false);

View file

@ -1,24 +1,24 @@
import {
ExecutionError,
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialsEncrypted,
ICredentialType,
IDataObject,
IExecutionError,
IRun,
IRunData,
IRunExecutionData,
ITaskData,
IWorkflowBase as IWorkflowBaseWorkflow,
IWorkflowCredentials,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
IDeferredPromise,
IDeferredPromise, WorkflowExecute,
} from 'n8n-core';
import * as PCancelable from 'p-cancelable';
import { ObjectID, Repository } from 'typeorm';
@ -374,10 +374,10 @@ export interface ITransferNodeTypes {
export interface IWorkflowErrorData {
[key: string]: IDataObject | string | number | IExecutionError;
[key: string]: IDataObject | string | number | ExecutionError;
execution: {
id?: string;
error: IExecutionError;
error: ExecutionError;
lastNodeExecuted: string;
mode: WorkflowExecuteMode;
};
@ -411,3 +411,9 @@ export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExe
executionId: string;
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 requestPromise from 'request-promise-native';
import { createHmac } from 'crypto';
import { compare } from 'bcryptjs';
import { compare } from '@node-rs/bcrypt';
import * as promClient from 'prom-client';
import {
@ -132,6 +132,7 @@ class App {
protocol: string;
sslKey: string;
sslCert: string;
payloadSizeMax: number;
presetCredentialsLoaded: boolean;
@ -145,6 +146,7 @@ class App {
this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean;
this.executionTimeout = config.get('executions.timeout') 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.restEndpoint = config.get('endpoints.rest') as string;
@ -369,7 +371,7 @@ class App {
// Support application/json type post data
this.app.use(bodyParser.json({
limit: '16mb', verify: (req, res, buf) => {
limit: this.payloadSizeMax + 'mb', verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},
@ -378,7 +380,7 @@ class App {
// Support application/xml type post data
// @ts-ignore
this.app.use(bodyParser.xml({
limit: '16mb', xmlParseOptions: {
limit: this.payloadSizeMax + 'mb', xmlParseOptions: {
normalize: true, // Trim whitespace inside text nodes
normalizeTags: true, // Transform tags to lowercase
explicitArray: false, // Only put properties in array if length > 1
@ -386,7 +388,7 @@ class App {
}));
this.app.use(bodyParser.text({
limit: '16mb', verify: (req, res, buf) => {
limit: this.payloadSizeMax + 'mb', verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
},

View file

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

View file

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

View file

@ -27,12 +27,12 @@ import {
} from 'n8n-core';
import {
IDataObject,
IExecutionError,
ExecutionError,
IRun,
Workflow,
WorkflowExecuteMode,
WorkflowHooks,
WorkflowOperationError,
} from 'n8n-workflow';
import * as config from '../config';
@ -78,13 +78,13 @@ export class WorkflowRunner {
/**
* The process did error
*
* @param {IExecutionError} error
* @param {ExecutionError} error
* @param {Date} startedAt
* @param {WorkflowExecuteMode} executionMode
* @param {string} executionId
* @memberof WorkflowRunner
*/
processError(error: IExecutionError, startedAt: Date, executionMode: WorkflowExecuteMode, executionId: string) {
processError(error: ExecutionError, startedAt: Date, executionMode: WorkflowExecuteMode, executionId: string) {
const fullRunData: IRun = {
data: {
resultData: {
@ -158,8 +158,22 @@ export class WorkflowRunner {
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 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
const executionId = await this.activeExecutions.add(data, undefined);
@ -184,14 +198,7 @@ export class WorkflowRunner {
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
// Soft timeout to stop workflow execution after current running node
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) {
if (workflowTimeout > 0) {
const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
executionTimeout = setTimeout(() => {
this.activeExecutions.stopExecution(executionId, 'timeout');
@ -250,9 +257,7 @@ export class WorkflowRunner {
const fullRunData :IRun = {
data: {
resultData: {
error: {
message: 'Workflow has been canceled!',
} as IExecutionError,
error: new WorkflowOperationError('Workflow has been canceled!'),
runData: {},
},
},
@ -282,7 +287,6 @@ export class WorkflowRunner {
* the database. *
*************************************************/
let watchDogInterval: NodeJS.Timeout | undefined;
let resolved = false;
const watchDog = new Promise((res) => {
watchDogInterval = setInterval(async () => {
@ -303,28 +307,9 @@ export class WorkflowRunner {
}
};
await new Promise((res, rej) => {
jobData.then((data) => {
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);
}
});
});
await Promise.race([jobData, watchDog]);
clearWatchdogInterval();
} else {
await jobData;
}
@ -366,7 +351,7 @@ export class WorkflowRunner {
// We don't want errors here to crash n8n. Just log and proceed.
console.log('Error removing saved execution from database. More details: ', err);
}
resolve(runData);
});
@ -385,7 +370,7 @@ export class WorkflowRunner {
* @memberof WorkflowRunner
*/
async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
const startedAt = new Date();
let startedAt = new Date();
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
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).nodeTypeData = nodeTypeData;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = credentialsOverwrites;
@ -441,64 +425,93 @@ export class WorkflowRunner {
// Start timeout for the execution
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) {
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 timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
executionTimeout = setTimeout(() => {
this.activeExecutions.stopExecution(executionId, 'timeout');
const processTimeoutFunction = (timeout: number) => {
this.activeExecutions.stopExecution(executionId, 'timeout');
executionTimeout = setTimeout(() => subprocess.kill(), Math.max(timeout * 0.2, 5000)); // minimum 5 seconds
};
executionTimeout = setTimeout(() => subprocess.kill(), Math.max(timeout * 0.2, 5000)); // minimum 5 seconds
}, timeout);
if (workflowTimeout > 0) {
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
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);
this.activeExecutions.remove(executionId!, message.data.runData);
} else if (message.type === 'processError') {
clearTimeout(executionTimeout);
const executionError = message.data.executionError as IExecutionError;
const executionError = message.data.executionError as ExecutionError;
this.processError(executionError, startedAt, data.executionMode, executionId);
} else if (message.type === 'processHook') {
this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook);
} else if (message.type === 'timeout') {
// 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);
} else if (message.type === 'startExecution') {
const executionId = await this.activeExecutions.add(message.data.runData);
childExecutionIds.push(executionId);
subprocess.send({ type: 'executionId', data: {executionId} } as IProcessMessage);
} 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);
}
});
// 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'){
// 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);
} else if (code !== 0) {
// Process did exit with error code, so something went wrong.
const executionError = {
message: 'Workflow execution process did crash for an unknown reason!',
} as IExecutionError;
const executionError = new WorkflowOperationError('Workflow execution process did crash for an unknown reason!');
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);
});

View file

@ -4,6 +4,7 @@ import {
CredentialTypes,
Db,
ExternalHooks,
IWorkflowExecuteProcess,
IWorkflowExecutionDataProcessWithExecution,
NodeTypes,
WorkflowExecuteAdditionalData,
@ -16,10 +17,9 @@ import {
} from 'n8n-core';
import {
ExecutionError,
IDataObject,
IExecuteData,
IExecuteWorkflowInfo,
IExecutionError,
INodeExecutionData,
INodeType,
INodeTypeData,
@ -30,6 +30,7 @@ import {
IWorkflowExecuteHooks,
Workflow,
WorkflowHooks,
WorkflowOperationError,
} from 'n8n-workflow';
import * as config from '../config';
@ -40,6 +41,9 @@ export class WorkflowRunnerProcess {
workflow: Workflow | undefined;
workflowExecute: WorkflowExecute | undefined;
executionIdCallback: (executionId: string) => void | undefined;
childExecutions: {
[key: string]: IWorkflowExecuteProcess,
} = {};
static async stopProcess() {
setTimeout(() => {
@ -107,8 +111,18 @@ export class WorkflowRunnerProcess {
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 });
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();
const executeWorkflowFunction = additionalData.executeWorkflow;
@ -123,14 +137,20 @@ export class WorkflowRunnerProcess {
});
let result: IRun;
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) {
await sendToParentProcess('finishExecution', { executionId });
// Throw same error we had
throw e;
delete this.childExecutions[executionId];
// Throw same error we had
throw e;
}
await sendToParentProcess('finishExecution', { executionId, result });
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(result);
return returnData!.data!.main;
@ -254,6 +274,8 @@ const workflowRunner = new WorkflowRunnerProcess();
process.on('message', async (message: IProcessMessage) => {
try {
if (message.type === 'startWorkflow') {
await sendToParentProcess('start', {});
const runData = await workflowRunner.runWorkflow(message.data);
await sendToParentProcess('end', {
@ -267,10 +289,22 @@ process.on('message', async (message: IProcessMessage) => {
let runData: IRun;
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
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
await workflowRunner.workflowExecute.processSuccessExecution(workflowRunner.startedAt, workflowRunner.workflow!, timeOutError);
@ -301,11 +335,14 @@ process.on('message', async (message: IProcessMessage) => {
workflowRunner.executionIdCallback(message.data.executionId);
}
} catch (error) {
// Catch all uncaught errors and forward them to parent process
const executionError = {
message: error.message,
stack: error.stack,
} as IExecutionError;
...error,
name: error!.name || 'Error',
message: error!.message,
stack: error!.stack,
} as ExecutionError;
await sendToParentProcess('processError', {
executionError,

View file

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

View file

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

View file

@ -32,6 +32,7 @@ import {
IWorkflowExecuteAdditionalData,
IWorkflowMetadata,
NodeHelpers,
NodeOperationError,
NodeParameterValue,
Workflow,
WorkflowActivateMode,
@ -51,6 +52,9 @@ import { createHmac } from 'crypto';
import { fromBuffer } from 'file-type';
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
@ -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
const nodeType = workflow.nodeTypes.getByName(node.type);
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) {
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);
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) {
@ -333,10 +337,10 @@ export function getCredentials(workflow: Workflow, node: INode, type: string, ad
if (nodeCredentialDescription.required === true) {
// Credentials are required so error
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]) {
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 {
// Credentials are not required so resolve with undefined
@ -576,7 +580,7 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio
},
helpers: {
prepareBinaryData,
request: requestPromise,
request: requestPromiseWithDefaults,
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);
},
@ -642,7 +646,7 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi
},
helpers: {
prepareBinaryData,
request: requestPromise,
request: requestPromiseWithDefaults,
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);
},
@ -738,7 +742,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
prepareOutputData: NodeHelpers.prepareOutputData,
helpers: {
prepareBinaryData,
request: requestPromise,
request: requestPromiseWithDefaults,
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);
},
@ -836,7 +840,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
},
helpers: {
prepareBinaryData,
request: requestPromise,
request: requestPromiseWithDefaults,
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);
},
@ -892,7 +896,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio
return additionalData.restApiUrl;
},
helpers: {
request: requestPromise,
request: requestPromiseWithDefaults,
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);
},
@ -962,7 +966,7 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio
return workflow.getStaticData(type, node);
},
helpers: {
request: requestPromise,
request: requestPromiseWithDefaults,
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);
},
@ -1062,7 +1066,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi
prepareOutputData: NodeHelpers.prepareOutputData,
helpers: {
prepareBinaryData,
request: requestPromise,
request: requestPromiseWithDefaults,
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);
},

View file

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

View file

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

View file

@ -428,3 +428,20 @@ export interface ITimeoutHMS {
}
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:
</el-col>
<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
v-for="item in credentialTypes"
:key="item.name"
@ -198,6 +198,9 @@ export default mixins(
this.credentialData = currentCredentials;
} else {
Vue.nextTick(() => {
(this.$refs.credentialsDropdown as HTMLDivElement).focus();
});
if (this.credentialType || this.setCredentialType) {
const credentialType = this.$store.getters.credentialType(this.credentialType || this.setCredentialType);
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>
</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">
<template slot="title">
<font-awesome-icon icon="network-wired"/>&nbsp;
@ -152,6 +168,22 @@
</el-menu-item>
</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>
</div>
@ -167,6 +199,7 @@ import {
IExecutionResponse,
IExecutionsStopData,
IWorkflowDataUpdate,
IMenuItem,
} from '../Interface';
import About from '@/components/About.vue';
@ -266,6 +299,12 @@ export default mixins(
workflowRunning (): boolean {
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: {
clearExecutionData () {
@ -533,6 +572,11 @@ export default mixins(
.el-menu-item {
a {
color: #666;
&.primary-item {
color: $--color-primary;
vertical-align: baseline;
}
}
&.logo-item {

View file

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

View file

@ -207,8 +207,19 @@ export const pushConnection = mixins(
if (runDataExecuted.finished !== true) {
// There was a problem with executing the workflow
let errorMessage = 'There was a problem executing the workflow!';
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.$showMessage({

View file

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

View file

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

View file

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

View file

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

@ -48,6 +48,63 @@ export class MySql implements ICredentialType {
type: 'number' as NodePropertyTypes,
default: 10000,
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 {
ICredentialType,
IDisplayOptions,
NodePropertyTypes,
} from 'n8n-workflow';
@ -51,8 +52,22 @@ export class RabbitMQ implements ICredentialType {
default: false,
},
{
displayName: 'Client Certificate',
name: 'cert',
displayName: 'Passwordless',
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,
typeOptions: {
password: true,
@ -65,6 +80,26 @@ export class RabbitMQ implements ICredentialType {
},
},
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.',
},
{
@ -79,6 +114,9 @@ export class RabbitMQ implements ICredentialType {
ssl: [
true,
],
passwordless: [
true,
],
},
},
default: '',
@ -96,31 +134,13 @@ export class RabbitMQ implements ICredentialType {
ssl: [
true,
],
},
},
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: [
passwordless: [
true,
],
},
},
default: '',
description: 'SSL CA Certificates to use.',
description: 'SSL passphrase to use.',
},
// {
// displayName: 'Client ID',

View file

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

View file

@ -3,7 +3,6 @@ import {
NodePropertyTypes,
} from 'n8n-workflow';
export class WebflowOAuth2Api implements ICredentialType {
name = 'webflowOAuth2Api';
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",
"nodeVersion": "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": [
"Marketing & Content"
],

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import {
} from 'n8n-core';
import {
IDataObject, ILoadOptionsFunctions, INodeProperties,
IDataObject, ILoadOptionsFunctions, INodeProperties, NodeApiError, NodeOperationError,
} from 'n8n-workflow';
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
const credentials = this.getCredentials('activeCampaignApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
if (query === undefined) {
@ -53,7 +53,7 @@ export async function activeCampaignApiRequest(this: IHookFunctions | IExecuteFu
const responseData = await this.helpers.request!(options);
if (responseData.success === false) {
throw new Error(`ActiveCampaign error response: ${responseData.error} (${responseData.error_info})`);
throw new NodeApiError(this.getNode(), responseData);
}
if (dataKey === undefined) {
@ -63,13 +63,7 @@ export async function activeCampaignApiRequest(this: IHookFunctions | IExecuteFu
}
} catch (error) {
if (error.statusCode === 403) {
// 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;
throw new NodeApiError(this.getNode(), error);
}
}

View file

@ -6,7 +6,7 @@ import {
ILoadOptionsFunctions,
IWebhookFunctions,
} 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
const authenticationMethod = this.getNodeParameter('authentication', 0);
@ -27,7 +27,7 @@ export async function acuitySchedulingApiRequest(this: IHookFunctions | IExecute
if (authenticationMethod === 'apiKey') {
const credentials = this.getCredentials('acuitySchedulingApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
options.auth = {
@ -42,6 +42,6 @@ export async function acuitySchedulingApiRequest(this: IHookFunctions | IExecute
return await this.helpers.requestOAuth2!.call(this, 'acuitySchedulingOAuth2Api', options, true);
}
} catch (error) {
throw new Error('Acuity Scheduling Error: ' + error.message);
throw new NodeApiError(this.getNode(), error);
}
}

View file

@ -8,6 +8,7 @@ import {
INodeType,
INodeTypeDescription,
IWebhookResponseData,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -187,7 +188,7 @@ export class AffinityTrigger implements INodeType {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
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[];

View file

@ -12,6 +12,8 @@ import {
IDataObject,
IHookFunctions,
IWebhookFunctions,
NodeApiError,
NodeOperationError,
} 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
@ -19,7 +21,7 @@ export async function affinityApiRequest(this: IExecuteFunctions | IWebhookFunct
const credentials = this.getCredentials('affinityApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
const apiKey = `:${credentials.apiKey}`;
@ -47,11 +49,7 @@ export async function affinityApiRequest(this: IExecuteFunctions | IWebhookFunct
try {
return await this.helpers.request!(options);
} catch (error) {
if (error.response) {
const errorMessage = error.response.body.message || error.response.body.description || error.message;
throw new Error(`Affinity error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;
throw new NodeApiError(this.getNode(), error);
}
}

View file

@ -3,7 +3,8 @@ import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -149,7 +150,7 @@ export class AgileCrm implements INodeType {
Object.assign(body, JSON.parse(additionalFieldsJson));
} 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));
} else {
throw new Error('Additional fields must be a valid JSON');
throw new NodeOperationError(this.getNode(), 'Additional fields must be a valid JSON');
}
}
} else {
@ -483,7 +484,7 @@ export class AgileCrm implements INodeType {
if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson));
} 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));
} 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';
import {
IDataObject,
IDataObject, NodeApiError,
} from 'n8n-workflow';
import { IContactUpdate } from './ContactInterface';
@ -39,7 +39,7 @@ export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunction
try {
return await this.helpers.request!(options);
} 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) {
if (successfulUpdates.length === 0) {
throw new Error(`AgileCRM error response: ${error.message}`);
throw new NodeApiError(this.getNode(), error);
} 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": [
{
"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",
"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,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -636,7 +637,7 @@ export class Airtable implements INodeType {
}
} 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)];

View file

@ -7,6 +7,7 @@ import {
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -170,7 +171,7 @@ export class AirtableTrigger implements INodeType {
if (Array.isArray(records) && records.length) {
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) {

View file

@ -13,6 +13,8 @@ import {
IDataObject,
INodeExecutionData,
IPollFunctions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
@ -41,7 +43,7 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa
const credentials = this.getCredentials('airtableApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
query = query || {};
@ -73,23 +75,7 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa
try {
return await this.helpers.request!(options);
} catch (error) {
if (error.statusCode === 401) {
// 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;
throw new NodeApiError(this.getNode(), error);
}
}

View file

@ -1,21 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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>
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 682 B

View file

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

View file

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

View file

@ -17,4 +17,4 @@
}
]
}
}
}

View file

@ -9,6 +9,7 @@ import {
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -466,7 +467,7 @@ export class ApiTemplateIo implements INodeType {
if (overrideJson !== '') {
const data = validateJSON(overrideJson);
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;
}
@ -523,14 +524,14 @@ export class ApiTemplateIo implements INodeType {
if (jsonParameters === false) {
const properties = (this.getNodeParameter('propertiesUi', i) as IDataObject || {}).propertyValues as IDataObject[] || [];
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 }), {});
} else {
const propertiesJson = this.getNodeParameter('propertiesJson', i) as string;
data = validateJSON(propertiesJson);
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,
ILoadOptionsFunctions,
} from 'n8n-core';
import { NodeApiError } from 'n8n-workflow';
export async function apiTemplateIoApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
@ -42,14 +43,11 @@ export async function apiTemplateIoApiRequest(
try {
const response = await this.helpers.request!(options);
if (response.status === 'error') {
throw new Error(response.message);
throw new NodeApiError(this.getNode(), response.message);
}
return response;
} catch (error) {
if (error?.response?.body?.message) {
throw new Error(`APITemplate.io error response [${error.statusCode}]: ${error.response.body.message}`);
}
throw error;
throw new NodeApiError(this.getNode(), error);
}
}

View file

@ -1,102 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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>
<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>

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/"
}
],
"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,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -1639,7 +1641,7 @@ export class Asana implements INodeType {
const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {});
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[] = [];
@ -1674,7 +1676,7 @@ export class Asana implements INodeType {
const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {});
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[] = [];
@ -1711,7 +1713,7 @@ export class Asana implements INodeType {
// to retrieve the teams from an organization just work with workspaces that are an organization
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`;
@ -1750,15 +1752,15 @@ export class Asana implements INodeType {
let taskData;
try {
taskData = await asanaApiRequest.call(this, 'GET', `/tasks/${taskId}`, {});
} catch (e) {
throw new Error(`Could not find task with id "${taskId}" so tags could not be loaded.`);
} catch (error) {
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 responseData = await asanaApiRequest.call(this, 'GET', endpoint, {}, { workspace });
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[] = [];
@ -1790,7 +1792,7 @@ export class Asana implements INodeType {
const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {});
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[] = [];

View file

@ -10,6 +10,7 @@ import {
INodeType,
INodeTypeDescription,
IWebhookResponseData,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -155,7 +156,7 @@ export class AsanaTrigger implements INodeType {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
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;
@ -189,7 +190,7 @@ export class AsanaTrigger implements INodeType {
try {
await asanaApiRequest.call(this, 'DELETE', endpoint, body);
} catch (e) {
} catch (error) {
return false;
}

View file

@ -11,6 +11,8 @@ import {
import {
IDataObject,
INodePropertyOptions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -43,7 +45,7 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions |
const credentials = this.getCredentials('asanaApi');
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}`;
@ -54,25 +56,7 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions |
return await this.helpers.requestOAuth2.call(this, 'asanaOAuth2Api', options);
}
} catch (error) {
if (error.statusCode === 401) {
// 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;
throw new NodeApiError(this.getNode(), error);
}
}

View file

@ -1,18 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<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>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 951 B

View file

@ -9,7 +9,7 @@ import {
} from 'n8n-core';
import {
IDataObject,
IDataObject, NodeApiError,
} 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
@ -40,14 +40,7 @@ export async function automizyApiRequest(this: IExecuteFunctions | IExecuteSingl
//@ts-ignore
return await this.helpers.request.call(this, options);
} catch (error) {
if (error.response && error.response.body) {
throw new Error(
`Automizy error response [${error.statusCode}]: ${error.response.body.title}`,
);
}
throw error;
throw new NodeApiError(this.getNode(), 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,
IHookFunctions,
IWebhookFunctions,
NodeApiError,
} 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
@ -42,11 +43,7 @@ export async function autopilotApiRequest(this: IExecuteFunctions | IWebhookFunc
try {
return await this.helpers.request!(options);
} catch (error) {
if (error.response) {
const errorMessage = error.response.body.message || error.response.body.description || error.message;
throw new Error(`Autopilot error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;
throw new NodeApiError(this.getNode(), 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">
<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>
<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>

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 249 B

View file

@ -6,6 +6,8 @@ import {
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import { awsApiRequestREST } from './GenericFunctions';
@ -130,13 +132,7 @@ export class AwsLambda implements INodeType {
loadOptions: {
async getFunctions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let data;
try {
data = await awsApiRequestREST.call(this, 'lambda', 'GET', '/2015-03-31/functions/');
} catch (err) {
throw new Error(`AWS Error: ${err}`);
}
const data = await awsApiRequestREST.call(this, 'lambda', 'GET', '/2015-03-31/functions/');
for (const func of data.Functions!) {
returnData.push({
@ -162,22 +158,17 @@ export class AwsLambda implements INodeType {
Qualifier: this.getNodeParameter('qualifier', i) as string,
};
let responseData;
try {
responseData = await awsApiRequestREST.call(
this,
'lambda',
'POST',
`/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',
},
);
} catch (err) {
throw new Error(`AWS Error: ${err}`);
}
const responseData = await awsApiRequestREST.call(
this,
'lambda',
'POST',
`/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',
},
);
if (responseData !== null && responseData.errorMessage !== undefined) {
let errorMessage = responseData.errorMessage;
@ -186,7 +177,7 @@ export class AwsLambda implements INodeType {
errorMessage += `\n\nStack trace:\n${responseData.stackTrace}`;
}
throw new Error(errorMessage);
throw new NodeApiError(this.getNode(), responseData);
} else {
returnData.push({
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,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import { awsApiRequestSOAP } from './GenericFunctions';
@ -107,12 +109,7 @@ export class AwsSns implements INodeType {
// select them easily
async getTopics(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let data;
try {
data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics');
} catch (err) {
throw new Error(`AWS Error: ${err}`);
}
const data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics');
let topics = data.ListTopicsResponse.ListTopicsResult.Topics.member;
@ -149,12 +146,8 @@ export class AwsSns implements INodeType {
'Message=' + this.getNodeParameter('message', i) as string,
];
let responseData;
try {
responseData = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=Publish&' + params.join('&'));
} catch (err) {
throw new Error(`AWS Error: ${err}`);
}
const responseData = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=Publish&' + params.join('&'));
returnData.push({MessageId: responseData.PublishResponse.PublishResult.MessageId} as IDataObject);
}

View file

@ -9,6 +9,8 @@ import {
INodeType,
INodeTypeDescription,
IWebhookResponseData,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -68,12 +70,7 @@ export class AwsSnsTrigger implements INodeType {
// select them easily
async getTopics(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let data;
try {
data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics');
} catch (err) {
throw new Error(`AWS Error: ${err}`);
}
const data = await awsApiRequestSOAP.call(this, 'sns', 'GET', '/?Action=ListTopics');
let topics = data.ListTopicsResponse.ListTopicsResult.Topics.member;
@ -134,7 +131,7 @@ export class AwsSnsTrigger implements INodeType {
const topic = this.getNodeParameter('topic') as string;
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 = [

View file

@ -22,7 +22,7 @@ import {
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
ICredentialDataDecryptedObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow';
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
const credentials = this.getCredentials('aws');
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
@ -61,17 +61,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message;
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}`);
throw new NodeApiError(this.getNode(), error); // no XML parsing needed
}
}
@ -79,7 +69,7 @@ export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions
const response = await awsApiRequest.call(this, service, method, path, body, headers);
try {
return JSON.parse(response);
} catch (e) {
} catch (error) {
return response;
}
}
@ -95,7 +85,7 @@ export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions
resolve(data);
});
});
} catch (e) {
} catch (error) {
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 { sign } from 'aws4';
import { OptionsWithUri } from 'request';
import { parseString } from 'xml2js';
import { parseString as parseXml } from 'xml2js';
import {
IExecuteFunctions,
@ -11,7 +11,7 @@ import {
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
ICredentialDataDecryptedObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow';
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
const credentials = this.getCredentials('aws');
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
@ -52,17 +52,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message;
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}`);
throw new NodeApiError(this.getNode(), error, { parseXml: true });
}
}
@ -70,7 +60,7 @@ export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions
const response = await awsApiRequest.call(this, service, method, path, body, headers);
try {
return JSON.parse(response);
} catch (e) {
} catch (error) {
return response;
}
}
@ -79,14 +69,14 @@ export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions
const response = await awsApiRequest.call(this, service, method, path, body, headers);
try {
return await new Promise((resolve, reject) => {
parseString(response, { explicitArray: false }, (err, data) => {
parseXml(response, { explicitArray: false }, (err, data) => {
if (err) {
return reject(err);
}
resolve(data);
});
});
} catch (e) {
} catch (error) {
return response;
}
}

View file

@ -8,6 +8,8 @@ import {
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -459,11 +461,11 @@ export class AwsRekognition implements INodeType {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string;
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) {
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];
@ -494,7 +496,9 @@ export class AwsRekognition implements INodeType {
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' });
}
}
}

View file

@ -27,6 +27,8 @@ import {
import {
IDataObject,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
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
const credentials = this.getCredentials('aws');
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);
@ -59,17 +61,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message;
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}`);
throw new NodeApiError(this.getNode(), error);
}
}
@ -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);
try {
return JSON.parse(response);
} catch (e) {
} catch (error) {
return response;
}
}
@ -93,8 +85,8 @@ export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions
resolve(data);
});
});
} catch (e) {
return e;
} catch (error) {
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": [
{
"label": "Why this CEO loves n8n",
"label": "Why business process automation with n8n can change your daily life",
"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,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -422,7 +423,7 @@ export class AwsS3 implements INodeType {
const fileName = fileKey.split('/')[fileKey.split('/').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: '' });
@ -588,11 +589,11 @@ export class AwsS3 implements INodeType {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string;
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) {
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];

View file

@ -26,20 +26,20 @@ import {
} from 'n8n-core';
import {
IDataObject,
IDataObject, NodeApiError, NodeOperationError,
} 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
const credentials = this.getCredentials('aws');
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);
// Sign AWS API request with the user credentials
const signOpts = {headers: headers || {}, host: endpoint.host, method, path: `${endpoint.pathname}?${queryToString(query).replace(/\+/g, '%2B')}`, body};
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim()});
@ -57,17 +57,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message;
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}`);
throw new NodeApiError(this.getNode(), error, { parseXml: true });
}
}
@ -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);
try {
return JSON.parse(response);
} catch (e) {
} catch (error) {
return response;
}
}
@ -91,8 +81,8 @@ export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions
resolve(data);
});
});
} catch (e) {
return e;
} catch (error) {
return error;
}
}

View file

@ -19,9 +19,9 @@
],
"generic": [
{
"label": "Why this CEO loves n8n",
"label": "Why business process automation with n8n can change your daily life",
"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,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -1098,7 +1099,7 @@ export class AwsSes implements INodeType {
if (toAddresses.length) {
setParameter(params, 'Destination.ToAddresses.member', toAddresses);
} 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) {
@ -1151,7 +1152,7 @@ export class AwsSes implements INodeType {
if (toAddresses.length) {
setParameter(params, 'Destination.ToAddresses.member', toAddresses);
} 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) {

View file

@ -22,7 +22,7 @@ import {
} from 'n8n-core';
import {
IDataObject,
IDataObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow';
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
const credentials = this.getCredentials('aws');
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);
@ -52,17 +52,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message;
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}`);
throw new NodeApiError(this.getNode(), error, { parseXml: true });
}
}
@ -70,7 +60,7 @@ export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions
const response = await awsApiRequest.call(this, service, method, path, body, headers);
try {
return JSON.parse(response);
} catch (e) {
} catch (error) {
return response;
}
}
@ -86,7 +76,7 @@ export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions
resolve(data);
});
});
} catch (e) {
} catch (error) {
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 {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
@ -11,6 +10,8 @@ import {
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -21,6 +22,10 @@ import {
awsApiRequestSOAP,
} from '../GenericFunctions';
import {
pascalCase,
} from 'change-case';
export class AwsSqs implements INodeType {
description: INodeTypeDescription = {
displayName: 'AWS SQS',
@ -266,12 +271,17 @@ export class AwsSqs implements INodeType {
loadOptions: {
// Get all the available queues to display them to user so that it can be selected easily
async getQueues(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const params = [
'Version=2012-11-05',
`Action=ListQueues`,
];
let data;
try {
// loads first 1000 queues from SQS
data = await awsApiRequestSOAP.call(this, 'sqs', 'GET', `?Action=ListQueues`);
} catch (err) {
throw new Error(`AWS Error: ${err}`);
data = await awsApiRequestSOAP.call(this, 'sqs', 'GET', `?${params.join('&')}`);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
let queues = data.ListQueuesResponse.ListQueuesResult.QueueUrl;
@ -308,7 +318,11 @@ export class AwsSqs implements INodeType {
for (let i = 0; i < items.length; i++) {
const queueUrl = this.getNodeParameter('queue', i) as string;
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 sendInputData = this.getNodeParameter('sendInputData', i) as boolean;
@ -349,11 +363,11 @@ export class AwsSqs implements INodeType {
const item = items[i];
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) {
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;
@ -373,9 +387,9 @@ export class AwsSqs implements INodeType {
let responseData;
try {
responseData = await awsApiRequestSOAP.call(this, 'sqs', 'GET', `${queuePath}/?Action=${operation}&` + params.join('&'));
} catch (err) {
throw new Error(`AWS Error: ${err}`);
responseData = await awsApiRequestSOAP.call(this, 'sqs', 'GET', `${queuePath}?${params.join('&')}`);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
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": [
{
"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,
IHookFunctions,
IWebhookFunctions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -22,7 +24,7 @@ export async function bannerbearApiRequest(this: IExecuteFunctions | IWebhookFun
const credentials = this.getCredentials('bannerbearApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
const options: OptionsWithUri = {
@ -46,12 +48,7 @@ export async function bannerbearApiRequest(this: IExecuteFunctions | IWebhookFun
try {
return await this.helpers.request!(options);
} catch (error) {
if (error.response && error.response.body && error.response.body.message) {
// Try to return the error prettier
//@ts-ignore
throw new Error(`Bannerbear error response [${error.statusCode}]: ${error.response.body.message}`);
}
throw error;
throw new NodeApiError(this.getNode(), error);
}
}

View file

@ -7,6 +7,7 @@ import {
IDataObject,
IHookFunctions,
IWebhookFunctions,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -18,7 +19,7 @@ export async function createDatapoint(this: IExecuteFunctions | IWebhookFunction
const credentials = this.getCredentials('beeminderApi');
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`;
@ -30,7 +31,7 @@ export async function getAllDatapoints(this: IExecuteFunctions | IHookFunctions
const credentials = this.getCredentials('beeminderApi');
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`;
@ -46,7 +47,7 @@ export async function updateDatapoint(this: IExecuteFunctions | IWebhookFunction
const credentials = this.getCredentials('beeminderApi');
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`;
@ -58,7 +59,7 @@ export async function deleteDatapoint(this: IExecuteFunctions | IWebhookFunction
const credentials = this.getCredentials('beeminderApi');
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`;

View file

@ -10,6 +10,7 @@ import {
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -308,7 +309,7 @@ export class Beeminder implements INodeType {
const credentials = this.getCredentials('beeminderApi');
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`;

View file

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

View file

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

View file

@ -5,12 +5,12 @@ import {
IHookFunctions,
ILoadOptionsFunctions,
} 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
const credentials = this.getCredentials('bitbucketApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
let options: OptionsWithUri = {
method,
@ -30,8 +30,8 @@ export async function bitbucketApiRequest(this: IHookFunctions | IExecuteFunctio
try {
return await this.helpers.request!(options);
} catch (err) {
throw new Error('Bitbucket Error: ' + err.message);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}

View file

@ -10,7 +10,7 @@ import {
} from 'n8n-core';
import {
IDataObject,
IDataObject, NodeApiError, NodeOperationError,
} 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
@ -32,7 +32,7 @@ export async function bitlyApiRequest(this: IHookFunctions | IExecuteFunctions |
if (authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('bitlyApi');
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}`};
@ -42,15 +42,7 @@ export async function bitlyApiRequest(this: IHookFunctions | IExecuteFunctions |
return await this.helpers.requestOAuth2!.call(this, 'bitlyOAuth2Api', options, { tokenType: 'Bearer' });
}
} catch(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;
throw new NodeApiError(this.getNode(), 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,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -177,7 +178,7 @@ export class Bitwarden implements INodeType {
const updateFields = this.getNodeParameter('updateFields', i) as CollectionUpdateFields;
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;
@ -308,7 +309,7 @@ export class Bitwarden implements INodeType {
const updateFields = this.getNodeParameter('updateFields', i) as GroupUpdateFields;
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
@ -452,7 +453,7 @@ export class Bitwarden implements INodeType {
const updateFields = this.getNodeParameter('updateFields', i) as MemberUpdateFields;
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;

View file

@ -6,6 +6,7 @@ import {
IDataObject,
ILoadOptionsFunctions,
INodePropertyOptions,
NodeApiError,
} from 'n8n-workflow';
import {
@ -48,17 +49,7 @@ export async function bitwardenApiRequest(
try {
return await this.helpers.request!(options);
} catch (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;
throw new NodeApiError(this.getNode(), error);
}
}
@ -93,7 +84,7 @@ export async function getAccessToken(
const { access_token } = await this.helpers.request!(options);
return access_token;
} catch (error) {
throw error;
throw new NodeApiError(this.getNode(), error);
}
}

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