Merge branch 'master' into setup-i18n

This commit is contained in:
Iván Ovejero 2021-05-26 13:25:49 +02:00
commit 2a199ca8b3
745 changed files with 20507 additions and 6269 deletions

View file

@ -22,8 +22,10 @@ A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- OS: [e.g. Ubuntu Linux 18.04]
- n8n Version [e.g. 0.26.0]
- Node.js Version [e.g. 10.16.0]
- n8n Version [e.g. 0.119.0]
- Node.js Version [e.g. 14.16.0]
- Database system [e.g. SQLite; n8n uses SQLite as default otherwise changed]
- Operation mode [e.g. own; operation modes are `own`, `main` and `queue`. Default is `own`]
**Additional context**
Add any other context about the problem here.

View file

@ -9,7 +9,7 @@ jobs:
strategy:
matrix:
node-version: [12.x, 14.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v1

View file

@ -63,9 +63,7 @@ If you have problems or questions go to our forum, we will then try to help you
## Jobs
If you are interested in working for n8n and so shape the future of the project
check out our job posts:
[https://n8n.join.com](https://n8n.join.com)
check out our [job posts](https://apply.workable.com/n8n/)

View file

@ -49,6 +49,7 @@ Additional information and example workflows on the n8n.io website: [https://n8n
docker run -it --rm \
--name n8n \
-p 5678:5678 \
-v ~/.n8n:/home/node/.n8n \
n8nio/n8n
```
@ -262,9 +263,7 @@ If you have problems or questions go to our forum, we will then try to help you
## Jobs
If you are interested in working for n8n and so shape the future of the project
check out our job posts:
[https://n8n.join.com](https://n8n.join.com)
check out our [job posts](https://apply.workable.com/n8n/)

View file

@ -7,12 +7,15 @@
"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",
"test": "lerna run test",
"tslint": "lerna exec npm run tslint",
"watch": "lerna run --parallel watch"
"watch": "lerna run --parallel watch",
"webhook": "./packages/cli/bin/n8n webhook",
"worker": "./packages/cli/bin/n8n worker"
},
"devDependencies": {
"lerna": "^3.13.1",

View file

@ -2,6 +2,62 @@
This list shows all the versions which include breaking changes and how to upgrade.
## 0.118.0
### What changed?
The minimum Node.js version required for n8n is now v14.
### When is action necessary?
If you're using n8n via npm or PM2 or if you're contributing to n8n.
### How to upgrade:
Update the Node.js version to v14 or above.
----------------------------
### What changed?
In the Postgres, CrateDB, QuestDB and TimescaleDB nodes the `Execute Query` operation returns the result from all queries executed instead of just one of the results.
### When is action necessary?
If you use any of the above mentioned nodes with the `Execute Query` operation and the result is relevant to you, you are encouraged to revisit your logic. The node output may now contain more information than before. This change was made so that the behavior is more consistent across n8n where input with multiple rows should yield results acccording all input data instead of only one. Please note: n8n was already running multiple queries based on input. Only the output was changed.
## 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

@ -2,7 +2,7 @@
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png)
n8n is a free and open [fair-code](http://faircode.io) distributed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools.
n8n is a free and open [fair-code](http://faircode.io) distributed node-based Workflow Automation Tool. You can self-host n8n, easily extend it, and even use it with internal tools.
<a href="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png"><img src="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png" width="550" alt="n8n.io - Screenshot"></a>
@ -11,100 +11,145 @@ n8n is a free and open [fair-code](http://faircode.io) distributed node based Wo
<!-- TOC -->
- [Demo](#demo)
- [Getting Started](#getting-started)
- [Use npx](#use-npx)
- [Run with Docker](#run-with-docker)
- [Install with npm](#install-with-npm)
- [Sign-up on n8n.cloud](#sign-up-on-n8n.cloud)
- [Available integrations](#available-integrations)
- [Documentation](#documentation)
- [Create Custom Nodes](#create-custom-nodes)
- [Hosted n8n](#hosted-n8n)
- [Contributing](#contributing)
- [What does n8n mean and how do you pronounce it](#what-does-n8n-mean-and-how-do-you-pronounce-it)
- [Support](#support)
- [Jobs](#jobs)
- [Upgrading](#upgrading)
- [License](#license)
- [Development](#development)
<!-- /TOC -->
## Demo
[:tv: A short demo (< 3 min)](https://www.youtube.com/watch?v=3w7xIMKLVAg)
which shows how to create a simple workflow which automatically sends a new
Slack notification every time a Github repository received or lost a star.
📺 Here's a [short demo (<3 min)](https://www.youtube.com/watch?v=3w7xIMKLVAg) that shows how to create a simple workflow to automatically sends a notification on Slack every time a GitHub repository gets starred or un-starred.
## Getting Started
There are a couple of ways to get started with n8n.
### Use npx
To spin up n8n using npx, you can run:
```bash
npx n8n
```
It will download everything that is needed to start n8n.
You can then access n8n by opening:
[http://localhost:5678](http://localhost:5678)
**Note:** The minimum required version for Node.js is v14.15. Make sure to update Node.js to v14.15 or above.
### Run with Docker
To play around with n8n, you can also start it using Docker:
```bash
docker run -it --rm \
--name n8n \
-p 5678:5678 \
n8nio/n8n
```
Be aware that all the data will be lost once the Docker container gets removed. To persist the data mount the `~/.n8n` folder:
```bash
docker run -it --rm \
--name n8n \
-p 5678:5678 \
-v ~/.n8n:/home/node/.n8n \
n8nio/n8n
```
n8n also offers a Docker image for Raspberry Pi: `n8nio/n8n:latest-rpi`.
Refer to the [documentation](https://github.com/n8n-io/n8n/blob/master/docker/images/n8n/README.md) for more information on the Docker setup.
### Install with npm
To install n8n globally using npm:
```bash
npm install n8n -g
```
After the installation, start n8n running the following command:
```bash
n8n
# or
n8n start
```
### Sign-up on n8n.cloud
Sign-up for an [n8n.cloud](https://www.n8n.cloud/) account.
While n8n.cloud and n8n are the same in terms of features, n8n.cloud provides certain conveniences such as:
- Not having to set up and maintain your n8n instance
- Managed OAuth for authentication
- Easily upgrading to the newer n8n versions
## Available integrations
n8n has 200+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
n8n has 280+ different nodes that allow you to connect various services and build your automation workflows. You can find the list of all the integrations at [https://n8n.io/integrations](https://n8n.io/integrations)
## Documentation
The official n8n documentation can be found under: [https://docs.n8n.io](https://docs.n8n.io)
To learn more about n8n, refer to the official documentation here: [https://docs.n8n.io](https://docs.n8n.io)
Additional information and example workflows on the n8n.io website: [https://n8n.io](https://n8n.io)
You can find additional information and example workflows on the [n8n.io](https://n8n.io) website.
## Create Custom Nodes
It is very easy to create own nodes for n8n. More information about that can
be found in the documentation of "n8n-node-dev" which is a small CLI which
helps with n8n-node-development.
You can create custom nodes for n8n. Follow the instructions mentioned in the documentation to create your node: [Creating nodes](https://docs.n8n.io/nodes/creating-nodes/create-node.html)
[To n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev)
## Contributing
Additional information can be found on the [ documentation page](https://docs.n8n.io/#/create-node).
🐛 Did you find a bug?
✨ Do you want to contribute a feature?
## Hosted n8n
The [CONTRIBUTING guide](https://github.com/n8n-io/n8n/blob/master/CONTRIBUTING.md) will help you set up your development environment.
If you are interested in a hosted version of n8n on our infrastructure please contact us via:
[hosting@n8n.io](mailto:hosting@n8n.io)
You can find more information on how you can contribute to the project on our documentation: [How can I contribute?](https://docs.n8n.io/reference/contributing.html)
## What does n8n mean, and how do you pronounce it?
**Short answer:** n8n is an abbreviation for "nodemation", and it is pronounced as n-eight-n.
## What does n8n mean and how do you pronounce it?
**Short answer:** It means "nodemation" and it is pronounced as n-eight-n.
**Long answer:** I get that question quite often (more often than I expected)
so I decided it is probably best to answer it here. While looking for a
good name for the project with a free domain I realized very quickly that all the
good ones I could think of were already taken. So, in the end, I chose
nodemation. "node-" in the sense that it uses a Node-View and that it uses
Node.js and "-mation" for "automation" which is what the project is supposed to help with.
However, I did not like how long the name was and I could not imagine writing
something that long every time in the CLI. That is when I then ended up on
"n8n". Sure does not work perfectly but does neither for Kubernetes (k8s) and
did not hear anybody complain there. So I guess it should be ok.
**Long answer:** In n8n, you build your automation ("-mation") workflows by connecting different nodes in the Editor UI. The project is also built using Node.js. As a consequence, the project was named nodemation.
However, the name was long, and it wouldn't be a good idea to use such a long name in the CLI. Hence, nodemation got abbreviated as "n8n" (there are eight characters between the first and the last n!).
## Support
If you have problems or questions go to our forum, we will then try to help you asap:
[https://community.n8n.io](https://community.n8n.io)
If you run into issues or have any questions reach out to us via our community forum: [https://community.n8n.io](https://community.n8n.io).
## Jobs
If you are interested in working for n8n and so shape the future of the project
check out our job posts:
[https://n8n.join.com](https://n8n.join.com)
If you are interested in working at n8n and building the project, check out the [job openings](https://apply.workable.com/n8n/).
## Upgrading
Before you upgrade to the latest version make sure to check here if there are any breaking changes which concern you:
[Breaking Changes](https://github.com/n8n-io/n8n/blob/master/packages/cli/BREAKING-CHANGES.md)
Before you upgrade to the latest version, make sure to check the changelogs: [Changelog](https://docs.n8n.io/reference/changelog.html)
You can also find breaking changes here: [Breaking Changes](./BREAKING-CHANGES.md)
## License
n8n is [fair-code](http://faircode.io) distributed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) license
n8n is [fair-code](http://faircode.io) distributed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) license.
Additional information about license can be found in the [FAQ](https://docs.n8n.io/#/faq?id=license)
## Development
Have you found a bug :bug: ? Or maybe you have a nice feature :sparkles: to contribute ? The [CONTRIBUTING guide](https://github.com/n8n-io/n8n/blob/master/CONTRIBUTING.md) will help you get your development environment ready in minutes.
Additional information on the license can be found in the [FAQ](https://docs.n8n.io/reference/faq.html#license)

View file

@ -24,8 +24,8 @@ if (process.argv.length === 2) {
var nodeVersion = process.versions.node.split('.');
if (parseInt(nodeVersion[0], 10) < 12 || parseInt(nodeVersion[0], 10) === 12 && parseInt(nodeVersion[1], 10) < 9) {
console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 12.9 or later!\n`);
if (parseInt(nodeVersion[0], 10) < 14) {
console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 14 or later!\n`);
process.exit(0);
}

View file

@ -13,7 +13,6 @@ import {
CredentialTypes,
Db,
ExternalHooks,
GenericHelpers,
IWorkflowBase,
IWorkflowExecutionDataProcess,
LoadNodesAndCredentials,
@ -23,6 +22,13 @@ import {
WorkflowRunner,
} from '../src';
import {
getLogger,
} from '../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
export class Execute extends Command {
static description = '\nExecutes a given workflow';
@ -44,6 +50,9 @@ export class Execute extends Command {
async run() {
const logger = getLogger();
LoggerProxy.init(logger);
const { flags } = this.parse(Execute);
// Start directly with the init of the database to improve startup time
@ -54,12 +63,12 @@ export class Execute extends Command {
const loadNodesAndCredentialsPromise = loadNodesAndCredentials.init();
if (!flags.id && !flags.file) {
GenericHelpers.logOutput(`Either option "--id" or "--file" have to be set!`);
console.info(`Either option "--id" or "--file" have to be set!`);
return;
}
if (flags.id && flags.file) {
GenericHelpers.logOutput(`Either "id" or "file" can be set never both!`);
console.info(`Either "id" or "file" can be set never both!`);
return;
}
@ -71,7 +80,7 @@ export class Execute extends Command {
workflowData = JSON.parse(await fs.readFile(flags.file, 'utf8'));
} catch (error) {
if (error.code === 'ENOENT') {
GenericHelpers.logOutput(`The file "${flags.file}" could not be found.`);
console.info(`The file "${flags.file}" could not be found.`);
return;
}
@ -81,7 +90,7 @@ export class Execute extends Command {
// Do a basic check if the data in the file looks right
// TODO: Later check with the help of TypeScript data if it is valid or not
if (workflowData === undefined || workflowData.nodes === undefined || workflowData.connections === undefined) {
GenericHelpers.logOutput(`The file "${flags.file}" does not contain valid workflow data.`);
console.info(`The file "${flags.file}" does not contain valid workflow data.`);
return;
}
workflowId = workflowData.id!.toString();
@ -95,8 +104,8 @@ export class Execute extends Command {
workflowId = flags.id;
workflowData = await Db.collections!.Workflow!.findOne(workflowId);
if (workflowData === undefined) {
GenericHelpers.logOutput(`The workflow with the id "${workflowId}" does not exist.`);
return;
console.info(`The workflow with the id "${workflowId}" does not exist.`);
process.exit(1);
}
}
@ -138,7 +147,7 @@ export class Execute extends Command {
if (startNode === undefined) {
// If the workflow does not contain a start-node we can not know what
// should be executed and with which data to start.
GenericHelpers.logOutput(`The workflow does not contain a "Start" node. So it can not be executed.`);
console.info(`The workflow does not contain a "Start" node. So it can not be executed.`);
return Promise.resolve();
}
@ -163,26 +172,28 @@ export class Execute extends Command {
}
if (data.data.resultData.error) {
this.log('Execution was NOT successfull:');
this.log('====================================');
this.log(JSON.stringify(data, null, 2));
console.info('Execution was NOT successful. See log message for details.');
logger.info('Execution error:');
logger.info('====================================');
logger.info(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,
};
}
this.log('Execution was successfull:');
this.log('====================================');
this.log(JSON.stringify(data, null, 2));
console.info('Execution was successful:');
console.info('====================================');
console.info(JSON.stringify(data, null, 2));
} catch (e) {
console.error('\nGOT ERROR');
console.log('====================================');
console.error(e.message);
console.error(e.stack);
console.error('Error executing workflow. See log messages for details.');
logger.error('\nExecution error:');
logger.info('====================================');
logger.error(e.message);
logger.error(e.stack);
this.exit(1);
return;
}
this.exit();

View file

@ -14,10 +14,17 @@ import {
import {
Db,
GenericHelpers,
ICredentialsDecryptedDb,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import * as fs from 'fs';
import * as path from 'path';
@ -59,8 +66,11 @@ export class ExportCredentialsCommand extends Command {
};
async run() {
const { flags } = this.parse(ExportCredentialsCommand);
const logger = getLogger();
LoggerProxy.init(logger);
const { flags } = this.parse(ExportCredentialsCommand);
if (flags.backup) {
flags.all = true;
flags.pretty = true;
@ -68,41 +78,42 @@ export class ExportCredentialsCommand extends Command {
}
if (!flags.all && !flags.id) {
GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`);
console.info(`Either option "--all" or "--id" have to be set!`);
return;
}
if (flags.all && flags.id) {
GenericHelpers.logOutput(`You should either use "--all" or "--id" but never both!`);
console.info(`You should either use "--all" or "--id" but never both!`);
return;
}
if (flags.separate) {
try {
if (!flags.output) {
GenericHelpers.logOutput(`You must inform an output directory via --output when using --separate`);
console.info(`You must inform an output directory via --output when using --separate`);
return;
}
if (fs.existsSync(flags.output)) {
if (!fs.lstatSync(flags.output).isDirectory()) {
GenericHelpers.logOutput(`The paramenter --output must be a directory`);
console.info(`The paramenter --output must be a directory`);
return;
}
} else {
fs.mkdirSync(flags.output, { recursive: true });
}
} catch (e) {
console.error('\nFILESYSTEM ERROR');
console.log('====================================');
console.error(e.message);
console.error(e.stack);
console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.');
logger.error('\nFILESYSTEM ERROR');
logger.info('====================================');
logger.error(e.message);
logger.error(e.stack);
this.exit(1);
}
} else if (flags.output) {
if (fs.existsSync(flags.output)) {
if (fs.lstatSync(flags.output).isDirectory()) {
GenericHelpers.logOutput(`The paramenter --output must be a writeble file`);
console.info(`The paramenter --output must be a writeble file`);
return;
}
}
@ -143,18 +154,21 @@ export class ExportCredentialsCommand extends Command {
const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + credentials[i].id + '.json';
fs.writeFileSync(filename, fileContents);
}
console.log('Successfully exported', i, 'credentials.');
console.info(`Successfully exported ${i} credentials.`);
} else {
const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined);
if (flags.output) {
fs.writeFileSync(flags.output!, fileContents);
console.log('Successfully exported', credentials.length, 'credentials.');
console.info(`Successfully exported ${credentials.length} credentials.`);
} else {
console.log(fileContents);
console.info(fileContents);
}
}
// Force exit as process won't exit using MySQL or Postgres.
process.exit(0);
} catch (error) {
this.error(error.message);
console.error('Error exporting credentials. See log messages for details.');
logger.error(error.message);
this.exit(1);
}
}

View file

@ -9,9 +9,16 @@ import {
import {
Db,
GenericHelpers,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import * as fs from 'fs';
import * as path from 'path';
@ -49,6 +56,9 @@ export class ExportWorkflowsCommand extends Command {
};
async run() {
const logger = getLogger();
LoggerProxy.init(logger);
const { flags } = this.parse(ExportWorkflowsCommand);
if (flags.backup) {
@ -58,41 +68,42 @@ export class ExportWorkflowsCommand extends Command {
}
if (!flags.all && !flags.id) {
GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`);
console.info(`Either option "--all" or "--id" have to be set!`);
return;
}
if (flags.all && flags.id) {
GenericHelpers.logOutput(`You should either use "--all" or "--id" but never both!`);
console.info(`You should either use "--all" or "--id" but never both!`);
return;
}
if (flags.separate) {
try {
if (!flags.output) {
GenericHelpers.logOutput(`You must inform an output directory via --output when using --separate`);
console.info(`You must inform an output directory via --output when using --separate`);
return;
}
if (fs.existsSync(flags.output)) {
if (!fs.lstatSync(flags.output).isDirectory()) {
GenericHelpers.logOutput(`The paramenter --output must be a directory`);
console.info(`The paramenter --output must be a directory`);
return;
}
} else {
fs.mkdirSync(flags.output, { recursive: true });
}
} catch (e) {
console.error('\nFILESYSTEM ERROR');
console.log('====================================');
console.error(e.message);
console.error(e.stack);
console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.');
logger.error('\nFILESYSTEM ERROR');
logger.info('====================================');
logger.error(e.message);
logger.error(e.stack);
this.exit(1);
}
} else if (flags.output) {
if (fs.existsSync(flags.output)) {
if (fs.lstatSync(flags.output).isDirectory()) {
GenericHelpers.logOutput(`The paramenter --output must be a writeble file`);
console.info(`The paramenter --output must be a writeble file`);
return;
}
}
@ -119,18 +130,21 @@ export class ExportWorkflowsCommand extends Command {
const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + workflows[i].id + '.json';
fs.writeFileSync(filename, fileContents);
}
console.log('Successfully exported', i, 'workflows.');
console.info(`Successfully exported ${i} workflows.`);
} else {
const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined);
if (flags.output) {
fs.writeFileSync(flags.output!, fileContents);
console.log('Successfully exported', workflows.length, workflows.length === 1 ? 'workflow.' : 'workflows.');
console.info(`Successfully exported ${workflows.length} ${workflows.length === 1 ? 'workflow.' : 'workflows.'}`);
} else {
console.log(fileContents);
console.info(fileContents);
}
}
// Force exit as process won't exit using MySQL or Postgres.
process.exit(0);
} catch (error) {
this.error(error.message);
console.error('Error exporting workflows. See log messages for details.');
logger.error(error.message);
this.exit(1);
}
}

View file

@ -10,9 +10,16 @@ import {
import {
Db,
GenericHelpers,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import * as fs from 'fs';
import * as glob from 'glob-promise';
import * as path from 'path';
@ -37,17 +44,20 @@ export class ImportCredentialsCommand extends Command {
};
async run() {
const logger = getLogger();
LoggerProxy.init(logger);
const { flags } = this.parse(ImportCredentialsCommand);
if (!flags.input) {
GenericHelpers.logOutput(`An input file or directory with --input must be provided`);
console.info(`An input file or directory with --input must be provided`);
return;
}
if (flags.separate) {
if (fs.existsSync(flags.input)) {
if (!fs.lstatSync(flags.input).isDirectory()) {
GenericHelpers.logOutput(`The paramenter --input must be a directory`);
console.info(`The paramenter --input must be a directory`);
return;
}
}
@ -89,9 +99,11 @@ export class ImportCredentialsCommand extends Command {
await Db.collections.Credentials!.save(fileContents[i]);
}
}
console.log('Successfully imported', i, 'credentials.');
console.info(`Successfully imported ${i} ${i === 1 ? 'credential.' : 'credentials.'}`);
process.exit(0);
} catch (error) {
this.error(error.message);
console.error('An error occurred while exporting credentials. See log messages for details.');
logger.error(error.message);
this.exit(1);
}
}

View file

@ -5,9 +5,16 @@ import {
import {
Db,
GenericHelpers,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import * as fs from 'fs';
import * as glob from 'glob-promise';
import * as path from 'path';
@ -32,17 +39,20 @@ export class ImportWorkflowsCommand extends Command {
};
async run() {
const logger = getLogger();
LoggerProxy.init(logger);
const { flags } = this.parse(ImportWorkflowsCommand);
if (!flags.input) {
GenericHelpers.logOutput(`An input file or directory with --input must be provided`);
console.info(`An input file or directory with --input must be provided`);
return;
}
if (flags.separate) {
if (fs.existsSync(flags.input)) {
if (!fs.lstatSync(flags.input).isDirectory()) {
GenericHelpers.logOutput(`The paramenter --input must be a directory`);
console.info(`The paramenter --input must be a directory`);
return;
}
}
@ -69,9 +79,11 @@ export class ImportWorkflowsCommand extends Command {
}
}
console.log('Successfully imported', i, i === 1 ? 'workflow.' : 'workflows.');
console.info(`Successfully imported ${i} ${i === 1 ? 'workflow.' : 'workflows.'}`);
process.exit(0);
} catch (error) {
this.error(error.message);
console.error('An error occurred while exporting workflows. See log messages for details.');
logger.error(error.message);
this.exit(1);
}
}

View file

@ -25,11 +25,17 @@ import {
} from '../src';
import { IDataObject } from 'n8n-workflow';
import {
getLogger,
} from '../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
let processExistCode = 0;
export class Start extends Command {
static description = 'Starts n8n. Makes Web-UI available and starts active workflows';
@ -71,7 +77,7 @@ export class Start extends Command {
* get removed.
*/
static async stopProcess() {
console.log(`\nStopping n8n...`);
getLogger().info('\nStopping n8n...');
try {
const externalHooks = ExternalHooks();
@ -132,13 +138,18 @@ export class Start extends Command {
// Wrap that the process does not close but we can still use async
await (async () => {
try {
const logger = getLogger();
LoggerProxy.init(logger);
logger.info('Initializing n8n process');
// Start directly with the init of the database to improve startup time
const startDbInitPromise = Db.init().catch((error: Error) => {
console.error(`There was an error initializing DB: ${error.message}`);
logger.error(`There was an error initializing DB: "${error.message}"`);
processExistCode = 1;
// @ts-ignore
process.emit('SIGINT');
process.exit(1);
});
// Make sure the settings exist
@ -184,7 +195,7 @@ export class Start extends Command {
cumulativeTimeout += now - lastTimer;
lastTimer = now;
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.');
logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
process.exit(1);
}
}
@ -213,9 +224,9 @@ export class Start extends Command {
redis.on('error', (error) => {
if (error.toString().includes('ECONNREFUSED') === true) {
console.warn('Redis unavailable - trying to reconnect...');
logger.warn('Redis unavailable - trying to reconnect...');
} else {
console.warn('Error with Redis: ', error);
logger.warn('Error with Redis: ', error);
}
});
}

View file

@ -11,6 +11,13 @@ import {
GenericHelpers,
} from '../../src';
import {
getLogger,
} from '../../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
export class UpdateWorkflowCommand extends Command {
static description = '\Update workflows';
@ -34,25 +41,28 @@ export class UpdateWorkflowCommand extends Command {
};
async run() {
const logger = getLogger();
LoggerProxy.init(logger);
const { flags } = this.parse(UpdateWorkflowCommand);
if (!flags.all && !flags.id) {
GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`);
console.info(`Either option "--all" or "--id" have to be set!`);
return;
}
if (flags.all && flags.id) {
GenericHelpers.logOutput(`Either something else on top should be "--all" or "--id" can be set never both!`);
console.info(`Either something else on top should be "--all" or "--id" can be set never both!`);
return;
}
const updateQuery: IDataObject = {};
if (flags.active === undefined) {
GenericHelpers.logOutput(`No update flag like "--active=true" has been set!`);
console.info(`No update flag like "--active=true" has been set!`);
return;
} else {
if (!['false', 'true'].includes(flags.active)) {
GenericHelpers.logOutput(`Valid values for flag "--active" are only "false" or "true"!`);
console.info(`Valid values for flag "--active" are only "false" or "true"!`);
return;
}
updateQuery.active = flags.active === 'true';
@ -63,20 +73,21 @@ export class UpdateWorkflowCommand extends Command {
const findQuery: IDataObject = {};
if (flags.id) {
console.log(`Deactivating workflow with ID: ${flags.id}`);
console.info(`Deactivating workflow with ID: ${flags.id}`);
findQuery.id = flags.id;
} else {
console.log('Deactivating all workflows');
console.info('Deactivating all workflows');
findQuery.active = true;
}
await Db.collections.Workflow!.update(findQuery, updateQuery);
console.log('Done');
console.info('Done');
} catch (e) {
console.error('\nGOT ERROR');
console.log('====================================');
console.error(e.message);
console.error(e.stack);
console.error('Error updating database. See log messages for details.');
logger.error('\nGOT ERROR');
logger.info('====================================');
logger.error(e.message);
logger.error(e.stack);
this.exit(1);
}

View file

@ -20,6 +20,13 @@ import {
} from '../src';
import { IDataObject } from 'n8n-workflow';
import {
getLogger,
} from '../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
let processExistCode = 0;
@ -42,7 +49,7 @@ export class Webhook extends Command {
* get removed.
*/
static async stopProcess() {
console.log(`\nStopping n8n...`);
LoggerProxy.info(`\nStopping n8n...`);
try {
const externalHooks = ExternalHooks();
@ -54,17 +61,6 @@ export class Webhook extends Command {
process.exit(processExistCode);
}, 30000);
const removePromises = [];
if (activeWorkflowRunner !== undefined) {
removePromises.push(activeWorkflowRunner.removeAll());
}
// Remove all test webhooks
const testWebhooks = TestWebhooks.getInstance();
removePromises.push(testWebhooks.removeAll());
await Promise.all(removePromises);
// Wait for active workflow executions to finish
const activeExecutionsInstance = ActiveExecutions.getInstance();
let executingWorkflows = activeExecutionsInstance.getActiveExecutions();
@ -72,7 +68,7 @@ export class Webhook extends Command {
let count = 0;
while (executingWorkflows.length !== 0) {
if (count++ % 4 === 0) {
console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`);
LoggerProxy.info(`Waiting for ${executingWorkflows.length} active executions to finish...`);
}
await new Promise((resolve) => {
setTimeout(resolve, 500);
@ -81,7 +77,7 @@ export class Webhook extends Command {
}
} catch (error) {
console.error('There was an error shutting down n8n.', error);
LoggerProxy.error('There was an error shutting down n8n.', error);
}
process.exit(processExistCode);
@ -89,6 +85,9 @@ export class Webhook extends Command {
async run() {
const logger = getLogger();
LoggerProxy.init(logger);
// Make sure that n8n shuts down gracefully if possible
process.on('SIGTERM', Webhook.stopProcess);
process.on('SIGINT', Webhook.stopProcess);
@ -116,11 +115,12 @@ export class Webhook extends Command {
try {
// Start directly with the init of the database to improve startup time
const startDbInitPromise = Db.init().catch(error => {
console.error(`There was an error initializing DB: ${error.message}`);
logger.error(`There was an error initializing DB: "${error.message}"`);
processExistCode = 1;
// @ts-ignore
process.emit('SIGINT');
process.exit(1);
});
// Make sure the settings exist
@ -166,7 +166,7 @@ export class Webhook extends Command {
cumulativeTimeout += now - lastTimer;
lastTimer = now;
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.');
logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
process.exit(1);
}
}
@ -195,9 +195,9 @@ export class Webhook extends Command {
redis.on('error', (error) => {
if (error.toString().includes('ECONNREFUSED') === true) {
console.warn('Redis unavailable - trying to reconnect...');
logger.warn('Redis unavailable - trying to reconnect...');
} else {
console.warn('Error with Redis: ', error);
logger.warn('Error with Redis: ', error);
}
});
}
@ -209,14 +209,16 @@ export class Webhook extends Command {
await activeWorkflowRunner.initWebhooks();
const editorUrl = GenericHelpers.getBaseUrl();
this.log('Webhook listener waiting for requests.');
console.info('Webhook listener waiting for requests.');
} catch (error) {
this.error(`There was an error: ${error.message}`);
console.error('Exiting due to error. See log message for details.');
logger.error(`Webhook process cannot continue. "${error.message}"`);
processExistCode = 1;
// @ts-ignore
process.emit('SIGINT');
process.exit(1);
}
})();
}

View file

@ -37,6 +37,14 @@ import {
WorkflowExecuteAdditionalData,
} from '../src';
import {
getLogger,
} from '../src/Logger';
import {
LoggerProxy,
} from 'n8n-workflow';
import * as config from '../config';
import * as Bull from 'bull';
import * as Queue from '../src/Queue';
@ -71,7 +79,7 @@ export class Worker extends Command {
* get removed.
*/
static async stopProcess() {
console.log(`\nStopping n8n...`);
LoggerProxy.info(`Stopping n8n...`);
// Stop accepting new jobs
Worker.jobQueue.pause(true);
@ -95,7 +103,7 @@ export class Worker extends Command {
while (Object.keys(Worker.runningJobs).length !== 0) {
if (count++ % 4 === 0) {
const waitLeft = Math.ceil((stopTime - new Date().getTime()) / 1000);
console.log(`Waiting for ${Object.keys(Worker.runningJobs).length} active executions to finish... (wait ${waitLeft} more seconds)`);
LoggerProxy.info(`Waiting for ${Object.keys(Worker.runningJobs).length} active executions to finish... (wait ${waitLeft} more seconds)`);
}
await new Promise((resolve) => {
setTimeout(resolve, 500);
@ -103,7 +111,7 @@ export class Worker extends Command {
}
} catch (error) {
console.error('There was an error shutting down n8n.', error);
LoggerProxy.error('There was an error shutting down n8n.', error);
}
process.exit(Worker.processExistCode);
@ -113,7 +121,7 @@ export class Worker extends Command {
const jobData = job.data as IBullJobData;
const executionDb = await Db.collections.Execution!.findOne(jobData.executionId) as IExecutionFlattedDb;
const currentExecutionDb = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse;
console.log(`Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${jobData.executionId})`);
LoggerProxy.info(`Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${jobData.executionId})`);
let staticData = currentExecutionDb.workflowData!.staticData;
if (jobData.loadStaticData === true) {
@ -127,11 +135,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;
@ -159,7 +178,10 @@ export class Worker extends Command {
}
async run() {
console.log('Starting n8n worker...');
const logger = getLogger();
LoggerProxy.init(logger);
console.info('Starting n8n worker...');
// Make sure that n8n shuts down gracefully if possible
process.on('SIGTERM', Worker.stopProcess);
@ -172,11 +194,12 @@ export class Worker extends Command {
// Start directly with the init of the database to improve startup time
const startDbInitPromise = Db.init().catch(error => {
console.error(`There was an error initializing DB: ${error.message}`);
logger.error(`There was an error initializing DB: "${error.message}"`);
Worker.processExistCode = 1;
// @ts-ignore
process.emit('SIGINT');
process.exit(1);
});
// Make sure the settings exist
@ -210,10 +233,10 @@ export class Worker extends Command {
const versions = await GenericHelpers.getVersions();
console.log('\nn8n worker is now ready');
console.log(` * Version: ${versions.cli}`);
console.log(` * Concurrency: ${flags.concurrency}`);
console.log('');
console.info('\nn8n worker is now ready');
console.info(` * Version: ${versions.cli}`);
console.info(` * Concurrency: ${flags.concurrency}`);
console.info('');
Worker.jobQueue.on('global:progress', (jobId, progress) => {
// Progress of a job got updated which does get used
@ -241,27 +264,28 @@ export class Worker extends Command {
cumulativeTimeout += now - lastTimer;
lastTimer = now;
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.');
logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
process.exit(1);
}
}
console.warn('Redis unavailable - trying to reconnect...');
logger.warn('Redis unavailable - trying to reconnect...');
} else if (error.toString().includes('Error initializing Lua scripts') === true) {
// This is a non-recoverable error
// Happens when worker starts and Redis is unavailable
// Even if Redis comes back online, worker will be zombie
console.error('Error initializing worker.');
logger.error('Error initializing worker.');
process.exit(2);
} else {
console.error('Error from queue: ', error);
logger.error('Error from queue: ', error);
}
});
} catch (error) {
this.error(`There was an error: ${error.message}`);
logger.error(`Worker process cannot continue. "${error.message}"`);
Worker.processExistCode = 1;
// @ts-ignore
process.emit('SIGINT');
process.exit(1);
}
})();

View file

@ -1,5 +1,7 @@
import * as convict from 'convict';
import * as dotenv from 'dotenv';
import * as path from 'path';
import * as core from 'n8n-core';
dotenv.config();
@ -446,6 +448,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',
@ -566,6 +574,41 @@ const config = convict({
},
},
logs: {
level: {
doc: 'Log output level. Options are error, warn, info, verbose and debug.',
format: String,
default: 'info',
env: 'N8N_LOG_LEVEL',
},
output: {
doc: 'Where to output logs. Options are: console, file. Multiple can be separated by comma (",")',
format: String,
default: 'console',
env: 'N8N_LOG_OUTPUT',
},
file: {
fileCountMax: {
doc: 'Maximum number of files to keep.',
format: Number,
default: 100,
env: 'N8N_LOG_FILE_COUNT_MAX',
},
fileSizeMax: {
doc: 'Maximum size for each log file in MB.',
format: Number,
default: 16,
env: 'N8N_LOG_FILE_SIZE_MAX',
},
location: {
doc: 'Log file location; only used if log output is set to file.',
format: String,
default: path.join(core.UserSettings.getUserN8nFolderPath(), 'logs/n8n.log'),
env: 'N8N_LOG_FILE_LOCATION',
},
},
},
});
// Overwrite default configuration with settings which got defined in

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.114.0",
"version": "0.121.0",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -44,7 +44,7 @@
"workflow"
],
"engines": {
"node": ">=12.0.0"
"node": ">=14.0.0"
},
"files": [
"bin",
@ -55,7 +55,7 @@
"devDependencies": {
"@oclif/dev-cli": "^1.22.2",
"@types/basic-auth": "^1.1.2",
"@types/bcryptjs": "^2.4.1",
"@types/bcryptjs": "^2.4.2",
"@types/bull": "^3.3.10",
"@types/compression": "1.0.1",
"@types/connect-history-api-fallback": "^1.3.1",
@ -65,7 +65,7 @@
"@types/jest": "^26.0.13",
"@types/localtunnel": "^1.9.0",
"@types/lodash.get": "^4.4.6",
"@types/node": "14.0.27",
"@types/node": "^14.14.40",
"@types/open": "^6.1.0",
"@types/parseurl": "^1.3.1",
"@types/request-promise-native": "~1.0.15",
@ -88,6 +88,7 @@
"body-parser": "^1.18.3",
"body-parser-xml": "^1.1.0",
"bull": "^3.19.0",
"callsites": "^3.1.0",
"client-oauth2": "^4.2.5",
"compression": "^1.7.4",
"connect-history-api-fallback": "^1.6.0",
@ -103,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.72.0",
"n8n-editor-ui": "~0.91.0",
"n8n-nodes-base": "~0.118.0",
"n8n-workflow": "~0.59.0",
"oauth-1.0a": "^2.2.6",
"open": "^7.0.0",
"pg": "^8.3.0",
@ -115,7 +116,7 @@
"request-promise-native": "^1.0.7",
"sqlite3": "^5.0.1",
"sse-channel": "^3.1.1",
"tslib": "1.11.2",
"tslib": "1.14.1",
"typeorm": "^0.2.30"
},
"jest": {

View file

@ -20,7 +20,6 @@ import {
} from 'n8n-core';
import {
IDataObject,
IExecuteData,
IGetExecutePollFunctions,
IGetExecuteTriggerFunctions,
@ -36,6 +35,9 @@ import {
} from 'n8n-workflow';
import * as express from 'express';
import {
LoggerProxy as Logger,
} from 'n8n-workflow';
export class ActiveWorkflowRunner {
private activeWorkflows: ActiveWorkflows | null = null;
@ -44,7 +46,6 @@ export class ActiveWorkflowRunner {
[key: string]: IActivationError;
} = {};
async init() {
// Get the active workflows from database
@ -60,20 +61,24 @@ export class ActiveWorkflowRunner {
this.activeWorkflows = new ActiveWorkflows();
if (workflowsData.length !== 0) {
console.log('\n ================================');
console.log(' Start Active Workflows:');
console.log(' ================================');
console.info(' ================================');
console.info(' Start Active Workflows:');
console.info(' ================================');
for (const workflowData of workflowsData) {
console.log(` - ${workflowData.name}`);
Logger.debug(`Initializing active workflow "${workflowData.name}" (startup)`, { workflowName: workflowData.name, workflowId: workflowData.id });
try {
await this.add(workflowData.id.toString(), 'init', workflowData);
Logger.verbose(`Successfully started workflow "${workflowData.name}"`, { workflowName: workflowData.name, workflowId: workflowData.id });
console.log(` => Started`);
} catch (error) {
console.log(` => ERROR: Workflow could not be activated:`);
console.log(` ${error.message}`);
Logger.error(`Unable to initialize workflow "${workflowData.name}" (startup)`, { workflowName: workflowData.name, workflowId: workflowData.id });
}
}
Logger.verbose('Finished initializing active workflows (startup)');
}
}
@ -89,6 +94,7 @@ export class ActiveWorkflowRunner {
*/
async removeAll(): Promise<void> {
const activeWorkflowId: string[] = [];
Logger.verbose('Call to remove all active workflows received (removeAll)');
if (this.activeWorkflows !== null) {
// TODO: This should be renamed!
@ -118,6 +124,7 @@ export class ActiveWorkflowRunner {
* @memberof ActiveWorkflowRunner
*/
async executeWebhook(httpMethod: WebhookHttpMethod, path: string, req: express.Request, res: express.Response): Promise<IResponseCallbackData> {
Logger.debug(`Received webhoook "${httpMethod}" for path "${path}"`);
if (this.activeWorkflows === null) {
throw new ResponseHelper.ResponseError('The "activeWorkflows" instance did not get initialized yet.', 404, 404);
}
@ -181,7 +188,7 @@ export class ActiveWorkflowRunner {
}
const nodeTypes = NodeTypes();
const workflow = new Workflow({ id: webhook.workflowId.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings});
const workflow = new Workflow({ id: webhook.workflowId.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
const credentials = await WorkflowCredentials([workflow.getNode(webhook.node as string) as INode]);
@ -218,8 +225,8 @@ export class ActiveWorkflowRunner {
* @returns {Promise<string[]>}
* @memberof ActiveWorkflowRunner
*/
async getWebhookMethods(path: string) : Promise<string[]> {
const webhooks = await Db.collections.Webhook?.find({ webhookPath: path}) as IWebhookDb[];
async getWebhookMethods(path: string): Promise<string[]> {
const webhooks = await Db.collections.Webhook?.find({ webhookPath: path }) as IWebhookDb[];
// Gather all request methods in string array
const webhookMethods: string[] = webhooks.map(webhook => webhook.method);
@ -283,23 +290,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 +312,6 @@ export class ActiveWorkflowRunner {
}
try {
await Db.collections.Webhook?.insert(webhook);
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, false);
@ -451,6 +445,7 @@ export class ActiveWorkflowRunner {
return ((workflow: Workflow, node: INode) => {
const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode, activation);
returnFunctions.__emit = (data: INodeExecutionData[][]): void => {
Logger.debug(`Received event to trigger execution for workflow "${workflow.name}"`);
this.runWorkflow(workflowData, node, data, additionalData, mode);
};
return returnFunctions;
@ -468,10 +463,11 @@ export class ActiveWorkflowRunner {
* @returns {IGetExecuteTriggerFunctions}
* @memberof ActiveWorkflowRunner
*/
getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecuteTriggerFunctions{
getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecuteTriggerFunctions {
return ((workflow: Workflow, node: INode) => {
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode, activation);
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
Logger.debug(`Received trigger for workflow "${workflow.name}"`);
WorkflowHelpers.saveStaticData(workflow);
this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) => console.error(err));
};
@ -506,6 +502,7 @@ export class ActiveWorkflowRunner {
const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated(['n8n-nodes-base.start']);
if (canBeActivated === false) {
Logger.error(`Unable to activate workflow "${workflowData.name}"`);
throw new Error(`The workflow can not be activated because it does not contain any nodes which could start the workflow. Only workflows which have trigger or webhook nodes can be activated.`);
}
@ -520,7 +517,8 @@ export class ActiveWorkflowRunner {
if (workflowInstance.getTriggerNodes().length !== 0
|| workflowInstance.getPollNodes().length !== 0) {
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions);
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions);
Logger.verbose(`Successfully activated workflow "${workflowData.name}"`, { workflowId, workflowName: workflowData.name });
}
if (this.activationErrors[workflowId] !== undefined) {
@ -571,7 +569,8 @@ export class ActiveWorkflowRunner {
// if it's active in memory then it's a trigger
// so remove from list of actives workflows
if (this.activeWorkflows.isActive(workflowId)) {
this.activeWorkflows.remove(workflowId);
await this.activeWorkflows.remove(workflowId);
Logger.verbose(`Successfully deactivated workflow "${workflowId}"`, { workflowId });
}
return;

View file

@ -3,33 +3,14 @@ import * as express from 'express';
import { join as pathJoin } from 'path';
import {
readFile as fsReadFile,
} from 'fs';
import { promisify } from 'util';
} from 'fs/promises';
import { IDataObject } from 'n8n-workflow';
import { IPackageVersions } from './';
const fsReadFileAsync = promisify(fsReadFile);
let versionCache: IPackageVersions | undefined;
/**
* Displays a message to the user
*
* @export
* @param {string} message The message to display
* @param {string} [level='log']
*/
export function logOutput(message: string, level = 'log'): void {
if (level === 'log') {
console.log(message);
} else if (level === 'error') {
console.error(message);
}
}
/**
* Returns the base URL n8n is reachable from
*
@ -72,7 +53,7 @@ export async function getVersions(): Promise<IPackageVersions> {
return versionCache;
}
const packageFile = await fsReadFileAsync(pathJoin(__dirname, '../../package.json'), 'utf8') as string;
const packageFile = await fsReadFile(pathJoin(__dirname, '../../package.json'), 'utf8') as string;
const packageData = JSON.parse(packageFile);
versionCache = {
@ -122,7 +103,7 @@ export async function getConfigValue(configKey: string): Promise<string | boolea
let data;
try {
data = await fsReadFileAsync(fileEnvironmentVariable, 'utf8') as string;
data = await fsReadFile(fileEnvironmentVariable, 'utf8') as string;
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);

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

@ -4,25 +4,26 @@ import {
} from 'n8n-core';
import {
ICredentialType,
ILogger,
INodeType,
INodeTypeData,
LoggerProxy,
} from 'n8n-workflow';
import * as config from '../config';
import {
getLogger,
} from '../src/Logger';
import {
access as fsAccess,
readdir as fsReaddir,
readFile as fsReadFile,
stat as fsStat,
} from 'fs';
} from 'fs/promises';
import * as glob from 'glob-promise';
import * as path from 'path';
import { promisify } from 'util';
const fsAccessAsync = promisify(fsAccess);
const fsReaddirAsync = promisify(fsReaddir);
const fsReadFileAsync = promisify(fsReadFile);
const fsStatAsync = promisify(fsStat);
class LoadNodesAndCredentialsClass {
@ -37,7 +38,12 @@ class LoadNodesAndCredentialsClass {
nodeModulesPath = '';
logger: ILogger;
async init() {
this.logger = getLogger();
LoggerProxy.init(this.logger);
// Get the path to the node-modules folder to be later able
// to load the credentials and nodes
const checkPaths = [
@ -49,7 +55,7 @@ class LoadNodesAndCredentialsClass {
];
for (const checkPath of checkPaths) {
try {
await fsAccessAsync(checkPath);
await fsAccess(checkPath);
// Folder exists, so use it.
this.nodeModulesPath = path.dirname(checkPath);
break;
@ -102,13 +108,13 @@ class LoadNodesAndCredentialsClass {
const getN8nNodePackagesRecursive = async (relativePath: string): Promise<string[]> => {
const results: string[] = [];
const nodeModulesPath = `${this.nodeModulesPath}/${relativePath}`;
for (const file of await fsReaddirAsync(nodeModulesPath)) {
for (const file of await fsReaddir(nodeModulesPath)) {
const isN8nNodesPackage = file.indexOf('n8n-nodes-') === 0;
const isNpmScopedPackage = file.indexOf('@') === 0;
if (!isN8nNodesPackage && !isNpmScopedPackage) {
continue;
}
if (!(await fsStatAsync(nodeModulesPath)).isDirectory()) {
if (!(await fsStat(nodeModulesPath)).isDirectory()) {
continue;
}
if (isN8nNodesPackage) { results.push(`${relativePath}${file}`); }
@ -181,6 +187,11 @@ class LoadNodesAndCredentialsClass {
tempNode.description.translationFilePath = path.join(path.dirname(filePath), 'translations', `${tempNode.description.language}.js`);
}
if (tempNode.executeSingle) {
this.logger.warn(`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`, { filePath });
}
if (this.includeNodes !== undefined && !this.includeNodes.includes(fullNodeName)) {
return;
}
@ -238,7 +249,7 @@ class LoadNodesAndCredentialsClass {
const packagePath = path.join(this.nodeModulesPath, packageName);
// Read the data from the package.json file to see if any n8n data is defiend
const packageFileString = await fsReadFileAsync(path.join(packagePath, 'package.json'), 'utf8');
const packageFileString = await fsReadFile(path.join(packagePath, 'package.json'), 'utf8');
const packageFile = JSON.parse(packageFileString);
if (!packageFile.hasOwnProperty('n8n')) {
return;

114
packages/cli/src/Logger.ts Normal file
View file

@ -0,0 +1,114 @@
import config = require('../config');
import * as winston from 'winston';
import {
IDataObject,
ILogger,
LogTypes,
} from 'n8n-workflow';
import * as callsites from 'callsites';
import { basename } from 'path';
class Logger implements ILogger {
private logger: winston.Logger;
constructor() {
const level = config.get('logs.level');
const output = (config.get('logs.output') as string).split(',').map(output => output.trim());
this.logger = winston.createLogger({
level,
});
if (output.includes('console')) {
let format: winston.Logform.Format;
if (['debug', 'verbose'].includes(level)) {
format = winston.format.combine(
winston.format.metadata(),
winston.format.timestamp(),
winston.format.colorize({ all: true }),
winston.format.printf(({ level, message, timestamp, metadata }) => {
return `${timestamp} | ${level.padEnd(18)} | ${message}` + (Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : '');
}) as winston.Logform.Format
);
} else {
format = winston.format.printf(({ message }) => message) as winston.Logform.Format;
}
this.logger.add(
new winston.transports.Console({
format,
})
);
}
if (output.includes('file')) {
const fileLogFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.metadata(),
winston.format.json()
);
this.logger.add(
new winston.transports.File({
filename: config.get('logs.file.location'),
format: fileLogFormat,
maxsize: config.get('logs.file.fileSizeMax') as number * 1048576, // config * 1mb
maxFiles: config.get('logs.file.fileCountMax'),
})
);
}
}
log(type: LogTypes, message: string, meta: object = {}) {
const callsite = callsites();
// We are using the third array element as the structure is as follows:
// [0]: this file
// [1]: Should be LoggerProxy
// [2]: Should point to the caller.
// Note: getting line number is useless because at this point
// We are in runtime, so it means we are looking at compiled js files
const logDetails = {} as IDataObject;
if (callsite[2] !== undefined) {
logDetails.file = basename(callsite[2].getFileName() || '');
const functionName = callsite[2].getFunctionName();
if (functionName) {
logDetails.function = functionName;
}
}
this.logger.log(type, message, {...meta, ...logDetails});
}
// Convenience methods below
debug(message: string, meta: object = {}) {
this.log('debug', message, meta);
}
info(message: string, meta: object = {}) {
this.log('info', message, meta);
}
error(message: string, meta: object = {}) {
this.log('error', message, meta);
}
verbose(message: string, meta: object = {}) {
this.log('verbose', message, meta);
}
warn(message: string, meta: object = {}) {
this.log('warn', message, meta);
}
}
let activeLoggerInstance: Logger | undefined;
export function getLogger() {
if (activeLoggerInstance === undefined) {
activeLoggerInstance = new Logger();
}
return activeLoggerInstance;
}

View file

@ -7,6 +7,10 @@ import {
IPushDataType,
} from '.';
import {
LoggerProxy as Logger,
} from 'n8n-workflow';
export class Push {
private channel: sseChannel;
private connections: {
@ -24,6 +28,7 @@ export class Push {
this.channel.on('disconnect', (channel: string, res: express.Response) => {
if (res.req !== undefined) {
Logger.debug(`Remove editor-UI session`, { sessionId: res.req.query.sessionId });
delete this.connections[res.req.query.sessionId as string];
}
});
@ -39,6 +44,8 @@ export class Push {
* @memberof Push
*/
add(sessionId: string, req: express.Request, res: express.Response) {
Logger.debug(`Add editor-UI session`, { sessionId });
if (this.connections[sessionId] !== undefined) {
// Make sure to remove existing connection with the same session
// id if one exists already
@ -64,11 +71,12 @@ export class Push {
send(type: IPushDataType, data: any, sessionId?: string) { // tslint:disable-line:no-any
if (sessionId !== undefined && this.connections[sessionId] === undefined) {
// TODO: Log that properly!
console.error(`The session "${sessionId}" is not registred.`);
Logger.error(`The session "${sessionId}" is not registred.`, { sessionId });
return;
}
Logger.debug(`Send data of type "${type}" to editor-UI`, { dataType: type, sessionId });
const sendData: IPushData = {
type,
data,

View file

@ -8,7 +8,6 @@ import {
resolve as pathResolve,
} from 'path';
import {
getConnection,
getConnectionManager,
In,
} from 'typeorm';
@ -22,6 +21,8 @@ import { RequestOptions } from 'oauth-1.0a';
import * as csrf from 'csrf';
import * as requestPromise from 'request-promise-native';
import { createHmac } from 'crypto';
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
import { compare } from 'bcryptjs';
import * as promClient from 'prom-client';
@ -132,6 +133,7 @@ class App {
protocol: string;
sslKey: string;
sslCert: string;
payloadSizeMax: number;
presetCredentialsLoaded: boolean;
@ -145,6 +147,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 +372,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 +381,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 +389,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;
},
@ -570,6 +573,7 @@ class App {
const newWorkflowData = req.body as IWorkflowBase;
const id = req.params.id;
newWorkflowData.id = id;
await this.externalHooks.run('workflow.update', [newWorkflowData]);
@ -714,6 +718,7 @@ class App {
// get generated dynamically
this.app.get(`/${this.restEndpoint}/node-parameter-options`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<INodePropertyOptions[]> => {
const nodeType = req.query.nodeType as string;
const path = req.query.path as string;
let credentials: INodeCredentials | undefined = undefined;
const currentNodeParameters = JSON.parse('' + req.query.currentNodeParameters) as INodeParameters;
if (req.query.credentials !== undefined) {
@ -723,7 +728,7 @@ class App {
const nodeTypes = NodeTypes();
const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, JSON.parse('' + req.query.currentNodeParameters), credentials!);
const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, path, JSON.parse('' + req.query.currentNodeParameters), credentials!);
const workflowData = loadDataInstance.getWorkflowData() as IWorkflowBase;
const workflowCredentials = await WorkflowCredentials(workflowData.nodes);
@ -1737,6 +1742,7 @@ class App {
}
);
}
returnData.sort((a, b) => parseInt(b.id, 10) - parseInt(a.id, 10));
return returnData;
}

View file

@ -30,13 +30,14 @@ import {
IWebhookData,
IWebhookResponseData,
IWorkflowExecuteAdditionalData,
LoggerProxy as Logger,
NodeHelpers,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
const activeExecutions = ActiveExecutions.getInstance();
const activeExecutions = ActiveExecutions.getInstance();
/**
* Returns all the webhooks which should be created for the give workflow
@ -144,7 +145,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 +157,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,
},
},
};
@ -285,6 +287,8 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
const workflowRunner = new WorkflowRunner();
const executionId = await workflowRunner.run(runData, true, !didSendResponse);
Logger.verbose(`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`, { executionId });
// Get a promise which resolves when the workflow did execute and send then response
const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<IExecutionDb | undefined>;
executePromise.then((data) => {

View file

@ -8,6 +8,7 @@ import {
IExecutionResponse,
IPushDataExecutionFinished,
IWorkflowBase,
IWorkflowExecuteProcess,
IWorkflowExecutionDataProcess,
NodeTypes,
Push,
@ -36,6 +37,7 @@ import {
IWorkflowExecuteAdditionalData,
IWorkflowExecuteHooks,
IWorkflowHooksOptionalParameters,
LoggerProxy as Logger,
Workflow,
WorkflowExecuteMode,
WorkflowHooks,
@ -43,11 +45,10 @@ import {
import * as config from '../config';
import { LessThanOrEqual } from "typeorm";
import { LessThanOrEqual } from 'typeorm';
const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
/**
* Checks if there was an error and if errorWorkflow or a trigger is defined. If so it collects
* all the data and executes it
@ -84,9 +85,11 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo
// Run the error workflow
// To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow.
if (workflowData.settings !== undefined && workflowData.settings.errorWorkflow && !(mode === 'error' && workflowData.id && workflowData.settings.errorWorkflow.toString() === workflowData.id.toString())) {
Logger.verbose(`Start external error workflow`, { executionId, errorWorkflowId: workflowData.settings.errorWorkflow.toString(), workflowId: workflowData.id });
// If a specific error workflow is set run only that one
WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData);
} else if (mode !== 'error' && workflowData.id !== undefined && workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)) {
Logger.verbose(`Start internal error workflow`, { executionId, workflowId: workflowData.id });
// If the workflow contains
WorkflowHelpers.executeErrorWorkflow(workflowData.id.toString(), workflowErrorData);
}
@ -101,6 +104,8 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo
let throttling = false;
function pruneExecutionData(): void {
if (!throttling) {
Logger.verbose('Pruning execution data from database');
throttling = true;
const timeout = config.get('executions.pruneDataTimeout') as number; // in seconds
const maxAge = config.get('executions.pruneDataMaxAge') as number; // in h
@ -132,6 +137,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
if (this.sessionId === undefined) {
return;
}
Logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id });
const pushInstance = Push.getInstance();
pushInstance.send('nodeExecuteBefore', {
@ -146,6 +152,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
if (this.sessionId === undefined) {
return;
}
Logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id });
const pushInstance = Push.getInstance();
pushInstance.send('nodeExecuteAfter', {
@ -157,6 +164,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
],
workflowExecuteBefore: [
async function (this: WorkflowHooks): Promise<void> {
Logger.debug(`Executing hook (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id });
// Push data to session which started the workflow
if (this.sessionId === undefined) {
return;
@ -167,13 +175,14 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
mode: this.mode,
startedAt: new Date(),
retryOf: this.retryOf,
workflowId: this.workflowData.id as string,
workflowId: this.workflowData.id, sessionId: this.sessionId as string,
workflowName: this.workflowData.name,
}, this.sessionId);
},
],
workflowExecuteAfter: [
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> {
Logger.debug(`Executing hook (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id });
// Push data to session which started the workflow
if (this.sessionId === undefined) {
return;
@ -194,6 +203,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
};
// Push data to editor-ui once workflow finished
Logger.debug(`Save execution progress to database for execution ID ${this.executionId} `, { executionId: this.executionId, workflowId: this.workflowData.id });
// TODO: Look at this again
const sendData: IPushDataExecutionFinished = {
executionId: this.executionId,
@ -231,6 +241,8 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
}
try {
Logger.debug(`Save execution progress to database for execution ID ${this.executionId} `, { executionId: this.executionId, nodeName });
const execution = await Db.collections.Execution!.findOne(this.executionId);
if (execution === undefined) {
@ -285,7 +297,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
// For busy machines, we may get "Database is locked" errors.
// We do this to prevent crashes and executions ending in `unknown` state.
console.log(`Failed saving execution progress to database for execution ID ${this.executionId}`, err);
Logger.error(`Failed saving execution progress to database for execution ID ${this.executionId} (hookFunctionsPreExecute, nodeExecuteAfter)`, { ...err, executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id });
}
},
@ -306,6 +318,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
workflowExecuteBefore: [],
workflowExecuteAfter: [
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> {
Logger.debug(`Executing hook (hookFunctionsSave)`, { executionId: this.executionId, workflowId: this.workflowData.id });
// Prune old execution data
if (config.get('executions.pruneData')) {
@ -320,8 +333,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
try {
await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData);
} catch (e) {
// TODO: Add proper logging!
console.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: ${e.message}`);
Logger.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (hookFunctionsSave)`, { executionId: this.executionId, workflowId: this.workflowData.id });
}
}
@ -374,6 +386,9 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
fullExecutionData.workflowId = this.workflowData.id.toString();
}
// Leave log message before flatten as that operation increased memory usage a lot and the chance of a crash is highest here
Logger.debug(`Save execution data to database for execution ID ${this.executionId}`, { executionId: this.executionId, workflowId: this.workflowData.id });
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
// Save the Execution in DB
@ -419,8 +434,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
try {
await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData);
} catch (e) {
// TODO: Add proper logging!
console.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: ${e.message}`);
Logger.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (workflowExecuteAfter)`, { sessionId: this.sessionId, workflowId: this.workflowData.id });
}
}
@ -569,7 +583,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();
@ -606,9 +620,27 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
// and get executionID from `activeExecutions` running on main process
additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow;
let subworkflowTimeout = additionalData.executionTimeoutTimestamp;
if (workflowData.settings?.executionTimeout !== undefined && workflowData.settings.executionTimeout > 0) {
// We might have received a max timeout timestamp from the parent workflow
// If we did, then we get the minimum time between the two timeouts
// If no timeout was given from the parent, then we use our timeout.
subworkflowTimeout = Math.min(additionalData.executionTimeoutTimestamp || Number.MAX_SAFE_INTEGER, Date.now() + (workflowData.settings.executionTimeout as number * 1000));
}
additionalDataIntegrated.executionTimeoutTimestamp = subworkflowTimeout;
// 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 +648,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 +671,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 +693,7 @@ export async function getBase(credentials: IWorkflowCredentials, currentNodePara
webhookBaseUrl,
webhookTestBaseUrl,
currentNodeParameters,
executionTimeoutTimestamp,
};
}

View file

@ -18,8 +18,8 @@ import {
IRunExecutionData,
ITaskData,
IWorkflowCredentials,
Workflow,
} from 'n8n-workflow';
LoggerProxy as Logger,
Workflow,} from 'n8n-workflow';
import * as config from '../config';
@ -86,7 +86,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
if (workflowData === undefined) {
// The error workflow could not be found
console.error(`ERROR: Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find error workflow "${workflowId}"`);
Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find error workflow "${workflowId}"`, { workflowId });
return;
}
@ -105,7 +105,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
}
if (workflowStartNode === undefined) {
console.error(`ERROR: Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`);
Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`);
return;
}
@ -153,7 +153,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
const workflowRunner = new WorkflowRunner();
await workflowRunner.run(runData);
} catch (error) {
console.error(`ERROR: Calling Error Workflow for "${workflowErrorData.workflow.id}": ${error.message}`);
Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`, { workflowId: workflowErrorData.workflow.id });
}
}
@ -315,8 +315,7 @@ export async function saveStaticData(workflow: Workflow): Promise <void> {
await saveStaticDataById(workflow.id!, workflow.staticData);
workflow.staticData.__dataChanged = false;
} catch (e) {
// TODO: Add proper logging!
console.error(`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: ${e.message}`);
Logger.error(`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${e.message}"`, { workflowId: workflow.id });
}
}
}

View file

@ -27,12 +27,13 @@ import {
} from 'n8n-core';
import {
IDataObject,
IExecutionError,
ExecutionError,
IRun,
LoggerProxy as Logger,
Workflow,
WorkflowExecuteMode,
WorkflowHooks,
WorkflowOperationError,
} from 'n8n-workflow';
import * as config from '../config';
@ -78,13 +79,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,25 +159,43 @@ 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);
Logger.verbose(`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, {executionId});
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true);
let workflowExecution: PCancelable<IRun>;
if (data.executionData !== undefined) {
Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {executionId});
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode, data.executionData);
workflowExecution = workflowExecute.processRunExecutionData(workflow);
} else if (data.runData === undefined || data.startNodes === undefined || data.startNodes.length === 0 || data.destinationNode === undefined) {
Logger.debug(`Execution ID ${executionId} will run executing all nodes.`, {executionId});
// Execute all nodes
// Can execute without webhook so go on
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode);
} else {
Logger.debug(`Execution ID ${executionId} is a partial execution.`, {executionId});
// Execute only the nodes between start and destination nodes
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
workflowExecution = workflowExecute.runPartialWorkflow(workflow, data.runData, data.startNodes, data.destinationNode);
@ -184,14 +203,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 +262,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 +292,6 @@ export class WorkflowRunner {
* the database. *
*************************************************/
let watchDogInterval: NodeJS.Timeout | undefined;
let resolved = false;
const watchDog = new Promise((res) => {
watchDogInterval = setInterval(async () => {
@ -303,28 +312,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 +356,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 +375,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 +418,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 +430,96 @@ 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') {
Logger.debug(`Received child process message of type ${message.type} for execution ID ${executionId}.`, {executionId});
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'){
Logger.debug(`Subprocess for execution ID ${executionId} timed out.`, {executionId});
// 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) {
Logger.debug(`Subprocess for execution ID ${executionId} finished with error code ${code}.`, {executionId});
// 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,30 +17,39 @@ import {
} from 'n8n-core';
import {
ExecutionError,
IDataObject,
IExecuteData,
IExecuteWorkflowInfo,
IExecutionError,
ILogger,
INodeExecutionData,
INodeType,
INodeTypeData,
IRun,
IRunExecutionData,
ITaskData,
IWorkflowExecuteAdditionalData,
IWorkflowExecuteHooks,
LoggerProxy,
Workflow,
WorkflowHooks,
WorkflowOperationError,
} from 'n8n-workflow';
import {
getLogger,
} from '../src/Logger';
import * as config from '../config';
export class WorkflowRunnerProcess {
data: IWorkflowExecutionDataProcessWithExecution | undefined;
logger: ILogger;
startedAt = new Date();
workflow: Workflow | undefined;
workflowExecute: WorkflowExecute | undefined;
executionIdCallback: (executionId: string) => void | undefined;
childExecutions: {
[key: string]: IWorkflowExecuteProcess,
} = {};
static async stopProcess() {
setTimeout(() => {
@ -53,7 +63,13 @@ export class WorkflowRunnerProcess {
process.on('SIGTERM', WorkflowRunnerProcess.stopProcess);
process.on('SIGINT', WorkflowRunnerProcess.stopProcess);
const logger = this.logger = getLogger();
LoggerProxy.init(logger);
this.data = inputData;
logger.verbose('Initializing n8n sub-process', { pid: process.pid, workflowId: this.data.workflowData.id });
let className: string;
let tempNode: INodeType;
let filePath: string;
@ -107,8 +123,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,13 +149,21 @@ 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);
@ -160,19 +194,14 @@ export class WorkflowRunnerProcess {
* @param {any[]} parameters
* @memberof WorkflowRunnerProcess
*/
sendHookToParentProcess(hook: string, parameters: any[]) { // tslint:disable-line:no-any
async sendHookToParentProcess(hook: string, parameters: any[]) { // tslint:disable-line:no-any
try {
sendToParentProcess('processHook', {
await sendToParentProcess('processHook', {
hook,
parameters,
});
} catch (error) {
// TODO: Add proper logging
console.error(`There was a problem sending hook: "${hook}"`);
console.error('Parameters:');
console.error(parameters);
console.error('Error:');
console.error(error);
this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error});
}
}
@ -188,22 +217,22 @@ export class WorkflowRunnerProcess {
const hookFunctions: IWorkflowExecuteHooks = {
nodeExecuteBefore: [
async (nodeName: string): Promise<void> => {
this.sendHookToParentProcess('nodeExecuteBefore', [nodeName]);
await this.sendHookToParentProcess('nodeExecuteBefore', [nodeName]);
},
],
nodeExecuteAfter: [
async (nodeName: string, data: ITaskData): Promise<void> => {
this.sendHookToParentProcess('nodeExecuteAfter', [nodeName, data]);
await this.sendHookToParentProcess('nodeExecuteAfter', [nodeName, data]);
},
],
workflowExecuteBefore: [
async (): Promise<void> => {
this.sendHookToParentProcess('workflowExecuteBefore', []);
await this.sendHookToParentProcess('workflowExecuteBefore', []);
},
],
workflowExecuteAfter: [
async (fullRunData: IRun, newStaticData?: IDataObject): Promise<void> => {
this.sendHookToParentProcess('workflowExecuteAfter', [fullRunData, newStaticData]);
await this.sendHookToParentProcess('workflowExecuteAfter', [fullRunData, newStaticData]);
},
],
};
@ -254,6 +283,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 +298,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 +344,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

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as config from '../../../../config';
export class ChangeCredentialDataSize1620729500000 implements MigrationInterface {
name = 'ChangeCredentialDataSize1620729500000';
async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'credentials_entity` MODIFY COLUMN `type` varchar(128) NOT NULL');
}
async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'credentials_entity` MODIFY COLUMN `type` varchar(32) NOT NULL');
}
}

View file

@ -4,6 +4,7 @@ import { CreateIndexStoppedAt1594902918301 } from './1594902918301-CreateIndexSt
import { AddWebhookId1611149998770 } from './1611149998770-AddWebhookId';
import { MakeStoppedAtNullable1607431743767 } from './1607431743767-MakeStoppedAtNullable';
import { ChangeDataSize1615306975123 } from './1615306975123-ChangeDataSize';
import { ChangeCredentialDataSize1620729500000 } from './1620729500000-ChangeCredentialDataSize';
export const mysqlMigrations = [
InitialMigration1588157391238,
@ -12,4 +13,5 @@ export const mysqlMigrations = [
AddWebhookId1611149998770,
MakeStoppedAtNullable1607431743767,
ChangeDataSize1615306975123,
ChangeCredentialDataSize1620729500000,
];

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.67.0",
"version": "0.72.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.59.0",
"oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0",
"request": "^2.88.2",

View file

@ -7,6 +7,7 @@ import {
IPollResponse,
ITriggerResponse,
IWorkflowExecuteAdditionalData,
LoggerProxy as Logger,
Workflow,
WorkflowActivateMode,
WorkflowExecuteMode,
@ -17,6 +18,7 @@ import {
IWorkflowData,
} from './';
export class ActiveWorkflows {
private workflowData: {
[key: string]: IWorkflowData;
@ -163,6 +165,7 @@ export class ActiveWorkflows {
// The trigger function to execute when the cron-time got reached
const executeTrigger = async () => {
Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, {workflowName: workflow.name, workflowId: workflow.id});
const pollResponse = await workflow.runPoll(node, pollFunctions);
if (pollResponse !== null) {

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

@ -18,10 +18,12 @@ const TEMP_WORKFLOW_NAME = 'Temp-Workflow';
export class LoadNodeParameterOptions {
path: string;
workflow: Workflow;
constructor(nodeTypeName: string, nodeTypes: INodeTypes, currentNodeParameters: INodeParameters, credentials?: INodeCredentials) {
constructor(nodeTypeName: string, nodeTypes: INodeTypes, path: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials) {
this.path = path;
const nodeType = nodeTypes.getByName(nodeTypeName);
if (nodeType === undefined) {
@ -89,7 +91,7 @@ export class LoadNodeParameterOptions {
throw new Error(`The node-type "${node!.type}" does not have the method "${methodName}" defined!`);
}
const thisArgs = NodeExecuteFunctions.getLoadOptionsFunctions(this.workflow, node!, additionalData);
const thisArgs = NodeExecuteFunctions.getLoadOptionsFunctions(this.workflow, node!, this.path, additionalData);
return nodeType!.methods.loadOptions[methodName].call(thisArgs);
}

View file

@ -32,6 +32,7 @@ import {
IWorkflowExecuteAdditionalData,
IWorkflowMetadata,
NodeHelpers,
NodeOperationError,
NodeParameterValue,
Workflow,
WorkflowActivateMode,
@ -50,7 +51,13 @@ import * as requestPromise from 'request-promise-native';
import { createHmac } from 'crypto';
import { fromBuffer } from 'file-type';
import { lookup } from 'mime-types';
import {
LoggerProxy as Logger,
} from 'n8n-workflow';
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
@ -184,8 +191,12 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin
};
}
Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" expired. Should revalidate.`);
const newToken = await token.refresh(tokenRefreshOptions);
Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" has been renewed.`);
credentials.oauthTokenData = newToken.data;
// Find the name of the credentials
@ -197,6 +208,8 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin
// Save the refreshed token
await additionalData.credentialsHelper.updateCredentials(name, credentialsType, credentials);
Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" has been saved to database successfully.`);
// Make the request again with the new token
const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject);
@ -309,16 +322,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 +346,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 +589,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 +655,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);
},
@ -678,7 +691,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
return continueOnFail(node);
},
evaluateExpression: (expression: string, itemIndex: number) => {
return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode);
return workflow.expression.resolveSimpleParameterValue('=' + expression, {}, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode);
},
async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any> { // tslint:disable-line:no-any
return additionalData.executeWorkflow(workflowInfo, additionalData, inputData);
@ -729,7 +742,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
return getWorkflowMetadata(workflow);
},
getWorkflowDataProxy: (itemIndex: number): IWorkflowDataProxyData => {
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode);
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, {}, mode);
return dataProxy.getDataProxy();
},
getWorkflowStaticData(type: string): IDataObject {
@ -738,7 +751,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);
},
@ -776,7 +789,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
},
evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => {
evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex;
return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData, mode);
return workflow.expression.resolveSimpleParameterValue('=' + expression, {}, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData, mode);
},
getContext(type: string): IContextObject {
return NodeHelpers.getContext(runExecutionData, type, node);
@ -828,7 +841,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
return getWorkflowMetadata(workflow);
},
getWorkflowDataProxy: (): IWorkflowDataProxyData => {
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode);
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, {}, mode);
return dataProxy.getDataProxy();
},
getWorkflowStaticData(type: string): IDataObject {
@ -836,7 +849,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);
},
@ -858,18 +871,20 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
* @param {IWorkflowExecuteAdditionalData} additionalData
* @returns {ILoadOptionsFunctions}
*/
export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData): ILoadOptionsFunctions {
return ((workflow: Workflow, node: INode) => {
export function getLoadOptionsFunctions(workflow: Workflow, node: INode, path: string, additionalData: IWorkflowExecuteAdditionalData): ILoadOptionsFunctions {
return ((workflow: Workflow, node: INode, path: string) => {
const that = {
getCredentials(type: string): ICredentialDataDecryptedObject | undefined {
return getCredentials(workflow, node, type, additionalData, 'internal');
},
getCurrentNodeParameter: (parameterName: string): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object | undefined => {
getCurrentNodeParameter: (parameterPath: string): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object | undefined => {
const nodeParameters = additionalData.currentNodeParameters;
if (nodeParameters && nodeParameters[parameterName]) {
return nodeParameters[parameterName];
if (parameterPath.charAt(0) === '&') {
parameterPath = `${path.split('.').slice(1, -1).join('.')}.${parameterPath.slice(1)}`;
}
return undefined;
return get(nodeParameters, parameterPath);
},
getCurrentNodeParameters: (): INodeParameters | undefined => {
return additionalData.currentNodeParameters;
@ -892,7 +907,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);
},
@ -902,7 +917,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio
},
};
return that;
})(workflow, node);
})(workflow, node, path);
}
@ -962,7 +977,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 +1077,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,
@ -15,8 +15,10 @@ import {
ITaskDataConnections,
IWaitingForExecution,
IWorkflowExecuteAdditionalData,
LoggerProxy as Logger,
Workflow,
WorkflowExecuteMode,
WorkflowOperationError,
} from 'n8n-workflow';
import {
NodeExecuteFunctions,
@ -399,6 +401,13 @@ export class WorkflowExecute {
nodeToAdd = parentNode;
}
const parentNodesNodeToAdd = workflow.getParentNodes(nodeToAdd as string);
if (parentNodesNodeToAdd.includes(parentNodeName) && nodeSuccessData[outputIndex].length === 0) {
// We do not add the node if there is no input data and the node that should be connected
// is a child of the parent node. Because else it would run a node even though it should be
// specifically not run, as it did not receive any data.
nodeToAdd = undefined;
}
if (nodeToAdd === undefined) {
// No node has to get added so process
@ -481,6 +490,8 @@ export class WorkflowExecute {
* @memberof WorkflowExecute
*/
processRunExecutionData(workflow: Workflow): PCancelable<IRun> {
Logger.verbose('Workflow execution started', { workflowId: workflow.id });
const startedAt = new Date();
const workflowIssues = workflow.checkReadyForExecution();
@ -490,7 +501,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;
@ -501,7 +512,6 @@ export class WorkflowExecute {
this.runExecutionData.startData = {};
}
let currentExecutionTry = '';
let lastExecutionTry = '';
@ -517,8 +527,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 +559,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();
@ -557,6 +573,7 @@ export class WorkflowExecute {
executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
executionNode = executionData.node;
Logger.debug(`Start processing node "${executionNode.name}"`, { node: executionNode.name, workflowId: workflow.id });
await this.executeHook('nodeExecuteBefore', [executionNode.name]);
// Get the index of the current run
@ -654,7 +671,9 @@ export class WorkflowExecute {
}
}
Logger.debug(`Running node "${executionNode.name}" started`, { node: executionNode.name, workflowId: workflow.id });
nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
Logger.debug(`Running node "${executionNode.name}" finished successfully`, { node: executionNode.name, workflowId: workflow.id });
if (nodeSuccessData === undefined) {
// Node did not get executed
@ -683,12 +702,16 @@ export class WorkflowExecute {
break;
} catch (error) {
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
executionError = {
...error,
message: error.message,
stack: error.stack,
};
Logger.debug(`Running node "${executionNode.name}" finished with error`, { node: executionNode.name, workflowId: workflow.id });
}
}
@ -784,7 +807,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 +815,7 @@ export class WorkflowExecute {
const fullRunData = this.getFullRunData(startedAt);
fullRunData.data.resultData.error = {
...error,
message: error.message,
stack: error.stack,
};
@ -815,12 +839,14 @@ 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) {
Logger.verbose(`Workflow execution finished with error`, { error: executionError, workflowId: workflow.id });
fullRunData.data.resultData.error = executionError;
} else {
Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id });
fullRunData.finished = true;
}

View file

@ -465,6 +465,30 @@ class NodeTypesClass implements INodeTypes {
},
},
},
'n8n-nodes-base.noOp': {
sourcePath: '',
type: {
description: {
displayName: 'No Operation, do nothing',
name: 'noOp',
icon: 'fa:arrow-right',
group: ['organization'],
version: 1,
description: 'No Operation',
defaults: {
name: 'NoOp',
color: '#b0b0b0',
},
inputs: ['main'],
outputs: ['main'],
properties: [],
},
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
return this.prepareOutputData(items);
},
},
},
'n8n-nodes-base.set': {
sourcePath: '',
type: {

View file

@ -1,8 +1,10 @@
import {
IConnections,
ILogger,
INode,
IRun,
LoggerProxy,
Workflow,
} from 'n8n-workflow';
@ -1152,11 +1154,188 @@ describe('WorkflowExecute', () => {
},
},
},
{
description: 'should not use empty data in sibling if parent did not send any data',
input: {
// Leave the workflowData in regular JSON to be able to easily
// copy it from/in the UI
workflowData: {
"nodes": [
{
"parameters": {},
"name": "Start",
"type": "n8n-nodes-base.start",
"typeVersion": 1,
"position": [
250,
300,
],
},
{
"parameters": {
"values": {
"number": [
{
"name": "value1",
},
],
},
"options": {},
},
"name": "Set",
"type": "n8n-nodes-base.set",
"typeVersion": 1,
"position": [
450,
300,
],
},
{
"parameters": {},
"name": "Merge",
"type": "n8n-nodes-base.merge",
"typeVersion": 1,
"position": [
1050,
250,
],
},
{
"parameters": {
"conditions": {
"number": [
{
"value1": "={{$json[\"value1\"]}}",
"operation": "equal",
"value2": 1,
},
],
},
},
"name": "IF",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
650,
300,
],
},
{
"parameters": {},
"name": "NoOpTrue",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
850,
150,
],
},
{
"parameters": {},
"name": "NoOpFalse",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
850,
400,
],
},
],
"connections": {
"Start": {
"main": [
[
{
"node": "Set",
"type": "main",
"index": 0,
},
],
],
},
"Set": {
"main": [
[
{
"node": "IF",
"type": "main",
"index": 0,
},
],
],
},
"IF": {
"main": [
[
{
"node": "NoOpTrue",
"type": "main",
"index": 0,
},
{
"node": "Merge",
"type": "main",
"index": 1,
},
],
[
{
"node": "NoOpFalse",
"type": "main",
"index": 0,
},
],
],
},
"NoOpTrue": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0,
},
],
],
},
},
},
},
output: {
nodeExecutionOrder: [
'Start',
'Set',
'IF',
'NoOpFalse',
],
nodeData: {
IF: [
[],
],
NoOpFalse: [
[
{
value1: 0,
},
],
],
},
},
},
];
const fakeLogger = {
log: () => {},
debug: () => {},
verbose: () => {},
info: () => {},
warn: () => {},
error: () => {},
} as ILogger;
const executionMode = 'manual';
const nodeTypes = Helpers.NodeTypes();
LoggerProxy.init(fakeLogger);
for (const testData of tests) {
test(testData.description, async () => {
@ -1201,7 +1380,6 @@ describe('WorkflowExecute', () => {
expect(result.finished).toEqual(true);
expect(result.data.executionData!.contextData).toEqual({});
expect(result.data.executionData!.nodeExecutionStack).toEqual([]);
expect(result.data.executionData!.waitingExecution).toEqual({});
});
}

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.84.0",
"version": "0.91.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -37,7 +37,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",
@ -66,7 +66,7 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.55.0",
"n8n-workflow": "~0.59.0",
"node-sass": "^4.12.0",
"normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1",

View file

@ -131,7 +131,7 @@ export interface IRestApi {
getSettings(): Promise<IN8nUISettings>;
getNodeTypes(): Promise<INodeTypeDescription[]>;
getNodesInformation(nodeList: string[]): Promise<INodeTypeDescription[]>;
getNodeParameterOptions(nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
getNodeParameterOptions(nodeType: string, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
removeTestWebhook(workflowId: string): Promise<boolean>;
runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>;
createNewWorkflow(sendData: IWorkflowData): Promise<IWorkflowDb>;
@ -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"
@ -50,6 +50,7 @@
<script lang="ts">
import Vue from 'vue';
import { externalHooks } from '@/components/mixins/externalHooks';
import { restApi } from '@/components/mixins/restApi';
import { showMessage } from '@/components/mixins/showMessage';
import CredentialsInput from '@/components/CredentialsInput.vue';
@ -71,6 +72,7 @@ import { INodeUi } from '../Interface';
export default mixins(
restApi,
showMessage,
externalHooks,
).extend({
name: 'CredentialsEdit',
props: [
@ -195,9 +197,11 @@ export default mixins(
});
return;
}
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) {
@ -224,6 +228,9 @@ export default mixins(
this.credentialType = null;
}
},
async credentialType (newValue, oldValue) {
this.$externalHooks().run('credentialsEdit.credentialTypeChanged', { newValue, oldValue, editCredentials: !!this.editCredentials, credentialType: this.credentialType, setCredentialType: this.setCredentialType });
},
},
methods: {
getCredentialProperties (name: string): INodeProperties[] {

View file

@ -37,6 +37,7 @@
</template>
<script lang="ts">
import { externalHooks } from '@/components/mixins/externalHooks';
import { restApi } from '@/components/mixins/restApi';
import { ICredentialsResponse } from '@/Interface';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
@ -47,6 +48,7 @@ import { genericHelpers } from '@/components/mixins/genericHelpers';
import mixins from 'vue-typed-mixins';
export default mixins(
externalHooks,
genericHelpers,
nodeHelpers,
restApi,
@ -75,6 +77,7 @@ export default mixins(
this.loadCredentials();
this.loadCredentialTypes();
}
this.$externalHooks().run('credentialsList.dialogVisibleChanged', { dialogVisible: newValue });
},
},
methods: {

View file

@ -26,7 +26,7 @@
</svg>
<div v-if="showDocumentHelp && nodeType" class="text">
Need help? <a id="doc-hyperlink" v-if="showDocumentHelp && nodeType" :href="documentationUrl" target="_blank">Open {{nodeType.displayName}} documentation</a>
Need help? <a id="doc-hyperlink" v-if="showDocumentHelp && nodeType" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open {{nodeType.displayName}} documentation</a>
</div>
</div>
</transition>
@ -49,10 +49,16 @@ import {
IUpdateInformation,
} from '../Interface';
import { externalHooks } from '@/components/mixins/externalHooks';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import NodeSettings from '@/components/NodeSettings.vue';
import RunData from '@/components/RunData.vue';
export default Vue.extend({
import mixins from 'vue-typed-mixins';
export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
name: 'DataDisplay',
components: {
NodeSettings,
@ -88,6 +94,13 @@ export default Vue.extend({
return null;
},
},
watch: {
node (node, oldNode) {
if(node && !oldNode) {
this.$externalHooks().run('dataDisplay.nodeTypeChanged', { nodeSubtitle: this.getNodeSubtitle(node, this.nodeType, this.getWorkflow()) });
}
},
},
methods: {
valueChanged (parameterData: IUpdateInformation) {
this.$emit('valueChanged', parameterData);
@ -102,6 +115,9 @@ export default Vue.extend({
this.$store.commit('setActiveNode', null);
}
},
onDocumentationUrlClick () {
this.$externalHooks().run('dataDisplay.onDocumentationUrlClick', { nodeType: this.nodeType, documentationUrl: this.documentationUrl });
},
},
});

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

@ -158,6 +158,7 @@ import Vue from 'vue';
import ExecutionTime from '@/components/ExecutionTime.vue';
import WorkflowActivator from '@/components/WorkflowActivator.vue';
import { externalHooks } from '@/components/mixins/externalHooks';
import { restApi } from '@/components/mixins/restApi';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { showMessage } from '@/components/mixins/showMessage';
@ -182,6 +183,7 @@ import {
import mixins from 'vue-typed-mixins';
export default mixins(
externalHooks,
genericHelpers,
restApi,
showMessage,
@ -436,7 +438,8 @@ export default mixins(
this.$store.commit('setActiveExecutions', results[1]);
const alreadyPresentExecutionIds = this.finishedExecutions.map(exec => exec.id);
// execution IDs are typed as string, int conversion is necessary so we can order.
const alreadyPresentExecutionIds = this.finishedExecutions.map(exec => parseInt(exec.id, 10));
let lastId = 0;
const gaps = [] as number[];
for(let i = results[0].results.length - 1; i >= 0; i--) {
@ -457,7 +460,7 @@ export default mixins(
// Check new results from end to start
// Add new items accordingly.
const executionIndex = alreadyPresentExecutionIds.indexOf(currentItem.id);
const executionIndex = alreadyPresentExecutionIds.indexOf(currentId);
if (executionIndex !== -1) {
// Execution that we received is already present.
@ -475,7 +478,7 @@ export default mixins(
// Find the correct position to place this newcomer
let j;
for (j = this.finishedExecutions.length - 1; j >= 0; j--) {
if (currentItem.id < this.finishedExecutions[j].id) {
if (currentId < parseInt(this.finishedExecutions[j].id, 10)) {
this.finishedExecutions.splice(j + 1, 0, currentItem);
break;
}
@ -558,6 +561,8 @@ export default mixins(
await this.loadWorkflows();
await this.refreshData();
this.handleAutoRefreshToggle();
this.$externalHooks().run('executionsList.openDialog');
},
async retryExecution (execution: IExecutionShortResponse, loadWorkflow?: boolean) {
this.isDataLoading = true;

View file

@ -30,7 +30,7 @@
<div class="editor-description">
Result
</div>
<expression-input :parameter="parameter" resolvedValue="true" rows="8" :value="value" :path="path"></expression-input>
<expression-input :parameter="parameter" resolvedValue="true" ref="expressionResult" rows="8" :value="value" :path="path"></expression-input>
</div>
</el-col>
@ -52,7 +52,11 @@ import {
Workflow,
} from 'n8n-workflow';
export default Vue.extend({
import { externalHooks } from '@/components/mixins/externalHooks';
import mixins from 'vue-typed-mixins';
export default mixins(externalHooks).extend({
name: 'ExpressionEdit',
props: [
'dialogVisible',
@ -81,7 +85,16 @@ export default Vue.extend({
},
itemSelected (eventData: IVariableItemSelected) {
// User inserted item from Expression Editor variable selector
(this.$refs.inputFieldExpression as any).itemSelected(eventData); // tslint:disable-line:no-any
this.$externalHooks().run('expressionEdit.itemSelected', { parameter: this.parameter, value: this.value, selectedItem: eventData });
},
},
watch: {
dialogVisible (newValue) {
const resolvedExpressionValue = this.$refs.expressionResult && (this.$refs.expressionResult as any).getValue() || undefined; // tslint:disable-line:no-any
this.$externalHooks().run('expressionEdit.dialogVisibleChanged', { dialogVisible: newValue, parameter: this.parameter, value: this.value, resolvedExpressionValue });
},
},
});

View file

@ -178,7 +178,11 @@ export default mixins(genericHelpers, translate)
} else if (optionParameter.typeOptions !== undefined && optionParameter.typeOptions.multipleValues === true) {
// Multiple values are allowed so append option to array
newParameterValue[optionParameter.name] = get(this.nodeValues, `${this.path}.${optionParameter.name}`, []);
(newParameterValue[optionParameter.name] as INodeParameters[]).push(JSON.parse(JSON.stringify(optionParameter.default)));
if (Array.isArray(optionParameter.default)) {
(newParameterValue[optionParameter.name] as INodeParameters[]).push(...JSON.parse(JSON.stringify(optionParameter.default)));
} else if (optionParameter.default !== '' && typeof optionParameter.default !== 'object') {
(newParameterValue[optionParameter.name] as INodeParameters[]).push(JSON.parse(JSON.stringify(optionParameter.default)));
}
} else {
// Add a new option
newParameterValue[optionParameter.name] = JSON.parse(JSON.stringify(optionParameter.default));

View file

@ -21,6 +21,8 @@
</a>
</el-menu-item>
<MenuItemsIterator :items="sidebarMenuTopItems" :root="true"/>
<el-submenu index="workflow" title="Workflow">
<template slot="title">
<font-awesome-icon icon="network-wired"/>&nbsp;
@ -120,30 +122,8 @@
<span slot="title" class="item-title-root">Help</span>
</template>
<el-menu-item index="help-documentation">
<template slot="title">
<a href="https://docs.n8n.io" target="_blank">
<font-awesome-icon icon="book"/>
<span slot="title" class="item-title">Documentation</span>
</a>
</template>
</el-menu-item>
<el-menu-item index="help-forum">
<template slot="title">
<a href="https://community.n8n.io" target="_blank">
<font-awesome-icon icon="users"/>
<span slot="title" class="item-title">Forum</span>
</a>
</template>
</el-menu-item>
<el-menu-item index="help-examples">
<template slot="title">
<a href="https://n8n.io/workflows" target="_blank">
<font-awesome-icon icon="network-wired"/>
<span slot="title" class="item-title">Workflows</span>
</a>
</template>
</el-menu-item>
<MenuItemsIterator :items="helpMenuItems" />
<el-menu-item index="help-about">
<template slot="title">
<font-awesome-icon class="about-icon" icon="info"/>
@ -152,6 +132,8 @@
</el-menu-item>
</el-submenu>
<MenuItemsIterator :items="sidebarMenuBottomItems" :root="true"/>
</el-menu>
</div>
@ -160,6 +142,7 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { MessageBoxInputData } from 'element-ui/types/message-box';
@ -167,6 +150,7 @@ import {
IExecutionResponse,
IExecutionsStopData,
IWorkflowDataUpdate,
IMenuItem,
} from '../Interface';
import About from '@/components/About.vue';
@ -186,6 +170,40 @@ import { workflowRun } from '@/components/mixins/workflowRun';
import { saveAs } from 'file-saver';
import mixins from 'vue-typed-mixins';
import MenuItemsIterator from './MainSidebarMenuItemsIterator.vue';
const helpMenuItems: IMenuItem[] = [
{
id: 'docs',
type: 'link',
properties: {
href: 'https://docs.n8n.io',
title: 'Documentation',
icon: 'book',
newWindow: true,
},
},
{
id: 'forum',
type: 'link',
properties: {
href: 'https://community.n8n.io',
title: 'Forum',
icon: 'users',
newWindow: true,
},
},
{
id: 'examples',
type: 'link',
properties: {
href: 'https://n8n.io/workflows',
title: 'Workflows',
icon: 'network-wired',
newWindow: true,
},
},
];
export default mixins(
genericHelpers,
@ -204,6 +222,7 @@ export default mixins(
ExecutionsList,
WorkflowOpen,
WorkflowSettings,
MenuItemsIterator,
},
data () {
return {
@ -217,6 +236,7 @@ export default mixins(
stopExecutionInProgress: false,
workflowOpenDialogVisible: false,
workflowSettingsDialogVisible: false,
helpMenuItems,
};
},
computed: {
@ -266,6 +286,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 +559,11 @@ export default mixins(
.el-menu-item {
a {
color: #666;
&.primary-item {
color: $--color-primary;
vertical-align: baseline;
}
}
&.logo-item {

View file

@ -0,0 +1,44 @@
<template>
<div>
<el-menu-item
v-for="item in items"
:key="item.id"
:index="item.id"
@click="onClick(item)"
>
<font-awesome-icon :icon="item.properties.icon" />
<span slot="title" :class="{'item-title-root': root, 'item-title': !root}">{{ item.properties.title }}</span>
</el-menu-item>
</div>
</template>
<script lang="ts">
import { IMenuItem } from '../Interface';
import Vue from 'vue';
export default Vue.extend({
name: 'MenuItemsIterator',
props: [
'items',
'root',
],
methods: {
onClick(item: IMenuItem) {
if (item && item.type === 'link' && item.properties) {
const href = item.properties.href;
if (!href) {
return;
}
if (item.properties.newWindow) {
window.open(href);
}
else {
window.location.assign(item.properties.href);
}
}
},
},
});
</script>

View file

@ -47,14 +47,11 @@
import Vue from 'vue';
import { nodeBase } from '@/components/mixins/nodeBase';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import {
INode,
INodeIssueObjectProperty,
INodePropertyOptions,
INodeTypeDescription,
ITaskData,
NodeHelpers,
} from 'n8n-workflow';
@ -62,7 +59,7 @@ import NodeIcon from '@/components/NodeIcon.vue';
import mixins from 'vue-typed-mixins';
export default mixins(nodeBase, workflowHelpers).extend({
export default mixins(nodeBase, nodeHelpers, workflowHelpers).extend({
name: 'Node',
components: {
NodeIcon,
@ -133,41 +130,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
}
},
nodeSubtitle (): string | undefined {
if (this.data.notesInFlow) {
return this.data.notes;
}
if (this.nodeType !== null && this.nodeType.subtitle !== undefined) {
return this.workflow.expression.getSimpleParameterValue(this.data as INode, this.nodeType.subtitle, 'internal') as string | undefined;
}
if (this.data.parameters.operation !== undefined) {
const operation = this.data.parameters.operation as string;
if (this.nodeType === null) {
return operation;
}
const operationData = this.nodeType.properties.find((property) => {
return property.name === 'operation';
});
if (operationData === undefined) {
return operation;
}
if (operationData.options === undefined) {
return operation;
}
const optionData = operationData.options.find((option) => {
return (option as INodePropertyOptions).value === this.data.parameters.operation;
});
if (optionData === undefined) {
return operation;
}
return optionData.name;
}
return undefined;
return this.getNodeSubtitle(this.data, this.nodeType, this.workflow);
},
workflowRunning (): boolean {
return this.$store.getters.isActionActive('workflowRunning');
@ -186,7 +149,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
this.disableNodes([this.data]);
},
executeNode () {
this.$emit('runWorkflow', this.data.name);
this.$emit('runWorkflow', this.data.name, 'Node.executeNode');
},
deleteNode () {
Vue.nextTick(() => {

View file

@ -24,10 +24,13 @@
<script lang="ts">
import Vue from 'vue';
import { externalHooks } from "@/components/mixins/externalHooks";
import { INodeTypeDescription } from 'n8n-workflow';
import NodeCreateItem from '@/components/NodeCreateItem.vue';
export default Vue.extend({
import mixins from "vue-typed-mixins";
export default mixins(externalHooks).extend({
name: 'NodeCreateList',
components: {
NodeCreateItem,
@ -70,13 +73,18 @@ export default Vue.extend({
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
});
this.$externalHooks().run('nodeCreateList.filteredNodeTypesComputed', { nodeFilter: this.nodeFilter, result: returnData, selectedType: this.selectedType });
return returnData;
},
},
watch: {
nodeFilter (newVal, oldVal) {
nodeFilter (newValue, oldValue) {
// Reset the index whenver the filter-value changes
this.activeNodeTypeIndex = 0;
this.$externalHooks().run('nodeCreateList.nodeFilterChanged', { oldValue, newValue, selectedType: this.selectedType, filteredNodes: this.filteredNodeTypes });
},
selectedType (newValue, oldValue) {
this.$externalHooks().run('nodeCreateList.selectedTypeChanged', { oldValue, newValue });
},
},
methods: {
@ -105,6 +113,12 @@ export default Vue.extend({
this.$emit('nodeTypeSelected', nodeTypeName);
},
},
async mounted() {
this.$externalHooks().run('nodeCreateList.mounted');
},
async destroyed() {
this.$externalHooks().run('nodeCreateList.destroyed');
},
});
</script>

View file

@ -37,9 +37,6 @@
<script lang="ts">
import Vue from 'vue';
import {
INodeIssues,
INodeIssueData,
INodeIssueObjectProperty,
INodeTypeDescription,
INodeParameters,
INodeProperties,
@ -59,12 +56,14 @@ import NodeCredentials from '@/components/NodeCredentials.vue';
import NodeWebhooks from '@/components/NodeWebhooks.vue';
import { get, set, unset } from 'lodash';
import { externalHooks } from '@/components/mixins/externalHooks';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import mixins from 'vue-typed-mixins';
export default mixins(
externalHooks,
genericHelpers,
nodeHelpers,
)
@ -323,6 +322,8 @@ export default mixins(
// Update the issues
this.updateNodeCredentialIssues(node);
this.$externalHooks().run('nodeSettings.credentialSelected', { updateInformation });
},
valueChanged (parameterData: IUpdateInformation) {
let newValue: NodeParameterValue;
@ -357,6 +358,7 @@ export default mixins(
// Get only the parameters which are different to the defaults
let nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false);
const oldNodeParameters = Object.assign({}, nodeParameters);
// Copy the data because it is the data of vuex so make sure that
// we do not edit it directly
@ -404,8 +406,11 @@ export default mixins(
name: node.name,
value: nodeParameters,
};
this.$store.commit('setNodeParameters', updateInformation);
this.$externalHooks().run('nodeSettings.valueChanged', { parameterPath, newValue, parameters: this.parameters, oldNodeParameters });
this.updateNodeParameterIssues(node, nodeType);
this.updateNodeCredentialIssues(node);
} else {

View file

@ -233,7 +233,7 @@ export default mixins(
// Get the resolved parameter values of the current node
const currentNodeParameters = this.$store.getters.activeNode.parameters;
const resolvedNodeParameters = this.getResolveNodeParameters(currentNodeParameters);
const resolvedNodeParameters = this.resolveParameter(currentNodeParameters);
const returnValues: string[] = [];
for (const parameterPath of loadOptionsDependsOn) {
@ -459,21 +459,6 @@ export default mixins(
},
},
methods: {
getResolveNodeParameters (nodeParameters: INodeParameters): INodeParameters {
const returnData: INodeParameters = {};
for (const key of Object.keys(nodeParameters)) {
if (Array.isArray(nodeParameters[key])) {
returnData[key] = (nodeParameters[key] as string[]).map(value => {
return this.resolveExpression(value as string) as string;
});
} else if (typeof nodeParameters[key] === 'object') {
returnData[key] = this.getResolveNodeParameters(nodeParameters[key] as INodeParameters);
} else {
returnData[key] = this.resolveExpression(nodeParameters[key] as string);
}
}
return returnData;
},
async loadRemoteParameterOptions () {
if (this.node === null || this.remoteMethod === undefined || this.remoteParameterOptionsLoading) {
return;
@ -484,10 +469,10 @@ export default mixins(
// Get the resolved parameter values of the current node
const currentNodeParameters = this.$store.getters.activeNode.parameters;
const resolvedNodeParameters = this.getResolveNodeParameters(currentNodeParameters);
const resolvedNodeParameters = this.resolveParameter(currentNodeParameters) as INodeParameters;
try {
const options = await this.restApi().getNodeParameterOptions(this.node.type, this.remoteMethod, resolvedNodeParameters, this.node.credentials);
const options = await this.restApi().getNodeParameterOptions(this.node.type, this.path, this.remoteMethod, resolvedNodeParameters, this.node.credentials);
this.remoteParameterOptions.push.apply(this.remoteParameterOptions, options);
} catch (error) {
this.remoteParameterOptionsLoadingIssues = error.message;

View file

@ -76,10 +76,11 @@
</template>
<script lang="ts">
import Vue from 'vue';
import {
INodeParameters,
INodeProperties,
NodeParameterValue,
} from 'n8n-workflow';
import { IUpdateInformation } from '@/Interface';
@ -88,9 +89,10 @@ import MultipleParameter from '@/components/MultipleParameter.vue';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { translate } from '@/components/mixins/translate';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import ParameterInputFull from '@/components/ParameterInputFull.vue';
import { get } from 'lodash';
import { get, set } from 'lodash';
import mixins from 'vue-typed-mixins';
@ -98,6 +100,7 @@ export default mixins(
genericHelpers,
nodeHelpers,
translate,
workflowHelpers,
)
.extend({
name: 'ParameterInputList',
@ -112,9 +115,12 @@ export default mixins(
'hideDelete', // boolean
],
computed: {
filteredParameters (): INodeProperties {
filteredParameters (): INodeProperties[] {
return this.parameters.filter((parameter: INodeProperties) => this.displayNodeParameter(parameter));
},
filteredParameterNames (): string[] {
return this.filteredParameters.map(parameter => parameter.name);
},
},
methods: {
multipleValues (parameter: INodeProperties): boolean {
@ -159,12 +165,75 @@ export default mixins(
// If it is not defined no need to do a proper check
return true;
}
const nodeValues: INodeParameters = {};
let rawValues = this.nodeValues;
if (this.path) {
rawValues = get(this.nodeValues, this.path);
}
// Resolve expressions
const resolveKeys = Object.keys(rawValues);
let key: string;
let i = 0;
let parameterGotResolved = false;
do {
key = resolveKeys.shift() as string;
if (typeof rawValues[key] === 'string' && rawValues[key].charAt(0) === '=') {
// Contains an expression that
if (rawValues[key].includes('$parameter') && resolveKeys.some(parameterName => rawValues[key].includes(parameterName))) {
// Contains probably an expression of a missing parameter so skip
resolveKeys.push(key);
continue;
} else {
// Contains probably no expression with a missing parameter so resolve
nodeValues[key] = this.resolveExpression(rawValues[key], nodeValues) as NodeParameterValue;
parameterGotResolved = true;
}
} else {
// Does not contain an expression, add directly
nodeValues[key] = rawValues[key];
}
// TODO: Think about how to calculate this best
if (i++ > 50) {
// Make sure we do not get caught
break;
}
} while(resolveKeys.length !== 0);
if (parameterGotResolved === true) {
if (this.path) {
rawValues = JSON.parse(JSON.stringify(this.nodeValues));
set(rawValues, this.path, nodeValues);
return this.displayParameter(rawValues, parameter, this.path);
} else {
return this.displayParameter(nodeValues, parameter, '');
}
}
return this.displayParameter(this.nodeValues, parameter, this.path);
},
valueChanged (parameterData: IUpdateInformation): void {
this.$emit('valueChanged', parameterData);
},
},
watch: {
filteredParameterNames(newValue, oldValue) {
// After a parameter does not get displayed anymore make sure that its value gets removed
// Is only needed for the edge-case when a parameter gets displayed depending on another field
// which contains an expression.
for (const parameter of oldValue) {
if (!newValue.includes(parameter)) {
const parameterData = {
name: `${this.path}.${parameter}`,
node: this.$store.getters.activeNode.name,
value: undefined,
};
this.$emit('valueChanged', parameterData);
}
}
},
},
beforeCreate: function () { // tslint:disable-line
// Because we have a circular dependency on CollectionParameter import it here
// to not break Vue.

View file

@ -5,7 +5,7 @@
<el-button
v-if="node && !isReadOnly"
:disabled="workflowRunning"
@click.stop="runWorkflow(node.name)"
@click.stop="runWorkflow(node.name, 'RunData.ExecuteNodeButton')"
class="execute-node-button"
:title="`Executes this ${node.name} node after executing any previous nodes that have not yet returned data`"
>
@ -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,8 +225,10 @@ import {
} from '@/constants';
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
import NodeErrorView from '@/components/Error/NodeViewError.vue';
import { copyPaste } from '@/components/mixins/copyPaste';
import { externalHooks } from "@/components/mixins/externalHooks";
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { workflowRun } from '@/components/mixins/workflowRun';
@ -239,6 +240,7 @@ const deselectedPlaceholder = '_!^&*';
export default mixins(
copyPaste,
externalHooks,
genericHelpers,
nodeHelpers,
workflowRun,
@ -247,6 +249,7 @@ export default mixins(
name: 'RunData',
components: {
BinaryDataDisplay,
NodeErrorView,
VueJsonPretty,
},
data () {
@ -616,8 +619,9 @@ export default mixins(
jsonData () {
this.refreshDataSize();
},
displayMode () {
displayMode (newValue, oldValue) {
this.closeBinaryDataDisplay();
this.$externalHooks().run('runData.displayModeChanged', { newValue, oldValue });
},
maxRunIndex () {
this.runIndex = Math.min(this.runIndex, this.maxRunIndex);
@ -739,13 +743,6 @@ export default mixins(
}
}
.error-display {
.error-message {
color: #ff0000;
font-weight: bold;
}
}
table {
border-collapse: collapse;
text-align: left;

View file

@ -379,7 +379,7 @@ export default mixins(
return returnData;
}
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, nodeName, connectionInputData, 'manual');
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, nodeName, connectionInputData, {}, 'manual');
const proxy = dataProxy.getDataProxy();
// @ts-ignore

View file

@ -167,6 +167,7 @@
<script lang="ts">
import Vue from 'vue';
import { externalHooks } from '@/components/mixins/externalHooks';
import { restApi } from '@/components/mixins/restApi';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { showMessage } from '@/components/mixins/showMessage';
@ -180,6 +181,7 @@ import {
import mixins from 'vue-typed-mixins';
export default mixins(
externalHooks,
genericHelpers,
restApi,
showMessage,
@ -225,6 +227,7 @@ export default mixins(
if (newValue) {
this.openDialog();
}
this.$externalHooks().run('workflowSettings.dialogVisibleChanged', { dialogVisible: newValue });
},
},
methods: {
@ -456,6 +459,8 @@ export default mixins(
}
}
const oldSettings = JSON.parse(JSON.stringify(this.$store.getters.workflowSettings));
this.$store.commit('setWorkflowSettings', localWorkflowSettings);
this.isLoading = false;
@ -467,6 +472,8 @@ export default mixins(
});
this.closeDialog();
this.$externalHooks().run('workflowSettings.saveSettings', { oldSettings });
},
toggleTimeout() {
this.workflowSettings.executionTimeout = this.workflowSettings.executionTimeout === -1 ? 0 : -1;

View file

@ -52,7 +52,7 @@ export const genericHelpers = mixins(showMessage).extend({
return true;
},
startLoading () {
startLoading (text?: string) {
if (this.loadingService !== null) {
return;
}
@ -60,7 +60,7 @@ export const genericHelpers = mixins(showMessage).extend({
this.loadingService = this.$loading(
{
lock: true,
text: 'Loading',
text: text || 'Loading',
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.8)',
},

View file

@ -13,6 +13,8 @@ import {
IRunData,
IRunExecutionData,
ITaskDataConnections,
INode,
INodePropertyOptions,
} from 'n8n-workflow';
import {
@ -321,5 +323,43 @@ export const nodeHelpers = mixins(
this.updateNodeCredentialIssues(node);
}
},
// @ts-ignore
getNodeSubtitle (data, nodeType, workflow): string | undefined {
if (data.notesInFlow) {
return data.notes;
}
if (nodeType !== null && nodeType.subtitle !== undefined) {
return workflow.expression.getSimpleParameterValue(data as INode, nodeType.subtitle, 'internal') as string | undefined;
}
if (data.parameters.operation !== undefined) {
const operation = data.parameters.operation as string;
if (nodeType === null) {
return operation;
}
const operationData:INodeProperties = nodeType.properties.find((property: INodeProperties) => {
return property.name === 'operation';
});
if (operationData === undefined) {
return operation;
}
if (operationData.options === undefined) {
return operation;
}
const optionData = operationData.options.find((option) => {
return (option as INodePropertyOptions).value === data.parameters.operation;
});
if (optionData === undefined) {
return operation;
}
return optionData.name;
}
return undefined;
},
},
});

View file

@ -8,6 +8,7 @@ import {
IPushDataTestWebhook,
} from '../../Interface';
import { externalHooks } from '@/components/mixins/externalHooks';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { showMessage } from '@/components/mixins/showMessage';
import { titleChange } from '@/components/mixins/titleChange';
@ -15,6 +16,7 @@ import { titleChange } from '@/components/mixins/titleChange';
import mixins from 'vue-typed-mixins';
export const pushConnection = mixins(
externalHooks,
nodeHelpers,
showMessage,
titleChange,
@ -202,14 +204,29 @@ export const pushConnection = mixins(
const runDataExecuted = pushData.data;
let runDataExecutedErrorMessage;
// @ts-ignore
const workflow = this.getWorkflow();
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>`;
}
runDataExecutedErrorMessage = errorMessage;
this.$titleSet(workflow.name, 'ERROR');
this.$showMessage({
title: 'Problem executing workflow',
@ -238,6 +255,20 @@ export const pushConnection = mixins(
// Set the node execution issues on all the nodes which produced an error so that
// it can be displayed in the node-view
this.updateNodesExecutionIssues();
let itemsCount = 0;
if(runDataExecuted.data.resultData.lastNodeExecuted && !runDataExecutedErrorMessage) {
itemsCount = runDataExecuted.data.resultData.runData[runDataExecuted.data.resultData.lastNodeExecuted][0].data!.main[0]!.length;
}
this.$externalHooks().run('pushConnection.executionFinished', {
itemsCount,
nodeName: runDataExecuted.data.resultData.lastNodeExecuted,
errorMessage: runDataExecutedErrorMessage,
runDataExecutedStartData: runDataExecuted.data.startData,
resultDataError: runDataExecuted.data.resultData.error,
});
} else if (receivedData.type === 'executionStarted') {
const pushData = receivedData.data as IPushDataExecutionStarted;

View file

@ -157,9 +157,10 @@ export const restApi = Vue.extend({
},
// Returns all the parameter options from the server
getNodeParameterOptions: (nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => {
getNodeParameterOptions: (nodeType: string, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => {
const sendData = {
nodeType,
path,
methodName,
credentials,
currentNodeParameters,

View file

@ -2,9 +2,13 @@ import Vue from 'vue';
import { Notification } from 'element-ui';
import { ElNotificationOptions } from 'element-ui/types/notification';
import mixins from 'vue-typed-mixins';
import { externalHooks } from '@/components/mixins/externalHooks';
// export const showMessage = {
export const showMessage = Vue.extend({
export const showMessage = mixins(externalHooks).extend({
methods: {
$showMessage (messageData: ElNotificationOptions) {
messageData.dangerouslyUseHTMLString = true;
@ -21,6 +25,7 @@ export const showMessage = Vue.extend({
type: 'error',
duration: 0,
});
this.$externalHooks().run('showMessage.showError', { title, message, errorMessage: error.message });
},
},
});

View file

@ -2,9 +2,12 @@ import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import {
IConnections,
IDataObject,
INode,
INodeExecutionData,
INodeIssues,
INodeParameters,
NodeParameterValue,
INodeType,
INodeTypes,
INodeTypeData,
@ -335,8 +338,8 @@ export const workflowHelpers = mixins(
return nodeData;
},
// Executes the given expression and returns its value
resolveExpression (expression: string) {
resolveParameter(parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) {
const inputIndex = 0;
const itemIndex = 0;
const runIndex = 0;
@ -362,7 +365,22 @@ export const workflowHelpers = mixins(
connectionInputData = [];
}
return workflow.expression.getParameterValue(expression, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, 'manual', true);
return workflow.expression.getParameterValue(parameter, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, 'manual', false) as IDataObject;
},
resolveExpression(expression: string, siblingParameters: INodeParameters = {}) {
const parameters = {
'__xxxxxxx__': expression,
...siblingParameters,
};
const returnData = this.resolveParameter(parameters) as IDataObject;
if (typeof returnData['__xxxxxxx__'] === 'object') {
const workflow = this.getWorkflow();
return workflow.expression.convertObjectValueToString(returnData['__xxxxxxx__'] as object);
}
return returnData['__xxxxxxx__'];
},
// Saves the currently loaded workflow to the database.

View file

@ -53,7 +53,7 @@ export const workflowRun = mixins(
return response;
},
async runWorkflow (nodeName: string): Promise<IExecutionPushResponse | undefined> {
async runWorkflow (nodeName: string, source?: string): Promise<IExecutionPushResponse | undefined> {
if (this.$store.getters.isActionActive('workflowRunning') === true) {
return;
}
@ -84,7 +84,7 @@ export const workflowRun = mixins(
duration: 0,
});
this.$titleSet(workflow.name as string, 'ERROR');
this.$externalHooks().run('workflow.runError', { errorMessages });
this.$externalHooks().run('workflowRun.runError', { errorMessages, nodeName });
return;
}
}
@ -172,7 +172,11 @@ export const workflowRun = mixins(
};
this.$store.commit('setWorkflowExecutionData', executionData);
return await this.runWorkflowApi(startRunData);
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);
this.$externalHooks().run('workflowRun.runWorkflow', { nodeName, source });
return runWorkflowApiResponse;
} catch (error) {
this.$titleSet(workflow.name as string, 'ERROR');
this.$showError(error, 'Problem running workflow', 'There was a problem running the workflow:');

View file

@ -58,6 +58,7 @@ import {
faFilePdf,
faFolderOpen,
faHdd,
faHome,
faHourglass,
faImage,
faInbox,
@ -94,6 +95,7 @@ import {
faTrash,
faUndo,
faUsers,
faClock,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
@ -138,6 +140,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);
@ -174,6 +177,7 @@ library.add(faTimes);
library.add(faTrash);
library.add(faUndo);
library.add(faUsers);
library.add(faClock);
Vue.component('font-awesome-icon', FontAwesomeIcon);

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

@ -199,7 +199,7 @@ export default mixins(
this.createNodeActive = false;
},
nodes: {
async handler (val, oldVal) {
async handler (value, oldValue) {
// Load a workflow
let workflowId = null as string | null;
if (this.$route && this.$route.params.name) {
@ -209,7 +209,7 @@ export default mixins(
deep: true,
},
connections: {
async handler (val, oldVal) {
async handler (value, oldValue) {
// Load a workflow
let workflowId = null as string | null;
if (this.$route && this.$route.params.name) {
@ -333,6 +333,7 @@ export default mixins(
},
openNodeCreator () {
this.createNodeActive = true;
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'add_node_button' });
},
async openExecution (executionId: string) {
this.resetWorkspace();
@ -355,6 +356,8 @@ export default mixins(
this.$store.commit('setWorkflowExecutionData', data);
await this.addNodes(JSON.parse(JSON.stringify(data.workflowData.nodes)), JSON.parse(JSON.stringify(data.workflowData.connections)));
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
},
async openWorkflow (workflowId: string) {
this.resetWorkspace();
@ -1053,6 +1056,8 @@ export default mixins(
this.$store.commit('setStateDirty', true);
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
// Automatically deselect all nodes and select the current one and also active
// current node
this.deselectAllNodes();
@ -1175,6 +1180,7 @@ export default mixins(
// Display the node-creator
this.createNodeActive = true;
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'node_connection_drop' });
});
this.instance.bind('connection', (info: OnConnectionBindInfo) => {
@ -1454,6 +1460,8 @@ export default mixins(
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
} else {
this.startLoading('Redirecting');
return;
}
});

View file

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "0.11.0",
"version": "0.13.0",
"description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -47,7 +47,7 @@
"@oclif/dev-cli": "^1.22.2",
"@types/copyfiles": "^2.1.1",
"@types/inquirer": "^6.5.0",
"@types/tmp": "^0.1.0",
"@types/tmp": "^0.2.0",
"@types/vorpal": "^1.11.0",
"tslint": "^6.1.2"
},
@ -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.71.0",
"n8n-workflow": "~0.58.0",
"oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0",
"request": "^2.88.2",

View file

@ -1,9 +1,13 @@
import { ChildProcess, spawn } from 'child_process';
const copyfiles = require('copyfiles');
import {
readFile as fsReadFile,
} from 'fs/promises';
import {
write as fsWrite,
} from 'fs';
import { join } from 'path';
import { file } from 'tmp-promise';
import { promisify } from 'util';
@ -32,7 +36,7 @@ export async function createCustomTsconfig () {
const tsconfigPath = join(__dirname, '../../src/tsconfig-build.json');
// Read the tsconfi file
const tsConfigString = await fsReadFileAsync(tsconfigPath, { encoding: 'utf8'}) as string;
const tsConfigString = await fsReadFile(tsconfigPath, { encoding: 'utf8'}) as string;
const tsConfig = JSON.parse(tsConfigString);
// Set absolute include paths

View file

@ -20,13 +20,51 @@ export class ERPNextApi implements ICredentialType {
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Environment',
name: 'environment',
type: 'options' as NodePropertyTypes,
default: 'cloudHosted',
options: [
{
name: 'Cloud-hosted',
value: 'cloudHosted',
},
{
name: 'Self-hosted',
value: 'selfHosted',
},
],
},
{
displayName: 'Subdomain',
name: 'subdomain',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'n8n',
description: 'ERPNext subdomain. For instance, entering n8n will make the url look like: https://n8n.erpnext.com/.',
description: 'Subdomain of cloud-hosted ERPNext instance. For example, "n8n" is the subdomain in: <code>https://n8n.erpnext.com</code>',
displayOptions: {
show: {
environment: [
'cloudHosted',
],
},
},
},
{
displayName: 'Domain',
name: 'domain',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'https://www.mydomain.com',
description: 'Fully qualified domain name of self-hosted ERPNext instance.',
displayOptions: {
show: {
environment: [
'selfHosted',
],
},
},
},
];
}

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 KitemakerApi implements ICredentialType {
name = 'kitemakerApi';
displayName = 'Kitemaker API';
documentationUrl = 'kitemaker';
properties = [
{
displayName: 'Personal Access Token',
name: 'personalAccessToken',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

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

@ -3,15 +3,11 @@ import {
NodePropertyTypes,
} from 'n8n-workflow';
export class Mqtt implements ICredentialType {
name = 'mqtt';
displayName = 'MQTT';
documentationUrl = 'mqtt';
properties = [
// The credentials to get from user and save encrypted.
// Properties can be defined exactly in the same way
// as node properties.
{
displayName: 'Protocol',
name: 'protocol',
@ -55,5 +51,19 @@ export class Mqtt implements ICredentialType {
},
default: '',
},
{
displayName: 'Clean Session',
name: 'clean',
type: 'boolean' as NodePropertyTypes,
default: true,
description: `Set to false to receive QoS 1 and 2 messages while offline.`,
},
{
displayName: 'Client ID',
name: 'clientId',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Client ID. If left empty, one is autogenrated for you',
},
];
}

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

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

View file

@ -0,0 +1,47 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class NotionOAuth2Api implements ICredentialType {
name = 'notionOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Notion OAuth2 API';
documentationUrl = 'notion';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.notion.com/v1/oauth/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.notion.com/v1/oauth/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'header',
},
];
}

View file

@ -20,5 +20,11 @@ export class PaddleApi implements ICredentialType {
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Use Sandbox environment API',
name: 'sandbox',
type: 'boolean' as NodePropertyTypes,
default: false,
},
];
}

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

@ -9,6 +9,22 @@ export class TwilioApi implements ICredentialType {
displayName = 'Twilio API';
documentationUrl = 'twilio';
properties = [
{
displayName: 'Auth Type',
name: 'authType',
type: 'options' as NodePropertyTypes,
default: 'authToken',
options: [
{
name: 'Auth Token',
value: 'authToken',
},
{
name: 'API Key',
value: 'apiKey',
},
],
},
{
displayName: 'Account SID',
name: 'accountSid',
@ -20,6 +36,42 @@ export class TwilioApi implements ICredentialType {
name: 'authToken',
type: 'string' as NodePropertyTypes,
default: '',
displayOptions: {
show: {
authType: [
'authToken',
],
},
},
},
{
displayName: 'API Key SID',
name: 'apiKeySid',
type: 'string' as NodePropertyTypes,
default: '',
displayOptions: {
show: {
authType: [
'apiKey',
],
},
},
},
{
displayName: 'API Key Secret',
name: 'apiKeySecret',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
default: '',
displayOptions: {
show: {
authType: [
'apiKey',
],
},
},
},
];
}

View file

@ -6,7 +6,9 @@ import {
const scopes = [
'attachments:write',
'channels:remove',
'comments:remove',
'messages:remove',
'threads:remove',
'workspaces:read',
];

View file

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

View file

@ -27,5 +27,14 @@ export class WooCommerceApi implements ICredentialType {
default: '',
placeholder: 'https://example.com',
},
{
displayName: 'Include Credentials in Query',
name: 'includeCredentialsInQuery',
type: 'boolean' as NodePropertyTypes,
default: false,
description: `Occasionally, some servers may not parse the Authorization header correctly</br>
(if you see a Consumer key is missing error when authenticating over SSL, you have a server issue).</br>
In this case, you may provide the consumer key/secret as query string parameters instead.`,
},
];
}

View file

@ -53,7 +53,7 @@ export class ZohoOAuth2Api implements ICredentialType {
},
{
name: 'CN - https://accounts.zoho.com.cn/oauth/v2/token',
value: ' https://accounts.zoho.com.cn/oauth/v2/token',
value: 'https://accounts.zoho.com.cn/oauth/v2/token',
},
],
default: 'https://accounts.zoho.com/oauth/v2/token',

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');
}
}

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