1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-09-20 09:27:44 +03:00

🔀 Merge branch 'master' into oauth-support

This commit is contained in:
Jan Oberhauser 2020-04-04 17:34:10 +02:00
commit 9dd9e0d8ba
376 changed files with 45850 additions and 2723 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ google-generated-credentials.json
_START_PACKAGE
.env
.vscode
.idea

View File

@ -2,8 +2,7 @@
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-logo.png)
n8n is a free and open 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) licensed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools.
<a href="https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-screenshot.png"><img src="https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-screenshot.png" width="550" alt="n8n.io - Screenshot"></a>
@ -17,7 +16,7 @@ received or lost a star.
## Available integrations
n8n has 80+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
n8n has 100+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
## Documentation
@ -55,6 +54,15 @@ 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://jobs.n8n.io](https://jobs.n8n.io)
## What does n8n mean and how do you pronounce it
**Short answer:** It means "nodemation"
@ -80,6 +88,6 @@ Have you found a bug :bug: ? Or maybe you have a nice feature :sparkles: to cont
## License
[Apache 2.0 with Commons Clause](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
n8n is [fair-code](http://faircode.io) licensed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
Additional information about license can be found in the [FAQ](https://docs.n8n.io/#/faq?id=license)

View File

@ -1,4 +1,4 @@
FROM node:10.16
FROM node:12.16
ARG N8N_VERSION
@ -6,13 +6,16 @@ RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!"
RUN \
apt-get update && \
apt-get -y install graphicsmagick
apt-get -y install graphicsmagick gosu
# Set a custom user to not have n8n run as root
USER root
RUN npm_config_user=root npm install -g n8n@${N8N_VERSION}
RUN npm_config_user=root npm install -g full-icu n8n@${N8N_VERSION}
ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu
WORKDIR /data
CMD "n8n"
COPY docker-entrypoint.sh /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@ -0,0 +1,15 @@
#!/bin/sh
if [ -d /root/.n8n ] ; then
chmod o+rx /root
chown -R node /root/.n8n
ln -s /root/.n8n /home/node/
fi
if [ "$#" -gt 0 ]; then
# Got started with arguments
exec gosu node "$@"
else
# Got started without arguments
exec gosu node n8n
fi

View File

@ -1,11 +1,11 @@
FROM node:12.13.0-alpine
FROM node:12.16-alpine
ARG N8N_VERSION
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
# Update everything and install needed dependencies
RUN apk add --update graphicsmagick tzdata git
RUN apk add --update graphicsmagick tzdata git tini su-exec
# # Set a custom user to not have n8n run as root
USER root
@ -13,9 +13,12 @@ USER root
# Install n8n and the also temporary all the packages
# it needs to build it correctly.
RUN apk --update add --virtual build-dependencies python build-base ca-certificates && \
npm_config_user=root npm install -g n8n@${N8N_VERSION} && \
npm_config_user=root npm install -g full-icu n8n@${N8N_VERSION} && \
apk del build-dependencies
ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu
WORKDIR /data
CMD ["n8n"]
COPY docker-entrypoint.sh /docker-entrypoint.sh
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]

View File

@ -2,8 +2,7 @@
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-logo.png)
n8n is a free and open 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) licensed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools.
<a href="https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-screenshot.png"><img src="https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-screenshot.png" width="550" alt="n8n.io - Screenshot"></a>
@ -21,6 +20,7 @@ self-hosted, easily extended, and so also used with internal tools.
- [Example Setup with Lets Encrypt](#example-setup-with-lets-encrypt)
- [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)
@ -34,7 +34,7 @@ Slack notification every time a Github repository received or lost a star.
## Available integrations
n8n has 50+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
n8n has 100+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
## Documentation
@ -128,11 +128,11 @@ it can not be used anymore as encrypting it is not possible anymore.
> may be dropped in the future.
Replace the following placeholders with the actual data:
- MONGO_DATABASE
- MONGO_HOST
- MONGO_PORT
- MONGO_USER
- MONGO_PASSWORD
- <MONGO_DATABASE>
- <MONGO_HOST>
- <MONGO_PORT>
- <MONGO_USER>
- <MONGO_PASSWORD>
```
docker run -it --rm \
@ -151,11 +151,12 @@ A full working setup with docker-compose can be found [here](https://github.com/
#### Use with PostgresDB
Replace the following placeholders with the actual data:
- POSTGRES_DATABASE
- POSTGRES_HOST
- POSTGRES_PASSWORD
- POSTGRES_PORT
- POSTGRES_USER
- <POSTGRES_DATABASE>
- <POSTGRES_HOST>
- <POSTGRES_PASSWORD>
- <POSTGRES_PORT>
- <POSTGRES_USER>
- <POSTGRES_SCHEMA>
```
docker run -it --rm \
@ -166,6 +167,7 @@ docker run -it --rm \
-e DB_POSTGRESDB_HOST=<POSTGRES_HOST> \
-e DB_POSTGRESDB_PORT=<POSTGRES_PORT> \
-e DB_POSTGRESDB_USER=<POSTGRES_USER> \
-e DB_POSTGRESDB_SCHEMA=<POSTGRES_SCHEMA> \
-e DB_POSTGRESDB_PASSWORD=<POSTGRES_PASSWORD> \
-v ~/.n8n:/root/.n8n \
n8nio/n8n \
@ -175,6 +177,31 @@ docker run -it --rm \
A full working setup with docker-compose can be found [here](https://github.com/n8n-io/n8n/blob/master/docker/compose/withPostgres/README.md)
#### Use with MySQL
Replace the following placeholders with the actual data:
- <MYSQLDB_DATABASE>
- <MYSQLDB_HOST>
- <MYSQLDB_PASSWORD>
- <MYSQLDB_PORT>
- <MYSQLDB_USER>
```
docker run -it --rm \
--name n8n \
-p 5678:5678 \
-e DB_TYPE=mysqldb \
-e DB_MYSQLDB_DATABASE=<MYSQLDB_DATABASE> \
-e DB_MYSQLDB_HOST=<MYSQLDB_HOST> \
-e DB_MYSQLDB_PORT=<MYSQLDB_PORT> \
-e DB_MYSQLDB_USER=<MYSQLDB_USER> \
-e DB_MYSQLDB_PASSWORD=<MYSQLDB_PASSWORD> \
-v ~/.n8n:/root/.n8n \
n8nio/n8n \
n8n start
```
## Passing Sensitive Data via File
To avoid passing sensitive information via environment variables "_FILE" may be
@ -189,6 +216,7 @@ The following environment variables support file input:
- DB_POSTGRESDB_PASSWORD_FILE
- DB_POSTGRESDB_PORT_FILE
- DB_POSTGRESDB_USER_FILE
- DB_POSTGRESDB_SCHEMA_FILE
- N8N_BASIC_AUTH_PASSWORD_FILE
- N8N_BASIC_AUTH_USER_FILE
@ -253,6 +281,17 @@ 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://jobs.n8n.io](https://jobs.n8n.io)
## Upgrading
Before you upgrade to the latest version make sure to check here if there are any breaking changes which concern you:
@ -262,6 +301,6 @@ Before you upgrade to the latest version make sure to check here if there are an
## License
n8n is licensed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
n8n is [fair-code](http://faircode.io) licensed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
Additional information about license can be found in the [FAQ](https://docs.n8n.io/#/faq?id=license)

View File

@ -0,0 +1,15 @@
#!/bin/sh
if [ -d /root/.n8n ] ; then
chmod o+rx /root
chown -R node /root/.n8n
ln -s /root/.n8n /home/node/
fi
if [ "$#" -gt 0 ]; then
# Got started with arguments
exec su-exec node "$@"
else
# Got started without arguments
exec su-exec node n8n
fi

View File

@ -1,6 +1,6 @@
# n8n Documentation
This is the documentation of n8n a free and open node-based Workflow Automation Tool.
This is the documentation of n8n a free and open [fair-code](http://faircode.io) licensed node-based Workflow Automation Tool.
It covers everything from setup, usage to development. It is still work in progress and all contributions are welcome.

View File

@ -38,4 +38,5 @@
- Links
- [![Jobs](https://n8n.io/favicon.ico ':size=16')Jobs](https://jobs.n8n.io)
- [![Website](https://n8n.io/favicon.ico ':size=16')n8n.io](https://n8n.io)

View File

@ -77,6 +77,20 @@ These settings can also be overwritten on a per workflow basis in the workflow
settings in the Editor UI.
## Execute In Same Process
All workflows get executed in their own separate process. This ensures that all CPU cores
get used and that they do not block each other on CPU intensive tasks. Additionally does
the crash of one execution not take down the whole application. The disadvantage is, however,
that it slows down the start-time considerably and uses much more memory. So in case, the
workflows are not CPU intensive and they have to start very fast it is possible to run them
all directly in the main-process with this setting.
```bash
export EXECUTIONS_PROCESS=main
```
## Exclude Nodes
It is possible to not allow users to use nodes of a specific node type. If you, for example,
@ -123,6 +137,19 @@ export NODE_FUNCTION_ALLOW_EXTERNAL=moment,lodash
```
## SSL
It is possible to start n8n with SSL enabled by supplying a certificate to use:
```bash
export N8N_PROTOCOL=https
export N8N_SSL_KEY=/data/certs/server.key
export N8N_SSL_CERT=/data/certs/server.pem
```
## Timezone
The timezone is set by default to "America/New_York". It gets for example used by the
@ -163,3 +190,52 @@ webhook URLs get registred with external services.
```bash
export WEBHOOK_TUNNEL_URL="https://n8n.example.com/"
```
## Configuration via file
It is also possible to configure n8n via a configuration file.
It is not necessary to define all values. Only the ones which should be
different from the defaults.
If needed also multiple files can be supplied to for example have generic
base settings and some specific ones depending on the environment.
The path to the JSON configuration file to use can be set via the environment
variable `N8N_CONFIG_FILES`.
```bash
# Single file
export N8N_CONFIG_FILES=/folder/my-config.json
# Multiple files can be comma-separated
export N8N_CONFIG_FILES=/folder/my-config.json,/folder/production.json
```
A possible configuration file could look like this:
```json
{
"executions": {
"process": "main",
"saveDataOnSuccess": "none"
},
"generic": {
"timezone": "Europe/Berlin"
},
"security": {
"basicAuth": {
"active": true,
"user": "frank",
"password": "some-secure-password"
}
},
"nodes": {
"exclude": "[\"n8n-nodes-base.executeCommand\",\"n8n-nodes-base.writeBinaryFile\"]"
}
}
```
All possible values which can be set and their defaults can be found here:
[https://github.com/n8n-io/n8n/blob/master/packages/cli/config/index.ts](https://github.com/n8n-io/n8n/blob/master/packages/cli/config/index.ts)

View File

@ -4,6 +4,13 @@ By default, n8n uses SQLite to save credentials, past executions, and workflows.
n8n however also supports MongoDB and PostgresDB.
## Shared Settings
The following environment variables get used by all databases:
- `DB_TABLE_PREFIX` (default: '') - Prefix for table names
## MongoDB
!> **WARNING**: Use Postgres if possible! Mongo has problems with saving large
@ -38,6 +45,7 @@ To use PostgresDB as database you can provide the following environment variable
- `DB_POSTGRESDB_PORT` (default: 5432)
- `DB_POSTGRESDB_USER` (default: 'root')
- `DB_POSTGRESDB_PASSWORD` (default: empty)
- `DB_POSTGRESDB_SCHEMA` (default: 'public')
```bash
@ -47,6 +55,31 @@ export DB_POSTGRESDB_HOST=postgresdb
export DB_POSTGRESDB_PORT=5432
export DB_POSTGRESDB_USER=n8n
export DB_POSTGRESDB_PASSWORD=n8n
export DB_POSTGRESDB_SCHEMA=n8n
n8n start
```
## MySQL
The compatibility with MySQL was tested, even so, it is advisable to observe the operation of the application with this DB, as it is a new option, recently added. If you spot any problems, feel free to submit a PR.
To use MySQL as database you can provide the following environment variables:
- `DB_TYPE=mysqldb`
- `DB_MYSQLDB_DATABASE` (default: 'n8n')
- `DB_MYSQLDB_HOST` (default: 'localhost')
- `DB_MYSQLDB_PORT` (default: 3306)
- `DB_MYSQLDB_USER` (default: 'root')
- `DB_MYSQLDB_PASSWORD` (default: empty)
```bash
export DB_TYPE=mysqldb
export DB_MYSQLDB_DATABASE=n8n
export DB_MYSQLDB_HOST=mysqldb
export DB_MYSQLDB_PORT=3306
export DB_MYSQLDB_USER=n8n
export DB_MYSQLDB_PASSWORD=n8n
n8n start
```
@ -68,7 +101,6 @@ should not be too much work:
- CockroachDB
- MariaDB
- Microsoft SQL
- MySQL
- Oracle
If you can not use any of the currently supported databases for some reason and

View File

@ -27,31 +27,13 @@ Information about that can be found in the [CONTRIBUTING guide](https://github.c
### What license does n8n use?
n8n is licensed under [Apache 2.0 with Commons Clause](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
n8n is [fair-code](http://faircode.io) licensed under [Apache 2.0 with Commons Clause](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
### Why does n8n has the Commons Clause attached to the license?
I love Open Source and the idea that everybody can use and extend what I wrote for free. But as much
as money can not buy you love, love can sadly literally not buy you anything. Especially does it not pay for rent, food, health insurance, and so on.
And even though people can theoretically contribute to a project are the main drivers which push a project
forward the most time very few and normally the creators or the company behind the project. So to make sure that
the project improves and stays alive long term did the Commons Clause get attached. That makes sure that
no other person/company can make money directly with n8n. Especially not in the way it is planned
to finance further development. For 99.99% of the people it will not make any difference at all but at
the same time does it protect the project.
As n8n itself depends on and uses a lot of other Open Source projects it is only fair and in our interest
to also help them. So it is planed to contribute a certain percentage of revenue/profit every month to these
projects. How much exactly is not decided yet.
Started already with the first very small monthly contributions via [Open Collective](https://opencollective.com/n8n). It is not much yet as revenue is zero and profit in minus but it is at least a start. I hope to be able to ramp that up substantially over time.
### Is n8n really Open Source?
### Is n8n open-source?
No, according to the definition of the [Open Source Initiative (OSI)](https://opensource.org/osd)
is n8n currently not Open Source. The reason is that [Commons Clause](https://commonsclause.com) which takes away some rights got attached to the Apache 2.0 license.
is n8n not open-source. The reason is that [Commons Clause](https://commonsclause.com) which takes away some rights got attached to the Apache 2.0 license.
The source code is however open and people and companies can use it totally free.
What is however not allowed is to make money directly with n8n. So you can for example not charge
other people to host or support n8n.
@ -61,20 +43,17 @@ The support part is mainly there because it was already in the license and I am
If you have bigger things planned simply write an email to [license@n8n.io](mailto:license@n8n.io).
### Why do you call n8n Open Source if the Open Source Initiative (OSI) says it is not?
### Why is n8n not open-source but [fair-code](http://faircode.io) licensed instead?
Because it is the best description and people know what Open Source is. It explains the best and fastest
what can be done with the license n8n uses.
I love open-source and the idea that everybody can use and extend what I wrote for free. But as much
as money can not buy you love, love can sadly literally not buy you anything. Especially does it not pay for rent, food, health insurance, and so on.
And even though people can theoretically contribute to a project are the main drivers which push a project
forward the most time very few and normally the creators or the company behind the project. So to make sure that the project improves and stays alive long term did the Commons Clause get attached. That makes sure that no other person/company can make money directly with n8n. Especially not in the way it is planned
to finance further development. For 99.99% of the people it will not make any difference at all but at
the same time does it protect the project.
If you ask people what it means when a project is Open Source they will mention things like:
As n8n itself depends on and uses a lot of other open-source projects it is only fair and in our interest
to also help them. So it is planed to contribute a certain percentage of revenue/profit every month to these
projects. How much exactly is not decided yet.
- The source code is open
- Everybody can use it for free
- It can be extended
Those are the things people associate with Open Source and what they care about most. And all of the
above can be done with n8n. So there is currently simply no better term to explain it fast and simple.
It is however also very important to me to not mislead anybody. That is why I try to mention everywhere
directly that the [Commons Clause](https://commonsclause.com) got applied. So that the 0.01% of the people
who care about that difference know about it.
Started already with the first very small monthly contributions via [Open Collective](https://opencollective.com/n8n). It is not much yet as revenue is zero and profit in minus but it is at least a start. I hope to be able to ramp that up substantially over time.

View File

@ -1,5 +1,5 @@
# License
n8n is licensed under [Apache 2.0 with Commons Clause](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
n8n is [fair-code](http://faircode.io) licensed under [Apache 2.0 with Commons Clause](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
Additional information about the license can be found in the [FAQ](faq.md?id=license).
Additional information about the license can be found in the [FAQ](faq.md?id=license) and [fair-code](http://faircode.io).

View File

@ -31,7 +31,7 @@ With the help of expressions, it is possible to set node parameters dynamically
An expression could look like this:
My name is: `{{$node["Webhook"].data["query"]["name"]}}`
My name is: `{{$node["Webhook"].json["query"]["name"]}}`
This one would return "My name is: " and then attach the value that the node with the name "Webhook" outputs and there select the property "query" and its key "name". So if the node would output this data:
@ -49,6 +49,7 @@ The following special variables are available:
- **$binary**: Incoming binary data of a node
- **$data**: Incoming JSON data of a node
- **$evaluateExpression**: Evaluates a string as expression
- **$env**: Environment variables
- **$node**: Data of other nodes (output-data, parameters)
- **$parameters**: Parameters of the current node

View File

@ -75,9 +75,9 @@ Example:
```typescript
// Returns the value of the JSON data property "myNumber" of Node "Set" (first item)
const myNumber = $item(0).$node["Set"].data["myNumber"];
const myNumber = $item(0).$node["Set"].json["myNumber"];
// Like above but data of the 6th item
const myNumber = $item(5).$node["Set"].data["myNumber"];
const myNumber = $item(5).$node["Set"].json["myNumber"];
// Returns the value of the parameter "channel" of Node "Slack".
// If it contains an expression the value will be resolved with the
@ -93,12 +93,28 @@ const channel = $item(9).$node["Slack"].parameter["channel"];
Works exactly like `$item` with the difference that it will always return the data of the first item.
```typescript
const myNumber = $node["Set"].data['myNumber'];
const myNumber = $node["Set"].json['myNumber'];
const channel = $node["Slack"].parameter["channel"];
```
#### Method: evaluateExpression(expression: string, itemIndex: number)
Evaluates a given string as expression.
If no `itemIndex` is provided it uses by default in the Function-Node the data of item 0 and
in the Function Item-Node the data of the current item.
Example:
```javascript
items[0].json.variable1 = evaluateExpression('{{1+2}}');
items[0].json.variable2 = evaluateExpression($node["Set"].json["myExpression"], 1);
return items;
```
#### Method: getWorkflowStaticData(type)
Gives access to the static workflow data.
@ -114,7 +130,7 @@ same in the whole workflow. And every node in the workflow can access it. The no
Example:
```typescript
```javascript
// Get the global workflow static data
const staticData = getWorkflowStaticData('global');
// Get the static data of the node

View File

@ -13,5 +13,6 @@ The following environment variables support file input:
- DB_POSTGRESDB_PASSWORD_FILE
- DB_POSTGRESDB_PORT_FILE
- DB_POSTGRESDB_USER_FILE
- DB_POSTGRESDB_SCHEMA_FILE
- N8N_BASIC_AUTH_PASSWORD_FILE
- N8N_BASIC_AUTH_USER_FILE

View File

@ -37,6 +37,7 @@ The "Error Trigger" node will trigger in case the execution fails and receives i
{
"execution": {
"id": "231",
"url": "https://n8n.example.com/execution/231",
"retryOf": "34",
"error": {
"message": "Example Error Message",
@ -56,6 +57,7 @@ The "Error Trigger" node will trigger in case the execution fails and receives i
All information is always present except:
- **execution.id**: Only present when the execution gets saved in the Database
- **execution.url**: Only present when the execution gets saved in the Database
- **execution.retryOf**: Only present when the execution is a retry of a previously failed one

View File

@ -30,6 +30,27 @@ it has to get changed to:
```
## 0.52.0
### What changed?
To make sure that all nodes work similarly, to allow to easily use the value
from other parts of the workflow and to be able to construct the source-date
manually in an expression, the node had to be changed. Instead of getting the
source-date directly from the flow the value has now to be manually set via
an expression.
### When is action necessary?
If you currently use "Date & Time"-Nodes.
### How to upgrade:
Open the "Date & Time"-Nodes and reference the date that should be converted
via an expression. Also, set the "Property Name" to the name of the property the
converted date should be set on.
## 0.37.0
### What changed?

View File

@ -2,8 +2,7 @@
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-logo.png)
n8n is a free and open 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) licensed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools.
<a href="https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-screenshot.png"><img src="https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-screenshot.png" width="550" alt="n8n.io - Screenshot"></a>
@ -18,6 +17,7 @@ self-hosted, easily extended, and so also used with internal tools.
- [Hosted n8n](#hosted-n8n)
- [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)
@ -32,7 +32,7 @@ Slack notification every time a Github repository received or lost a star.
## Available integrations
n8n has 80+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
n8n has 100+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
## Documentation
@ -84,6 +84,15 @@ 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://jobs.n8n.io](https://jobs.n8n.io)
## Upgrading
Before you upgrade to the latest version make sure to check here if there are any breaking changes which concern you:
@ -92,7 +101,7 @@ Before you upgrade to the latest version make sure to check here if there are an
## License
[Apache 2.0 with Commons Clause](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
n8n is [fair-code](http://faircode.io) licensed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
Additional information about license can be found in the [FAQ](https://docs.n8n.io/#/faq?id=license)

View File

@ -2,7 +2,10 @@ import { promises as fs } from 'fs';
import { Command, flags } from '@oclif/command';
import {
UserSettings,
} from "n8n-core";
} from 'n8n-core';
import {
INode,
} from 'n8n-workflow';
import {
ActiveExecutions,
@ -116,14 +119,15 @@ export class Execute extends Command {
// Check if the workflow contains the required "Start" node
// "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue
const requiredNodeTypes = ['n8n-nodes-base.start'];
let startNodeFound = false;
let startNode: INode | undefined= undefined;
for (const node of workflowData!.nodes) {
if (requiredNodeTypes.includes(node.type)) {
startNodeFound = true;
startNode = node;
break;
}
}
if (startNodeFound === false) {
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.`);
@ -136,6 +140,7 @@ export class Execute extends Command {
const runData: IWorkflowExecutionDataProcess = {
credentials,
executionMode: 'cli',
startNodes: [startNode.name],
workflowData: workflowData!,
};

View File

@ -2,7 +2,7 @@ import * as localtunnel from 'localtunnel';
import {
TUNNEL_SUBDOMAIN_ENV,
UserSettings,
} from "n8n-core";
} from 'n8n-core';
import { Command, flags } from '@oclif/command';
const open = require('open');
// import { dirname } from 'path';
@ -21,10 +21,6 @@ import {
} from "../src";
// // Add support for internationalization
// const fullIcuPath = require.resolve('full-icu');
// process.env.NODE_ICU_DATA = dirname(fullIcuPath);
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
let processExistCode = 0;
@ -181,7 +177,7 @@ export class Start extends Command {
Start.openBrowser();
}
this.log(`\nPress "o" to open in Browser.`);
process.stdin.on("data", (key: string) => {
process.stdin.on("data", (key) => {
if (key === 'o') {
Start.openBrowser();
inputText = '';

View File

@ -8,7 +8,7 @@ const config = convict({
database: {
type: {
doc: 'Type of database to use',
format: ['sqlite', 'mongodb', 'postgresdb'],
format: ['sqlite', 'mongodb', 'mysqldb', 'postgresdb'],
default: 'sqlite',
env: 'DB_TYPE'
},
@ -20,6 +20,12 @@ const config = convict({
env: 'DB_MONGODB_CONNECTION_URL'
}
},
tablePrefix: {
doc: 'Prefix for table names',
format: '*',
default: '',
env: 'DB_TABLE_PREFIX'
},
postgresdb: {
database: {
doc: 'PostgresDB Database',
@ -51,6 +57,44 @@ const config = convict({
default: 'root',
env: 'DB_POSTGRESDB_USER'
},
schema: {
doc: 'PostgresDB Schema',
format: String,
default: 'public',
env: 'DB_POSTGRESDB_SCHEMA'
},
},
mysqldb: {
database: {
doc: 'MySQL Database',
format: String,
default: 'n8n',
env: 'DB_MYSQLDB_DATABASE'
},
host: {
doc: 'MySQL Host',
format: String,
default: 'localhost',
env: 'DB_MYSQLDB_HOST'
},
password: {
doc: 'MySQL Password',
format: String,
default: '',
env: 'DB_MYSQLDB_PASSWORD'
},
port: {
doc: 'MySQL Port',
format: Number,
default: 3306,
env: 'DB_MYSQLDB_PORT'
},
user: {
doc: 'MySQL User',
format: String,
default: 'root',
env: 'DB_MYSQLDB_USER'
},
},
},
@ -68,6 +112,17 @@ const config = convict({
},
executions: {
// By default workflows get always executed in their own process.
// If this option gets set to "main" it will run them in the
// main-process instead.
process: {
doc: 'In what process workflows should be executed',
format: ['main', 'own'],
default: 'own',
env: 'EXECUTIONS_PROCESS'
},
// If a workflow executes all the data gets saved by default. This
// could be a problem when a workflow gets executed a lot and processes
// a lot of data. To not write the database full it is possible to
@ -133,6 +188,18 @@ const config = convict({
env: 'N8N_PROTOCOL',
doc: 'HTTP Protocol via which n8n can be reached'
},
ssl_key: {
format: String,
default: '',
env: 'N8N_SSL_KEY',
doc: 'SSL Key for HTTPS Protocol'
},
ssl_cert: {
format: String,
default: '',
env: 'N8N_SSL_CERT',
doc: 'SSL Cert for HTTPS Protocol'
},
security: {
basicAuth: {
@ -231,6 +298,15 @@ const config = convict({
});
// Overwrite default configuration with settings which got defined in
// optional configuration files
if (process.env.N8N_CONFIG_FILES !== undefined) {
const configFiles = process.env.N8N_CONFIG_FILES.split(',');
console.log(`\nLoading configuration overwrites from:\n - ${configFiles.join('\n - ')}\n`);
config.loadFile(configFiles);
}
config.validate({
allowed: 'strict',
});

View File

@ -9,6 +9,6 @@
"index.ts",
"src"
],
"exec": "npm run build && npm start",
"exec": "npm start",
"ext": "ts"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.44.0",
"version": "0.60.0",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -20,7 +20,7 @@
},
"scripts": {
"build": "tsc",
"dev": "nodemon",
"dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"",
"postpack": "rm -f oclif.manifest.json",
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
"start": "run-script-os",
@ -64,8 +64,10 @@
"@types/open": "^6.1.0",
"@types/parseurl": "^1.3.1",
"@types/request-promise-native": "^1.0.15",
"concurrently": "^5.1.0",
"jest": "^24.9.0",
"nodemon": "^2.0.2",
"p-cancelable": "^2.0.0",
"run-script-os": "^1.0.7",
"ts-jest": "^24.0.2",
"tslint": "^5.17.0",
@ -94,10 +96,11 @@
"localtunnel": "^2.0.0",
"lodash.get": "^4.4.2",
"mongodb": "^3.2.3",
"n8n-core": "~0.20.0",
"n8n-editor-ui": "~0.31.0",
"n8n-nodes-base": "~0.39.0",
"n8n-workflow": "~0.20.0",
"mysql2": "^2.0.1",
"n8n-core": "~0.29.0",
"n8n-editor-ui": "~0.40.0",
"n8n-nodes-base": "~0.55.0",
"n8n-workflow": "~0.26.0",
"open": "^7.0.0",
"pg": "^7.11.0",
"request-promise-native": "^1.0.7",

View File

@ -13,6 +13,7 @@ import {
} from '.';
import { ChildProcess } from 'child_process';
import * as PCancelable from 'p-cancelable';
export class ActiveExecutions {
@ -30,7 +31,7 @@ export class ActiveExecutions {
* @returns {string}
* @memberof ActiveExecutions
*/
add(process: ChildProcess, executionData: IWorkflowExecutionDataProcess): string {
add(executionData: IWorkflowExecutionDataProcess, process?: ChildProcess): string {
const executionId = this.nextId++;
this.activeExecutions[executionId] = {
@ -44,6 +45,22 @@ export class ActiveExecutions {
}
/**
* Attaches an execution
*
* @param {string} executionId
* @param {PCancelable<IRun>} workflowExecution
* @memberof ActiveExecutions
*/
attachWorkflowExecution(executionId: string, workflowExecution: PCancelable<IRun>) {
if (this.activeExecutions[executionId] === undefined) {
throw new Error(`No active execution with id "${executionId}" got found to attach to workflowExecution to!`);
}
this.activeExecutions[executionId].workflowExecution = workflowExecution;
}
/**
* Remove an active execution
*
@ -82,13 +99,20 @@ export class ActiveExecutions {
// In case something goes wrong make sure that promise gets first
// returned that it gets then also resolved correctly.
setTimeout(() => {
if (this.activeExecutions[executionId].process.connected) {
this.activeExecutions[executionId].process.send({
type: 'stopExecution'
});
}
}, 1);
if (this.activeExecutions[executionId].process !== undefined) {
// Workflow is running in subprocess
setTimeout(() => {
if (this.activeExecutions[executionId].process!.connected) {
this.activeExecutions[executionId].process!.send({
type: 'stopExecution'
});
}
}, 1);
} else {
// Workflow is running in current process
this.activeExecutions[executionId].workflowExecution!.cancel('Canceled by user');
}
return this.getPostExecutePromise(executionId);
}

View File

@ -113,26 +113,28 @@ export class ActiveWorkflowRunner {
const webhookData: IWebhookData | undefined = this.activeWebhooks!.get(httpMethod, path);
if (webhookData === undefined) {
// The requested webhook is not registred
throw new ResponseHelper.ResponseError('The requested webhook is not registred.', 404, 404);
// The requested webhook is not registered
throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404);
}
const workflowData = await Db.collections.Workflow!.findOne(webhookData.workflowId);
if (workflowData === undefined) {
throw new ResponseHelper.ResponseError(`Could not find workflow with id "${webhookData.workflowId}"`, 404, 404);
}
const nodeTypes = NodeTypes();
const workflow = new Workflow({ id: webhookData.workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings});
// Get the node which has the webhook defined to know where to start from and to
// get additional data
const workflowStartNode = webhookData.workflow.getNode(webhookData.node);
const workflowStartNode = workflow.getNode(webhookData.node);
if (workflowStartNode === null) {
throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404);
}
const executionMode = 'webhook';
const workflowData = await Db.collections.Workflow!.findOne(webhookData.workflow.id!);
if (workflowData === undefined) {
throw new ResponseHelper.ResponseError(`Could not find workflow with id "${webhookData.workflow.id}"`, 404, 404);
}
return new Promise((resolve, reject) => {
WebhookHelpers.executeWebhook(webhookData, workflowData, workflowStartNode, executionMode, undefined, req, res, (error: Error | null, data: object) => {
const executionMode = 'webhook';
WebhookHelpers.executeWebhook(workflow, webhookData, workflowData, workflowStartNode, executionMode, undefined, req, res, (error: Error | null, data: object) => {
if (error !== null) {
return reject(error);
}
@ -202,7 +204,9 @@ export class ActiveWorkflowRunner {
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData);
for (const webhookData of webhooks) {
await this.activeWebhooks!.add(webhookData, mode);
await this.activeWebhooks!.add(workflow, webhookData, mode);
// Save static data!
await WorkflowHelpers.saveStaticData(workflow);
}
}
@ -214,8 +218,19 @@ export class ActiveWorkflowRunner {
* @returns
* @memberof ActiveWorkflowRunner
*/
removeWorkflowWebhooks(workflowId: string): Promise<boolean> {
return this.activeWebhooks!.removeByWorkflowId(workflowId);
async removeWorkflowWebhooks(workflowId: string): Promise<void> {
const workflowData = await Db.collections.Workflow!.findOne(workflowId);
if (workflowData === undefined) {
throw new Error(`Could not find workflow with id "${workflowId}"`);
}
const nodeTypes = NodeTypes();
const workflow = new Workflow({ id: workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
await this.activeWebhooks!.removeWorkflow(workflow);
// Save the static workflow data if needed
await WorkflowHelpers.saveStaticData(workflow);
}
@ -330,7 +345,7 @@ export class ActiveWorkflowRunner {
throw new Error(`Could not find workflow with id "${workflowId}".`);
}
const nodeTypes = NodeTypes();
workflowInstance = new Workflow(workflowId, workflowData.nodes, workflowData.connections, workflowData.active, nodeTypes, workflowData.staticData, workflowData.settings);
workflowInstance = new Workflow({ id: workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated(['n8n-nodes-base.start']);
if (canBeActivated === false) {
@ -348,7 +363,7 @@ export class ActiveWorkflowRunner {
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, getTriggerFunctions, getPollFunctions);
if (this.activationErrors[workflowId] !== undefined) {
// If there were any activation errors delete them
// If there were activation errors delete them
delete this.activationErrors[workflowId];
}
} catch (error) {
@ -380,16 +395,9 @@ export class ActiveWorkflowRunner {
*/
async remove(workflowId: string): Promise<void> {
if (this.activeWorkflows !== null) {
const workflowData = this.activeWorkflows.get(workflowId);
// Remove all the webhooks of the workflow
await this.removeWorkflowWebhooks(workflowId);
if (workflowData) {
// Save the static workflow data if needed
await WorkflowHelpers.saveStaticData(workflowData.workflow);
}
if (this.activationErrors[workflowId] !== undefined) {
// If there were any activation errors delete them
delete this.activationErrors[workflowId];

View File

@ -18,6 +18,7 @@ import {
MongoDb,
PostgresDb,
SQLite,
MySQLDb,
} from './databases';
export let collections: IDatabaseCollections = {
@ -36,33 +37,58 @@ export async function init(synchronize?: boolean): Promise<IDatabaseCollections>
let connectionOptions: ConnectionOptions;
let dbNotExistError: string | undefined;
if (dbType === 'mongodb') {
entities = MongoDb;
connectionOptions = {
type: 'mongodb',
url: await GenericHelpers.getConfigValue('database.mongodb.connectionUrl') as string,
useNewUrlParser: true,
};
} else if (dbType === 'postgresdb') {
dbNotExistError = 'does not exist';
entities = PostgresDb;
connectionOptions = {
type: 'postgres',
database: await GenericHelpers.getConfigValue('database.postgresdb.database') as string,
host: await GenericHelpers.getConfigValue('database.postgresdb.host') as string,
password: await GenericHelpers.getConfigValue('database.postgresdb.password') as string,
port: await GenericHelpers.getConfigValue('database.postgresdb.port') as number,
username: await GenericHelpers.getConfigValue('database.postgresdb.user') as string,
};
} else if (dbType === 'sqlite') {
dbNotExistError = 'no such table:';
entities = SQLite;
connectionOptions = {
type: 'sqlite',
database: path.join(n8nFolder, 'database.sqlite'),
};
} else {
throw new Error(`The database "${dbType}" is currently not supported!`);
switch (dbType) {
case 'mongodb':
entities = MongoDb;
connectionOptions = {
type: 'mongodb',
entityPrefix: await GenericHelpers.getConfigValue('database.tablePrefix') as string,
url: await GenericHelpers.getConfigValue('database.mongodb.connectionUrl') as string,
useNewUrlParser: true,
};
break;
case 'postgresdb':
dbNotExistError = 'does not exist';
entities = PostgresDb;
connectionOptions = {
type: 'postgres',
entityPrefix: await GenericHelpers.getConfigValue('database.tablePrefix') as string,
database: await GenericHelpers.getConfigValue('database.postgresdb.database') as string,
host: await GenericHelpers.getConfigValue('database.postgresdb.host') as string,
password: await GenericHelpers.getConfigValue('database.postgresdb.password') as string,
port: await GenericHelpers.getConfigValue('database.postgresdb.port') as number,
username: await GenericHelpers.getConfigValue('database.postgresdb.user') as string,
schema: await GenericHelpers.getConfigValue('database.postgresdb.schema') as string,
};
break;
case 'mysqldb':
dbNotExistError = 'does not exist';
entities = MySQLDb;
connectionOptions = {
type: 'mysql',
database: await GenericHelpers.getConfigValue('database.mysqldb.database') as string,
entityPrefix: await GenericHelpers.getConfigValue('database.tablePrefix') as string,
host: await GenericHelpers.getConfigValue('database.mysqldb.host') as string,
password: await GenericHelpers.getConfigValue('database.mysqldb.password') as string,
port: await GenericHelpers.getConfigValue('database.mysqldb.port') as number,
username: await GenericHelpers.getConfigValue('database.mysqldb.user') as string,
};
break;
case 'sqlite':
dbNotExistError = 'no such table:';
entities = SQLite;
connectionOptions = {
type: 'sqlite',
database: path.join(n8nFolder, 'database.sqlite'),
entityPrefix: await GenericHelpers.getConfigValue('database.tablePrefix') as string,
};
break;
default:
throw new Error(`The database "${dbType}" is currently not supported!`);
}
Object.assign(connectionOptions, {

View File

@ -18,6 +18,7 @@ import {
} from 'n8n-core';
import * as PCancelable from 'p-cancelable';
import { ObjectID, Repository } from 'typeorm';
import { ChildProcess } from 'child_process';
@ -90,7 +91,7 @@ export interface ICredentialsDecryptedResponse extends ICredentialsDecryptedDb {
id: string;
}
export type DatabaseType = 'mongodb' | 'postgresdb' | 'sqlite';
export type DatabaseType = 'mongodb' | 'postgresdb' | 'mysqldb' | 'sqlite';
export type SaveExecutionDataType = 'all' | 'none';
export interface IExecutionBase {
@ -185,9 +186,10 @@ export interface IExecutionDeleteFilter {
export interface IExecutingWorkflowData {
executionData: IWorkflowExecutionDataProcess;
process: ChildProcess;
process?: ChildProcess;
startedAt: Date;
postExecutePromises: Array<IDeferredPromise<IRun | undefined>>;
workflowExecution?: PCancelable<IRun>;
}
export interface IN8nConfig {

View File

@ -49,29 +49,26 @@ export class ResponseError extends Error {
export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) {
resp.statusCode = 401;
resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
resp.end(message);
resp.json({code: resp.statusCode, message});
}
export function jwtAuthAuthorizationError(resp: Response, message?: string) {
resp.statusCode = 403;
resp.end(message);
resp.json({code: resp.statusCode, message});
}
export function sendSuccessResponse(res: Response, data: any, raw?: boolean, responseCode?: number) { // tslint:disable-line:no-any
res.setHeader('Content-Type', 'application/json');
if (responseCode !== undefined) {
res.status(responseCode);
}
if (raw === true) {
res.send(JSON.stringify(data));
return;
res.json(data);
} else {
res.send(JSON.stringify({
res.json({
data
}));
});
}
}
@ -103,7 +100,7 @@ export function sendErrorResponse(res: Response, error: ResponseError) {
response.stack = error.stack;
}
res.status(httpStatusCode).send(JSON.stringify(response));
res.status(httpStatusCode).json(response);
}

View File

@ -1,4 +1,7 @@
import * as express from 'express';
import {
readFileSync,
} from 'fs';
import {
dirname as pathDirname,
join as pathJoin,
@ -102,6 +105,10 @@ class App {
push: Push.Push;
versions: IPackageVersions | undefined;
protocol: string;
sslKey: string;
sslCert: string;
constructor() {
this.app = express();
@ -117,6 +124,10 @@ class App {
this.push = Push.getInstance();
this.activeExecutionsInstance = ActiveExecutions.getInstance();
this.protocol = config.get('protocol');
this.sslKey = config.get('ssl_key');
this.sslCert = config.get('ssl_cert');
}
@ -134,10 +145,10 @@ class App {
async config(): Promise<void> {
this.versions = await GenericHelpers.getVersions();
const authIgnoreRegex = new RegExp(`^\/(rest|healthz|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`);
const authIgnoreRegex = new RegExp(`^\/(healthz|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`);
// Check for basic auth credentials if activated
const basicAuthActive = config.get('security.basicAuth.active') as boolean;
const basicAuthActive = config.get('security.basicAuth.active') as boolean;
if (basicAuthActive === true) {
const basicAuthUser = await GenericHelpers.getConfigValue('security.basicAuth.user') as string;
if (basicAuthUser === '') {
@ -236,19 +247,28 @@ class App {
});
// Support application/json type post data
this.app.use(bodyParser.json({ limit: "16mb", verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
}}));
this.app.use(bodyParser.json({
limit: '16mb', verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
}
}));
// Support application/xml type post data
// @ts-ignore
this.app.use(bodyParser.xml({ limit: "16mb", xmlParseOptions: {
this.app.use(bodyParser.xml({ limit: '16mb', xmlParseOptions: {
normalize: true, // Trim whitespace inside text nodes
normalizeTags: true, // Transform tags to lowercase
explicitArray: false // Only put properties in array if length > 1
explicitArray: false, // Only put properties in array if length > 1
} }));
this.app.use(bodyParser.text({
limit: '16mb', verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
}
}));
// Make sure that Vue history mode works properly
this.app.use(history({
rewrites: [
@ -504,7 +524,7 @@ class App {
const credentials = await WorkflowCredentials(workflowData.nodes);
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
const nodeTypes = NodeTypes();
const workflowInstance = new Workflow(workflowData.id, workflowData.nodes, workflowData.connections, false, nodeTypes, undefined, workflowData.settings);
const workflowInstance = new Workflow({ id: workflowData.id, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes, staticData: undefined, settings: workflowData.settings });
const needsWebhook = await this.testWebhooks.needsWebhookData(workflowData, workflowInstance, additionalData, executionMode, sessionId, destinationNode);
if (needsWebhook === true) {
return {
@ -663,6 +683,10 @@ class App {
throw new Error('No encryption key got found to encrypt the credentials!');
}
if (incomingData.name === '') {
throw new Error('Credentials have to have a name set!');
}
// Check if credentials with the same name and type exist already
const findQuery = {
where: {
@ -703,6 +727,10 @@ class App {
const id = req.params.id;
if (incomingData.name === '') {
throw new Error('Credentials have to have a name set!');
}
// Add the date for newly added node access permissions
for (const nodeAccess of incomingData.nodesAccess) {
if (!nodeAccess.date) {
@ -838,6 +866,7 @@ class App {
return returnData;
}));
// ----------------------------------------
// OAuth2-Credential/Auth
// ----------------------------------------
@ -1108,6 +1137,12 @@ class App {
workflowData: fullExecutionData.workflowData,
};
const lastNodeExecuted = data!.executionData!.resultData.lastNodeExecuted as string;
// Remove the old error and the data of the last run of the node that it can be replaced
delete data!.executionData!.resultData.error;
data!.executionData!.resultData.runData[lastNodeExecuted].pop();
if (req.body.loadWorkflow === true) {
// Loads the currently saved workflow to execute instead of the
// one saved at the time of the execution.
@ -1117,6 +1152,18 @@ class App {
if (data.workflowData === undefined) {
throw new Error(`The workflow with the ID "${workflowId}" could not be found and so the data not be loaded for the retry.`);
}
// Replace all of the nodes in the execution stack with the ones of the new workflow
for (const stack of data!.executionData!.executionData!.nodeExecutionStack) {
// Find the data of the last executed node in the new workflow
const node = data.workflowData.nodes.find(node => node.name === stack.node.name);
if (node === undefined) {
throw new Error(`Could not find the node "${stack.node.name}" in workflow. It probably got deleted or renamed. Without it the workflow can sadly not be retried.`);
}
// Replace the node data in the stack that it really uses the current data
stack.node = node;
}
}
const workflowRunner = new WorkflowRunner();
@ -1372,7 +1419,20 @@ export async function start(): Promise<void> {
await app.config();
app.app.listen(PORT, async () => {
let server;
if (app.protocol === 'https' && app.sslKey && app.sslCert){
const https = require('https');
const privateKey = readFileSync(app.sslKey, 'utf8');
const cert = readFileSync(app.sslCert, 'utf8');
const credentials = { key: privateKey,cert };
server = https.createServer(credentials,app.app);
}else{
const http = require('http');
server = http.createServer(app.app);
}
server.listen(PORT, async () => {
const versions = await GenericHelpers.getVersions();
console.log(`n8n ready on port ${PORT}`);
console.log(`Version: ${versions.cli}`);

View File

@ -2,10 +2,12 @@ import * as express from 'express';
import {
IResponseCallbackData,
IWorkflowDb,
NodeTypes,
Push,
ResponseHelper,
WebhookHelpers,
IWorkflowDb,
WorkflowHelpers,
} from './';
import {
@ -56,24 +58,28 @@ export class TestWebhooks {
const webhookData: IWebhookData | undefined = this.activeWebhooks!.get(httpMethod, path);
if (webhookData === undefined) {
// The requested webhook is not registred
throw new ResponseHelper.ResponseError('The requested webhook is not registred.', 404, 404);
}
// Get the node which has the webhook defined to know where to start from and to
// get additional data
const workflowStartNode = webhookData.workflow.getNode(webhookData.node);
if (workflowStartNode === null) {
throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404);
// The requested webhook is not registered
throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404);
}
const webhookKey = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path);
const workflowData = this.testWebhookData[webhookKey].workflowData;
const nodeTypes = NodeTypes();
const workflow = new Workflow({ id: webhookData.workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings});
// Get the node which has the webhook defined to know where to start from and to
// get additional data
const workflowStartNode = workflow.getNode(webhookData.node);
if (workflowStartNode === null) {
throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404);
}
return new Promise(async (resolve, reject) => {
try {
const executionMode = 'manual';
const executionId = await WebhookHelpers.executeWebhook(webhookData, this.testWebhookData[webhookKey].workflowData, workflowStartNode, executionMode, this.testWebhookData[webhookKey].sessionId, request, response, (error: Error | null, data: IResponseCallbackData) => {
const executionId = await WebhookHelpers.executeWebhook(workflow, webhookData, this.testWebhookData[webhookKey].workflowData, workflowStartNode, executionMode, this.testWebhookData[webhookKey].sessionId, request, response, (error: Error | null, data: IResponseCallbackData) => {
if (error !== null) {
return reject(error);
}
@ -90,7 +96,7 @@ export class TestWebhooks {
// Inform editor-ui that webhook got received
if (this.testWebhookData[webhookKey].sessionId !== undefined) {
const pushInstance = Push.getInstance();
pushInstance.send('testWebhookReceived', { workflowId: webhookData.workflow.id, executionId }, this.testWebhookData[webhookKey].sessionId!);
pushInstance.send('testWebhookReceived', { workflowId: webhookData.workflowId, executionId }, this.testWebhookData[webhookKey].sessionId!);
}
} catch (error) {
@ -100,7 +106,7 @@ export class TestWebhooks {
// Remove the webhook
clearTimeout(this.testWebhookData[webhookKey].timeout);
delete this.testWebhookData[webhookKey];
this.activeWebhooks!.removeByWorkflowId(webhookData.workflow.id!.toString());
this.activeWebhooks!.removeWorkflow(workflow);
});
}
@ -136,7 +142,10 @@ export class TestWebhooks {
timeout,
workflowData,
};
await this.activeWebhooks!.add(webhookData, mode);
await this.activeWebhooks!.add(workflow, webhookData, mode);
// Save static data!
this.testWebhookData[key].workflowData.staticData = workflow.staticData;
}
return true;
@ -151,6 +160,8 @@ export class TestWebhooks {
* @memberof TestWebhooks
*/
cancelTestWebhook(workflowId: string): boolean {
const nodeTypes = NodeTypes();
let foundWebhook = false;
for (const webhookKey of Object.keys(this.testWebhookData)) {
const webhookData = this.testWebhookData[webhookKey];
@ -173,9 +184,12 @@ export class TestWebhooks {
}
}
const workflowData = webhookData.workflowData;
const workflow = new Workflow({ id: workflowData.id.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
// Remove the webhook
delete this.testWebhookData[webhookKey];
this.activeWebhooks!.removeByWorkflowId(workflowId);
this.activeWebhooks!.removeWorkflow(workflow);
}
return foundWebhook;
@ -190,13 +204,21 @@ export class TestWebhooks {
return;
}
return this.activeWebhooks.removeAll();
const nodeTypes = NodeTypes();
let workflowData: IWorkflowDb;
let workflow: Workflow;
const workflows: Workflow[] = [];
for (const webhookKey of Object.keys(this.testWebhookData)) {
workflowData = this.testWebhookData[webhookKey].workflowData;
workflow = new Workflow({ id: workflowData.id.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
workflows.push(workflow);
}
return this.activeWebhooks.removeAll(workflows);
}
}
let testWebhooksInstance: TestWebhooks | undefined;
export function getInstance(): TestWebhooks {

View File

@ -84,9 +84,9 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
* @param {((error: Error | null, data: IResponseCallbackData) => void)} responseCallback
* @returns {(Promise<string | undefined>)}
*/
export async function executeWebhook(webhookData: IWebhookData, workflowData: IWorkflowDb, workflowStartNode: INode, executionMode: WorkflowExecuteMode, sessionId: string | undefined, req: express.Request, res: express.Response, responseCallback: (error: Error | null, data: IResponseCallbackData) => void): Promise<string | undefined> {
export async function executeWebhook(workflow: Workflow, webhookData: IWebhookData, workflowData: IWorkflowDb, workflowStartNode: INode, executionMode: WorkflowExecuteMode, sessionId: string | undefined, req: express.Request, res: express.Response, responseCallback: (error: Error | null, data: IResponseCallbackData) => void): Promise<string | undefined> {
// Get the nodeType to know which responseMode is set
const nodeType = webhookData.workflow.nodeTypes.getByName(workflowStartNode.type);
const nodeType = workflow.nodeTypes.getByName(workflowStartNode.type);
if (nodeType === undefined) {
const errorMessage = `The type of the webhook node "${workflowStartNode.name}" is not known.`;
responseCallback(new Error(errorMessage), {});
@ -94,8 +94,8 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
}
// Get the responseMode
const responseMode = webhookData.workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], 'onReceived');
const responseCode = webhookData.workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], 200) as number;
const responseMode = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], 'onReceived');
const responseCode = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], 200) as number;
if (!['onReceived', 'lastNode'].includes(responseMode as string)) {
// If the mode is not known we error. Is probably best like that instead of using
@ -122,7 +122,7 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
let webhookResultData: IWebhookResponseData;
try {
webhookResultData = await webhookData.workflow.runWebhook(webhookData, workflowStartNode, additionalData, NodeExecuteFunctions, executionMode);
webhookResultData = await workflow.runWebhook(webhookData, workflowStartNode, additionalData, NodeExecuteFunctions, executionMode);
} catch (e) {
// Send error response to webhook caller
const errorMessage = 'Workflow Webhook Error: Workflow could not be started!';
@ -287,22 +287,28 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
return data;
}
const responseData = webhookData.workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson');
const responseData = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson');
if (didSendResponse === false) {
let data: IDataObject | IDataObject[];
if (responseData === 'firstEntryJson') {
// Return the JSON data of the first entry
if (returnData.data!.main[0]![0] === undefined) {
responseCallback(new Error('No item to return got found.'), {});
didSendResponse = true;
}
data = returnData.data!.main[0]![0].json;
const responsePropertyName = webhookData.workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], undefined);
const responsePropertyName = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], undefined);
if (responsePropertyName !== undefined) {
data = get(data, responsePropertyName as string) as IDataObject;
}
const responseContentType = webhookData.workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], undefined);
const responseContentType = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], undefined);
if (responseContentType !== undefined) {
// Send the webhook response manually to be able to set the content-type
@ -324,12 +330,18 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
} else if (responseData === 'firstEntryBinary') {
// Return the binary data of the first entry
data = returnData.data!.main[0]![0];
if (data === undefined) {
responseCallback(new Error('No item to return got found.'), {});
didSendResponse = true;
}
if (data.binary === undefined) {
responseCallback(new Error('No binary data to return got found.'), {});
didSendResponse = true;
}
const responseBinaryPropertyName = webhookData.workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], 'data');
const responseBinaryPropertyName = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], 'data');
if (responseBinaryPropertyName === undefined && didSendResponse === false) {
responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {});

View File

@ -13,7 +13,7 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
let node, type, name, foundCredentials;
for (node of nodes) {
if (!node.credentials) {
if (node.disabled === true || !node.credentials) {
continue;
}

View File

@ -10,6 +10,7 @@ import {
Push,
ResponseHelper,
WebhookHelpers,
WorkflowCredentials,
WorkflowHelpers,
} from './';
@ -51,10 +52,17 @@ import * as config from '../config';
*/
function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mode: WorkflowExecuteMode, executionId?: string, retryOf?: string): void {
// Check if there was an error and if so if an errorWorkflow is set
let pastExecutionUrl: string | undefined = undefined;
if (executionId !== undefined) {
pastExecutionUrl = `${WebhookHelpers.getWebhookBaseUrl()}execution/${executionId}`;
}
if (fullRunData.data.resultData.error !== undefined && workflowData.settings !== undefined && workflowData.settings.errorWorkflow) {
const workflowErrorData = {
execution: {
id: executionId,
url: pastExecutionUrl,
error: fullRunData.data.resultData.error,
lastNodeExecuted: fullRunData.data.resultData.lastNodeExecuted!,
mode,
@ -297,7 +305,8 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
const nodeTypes = NodeTypes();
const workflow = new Workflow(workflowInfo.id, workflowData!.nodes, workflowData!.connections, workflowData!.active, nodeTypes, workflowData!.staticData);
const workflowName = workflowData ? workflowData.name : undefined;
const workflow = new Workflow({ id: workflowInfo.id, name: workflowName, nodes: workflowData!.nodes, connections: workflowData!.connections, active: workflowData!.active, nodeTypes, staticData: workflowData!.staticData });
// Does not get used so set it simply to empty string
const executionId = '';
@ -307,6 +316,10 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
const additionalDataIntegrated = await getBase(additionalData.credentials);
additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(mode, executionId, workflowData!, { parentProcessMode: additionalData.hooks!.mode });
// Get the needed credentials for the current workflow as they will differ to the ones of the
// calling workflow.
additionalDataIntegrated.credentials = await WorkflowCredentials(workflowData!.nodes);
// Find Start-Node
const requiredNodeTypes = ['n8n-nodes-base.start'];
let startNode: INode | undefined;

View File

@ -90,8 +90,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
const executionMode = 'error';
const nodeTypes = NodeTypes();
const workflowInstance = new Workflow(workflowId, workflowData.nodes, workflowData.connections, workflowData.active, nodeTypes, workflowData.staticData, workflowData.settings);
const workflowInstance = new Workflow({ id: workflowId, name: workflowData.name, nodeTypes, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, staticData: workflowData.staticData, settings: workflowData.settings});
let node: INode;
let workflowStartNode: INode | undefined;

View File

@ -4,6 +4,7 @@ import {
ITransferNodeTypes,
IWorkflowExecutionDataProcess,
IWorkflowExecutionDataProcessWithExecution,
NodeTypes,
Push,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
@ -11,15 +12,19 @@ import {
import {
IProcessMessage,
WorkflowExecute,
} from 'n8n-core';
import {
IExecutionError,
IRun,
Workflow,
WorkflowHooks,
WorkflowExecuteMode,
} from 'n8n-workflow';
import * as config from '../config';
import * as PCancelable from 'p-cancelable';
import { join as pathJoin } from 'path';
import { fork } from 'child_process';
@ -80,7 +85,7 @@ export class WorkflowRunner {
/**
* Run the workflow in subprocess
* Run the workflow
*
* @param {IWorkflowExecutionDataProcess} data
* @param {boolean} [loadStaticData] If set will the static data be loaded from
@ -89,6 +94,70 @@ export class WorkflowRunner {
* @memberof WorkflowRunner
*/
async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
const executionsProcess = config.get('executions.process') as string;
if (executionsProcess === 'main') {
return this.runMainProcess(data, loadStaticData);
}
return this.runSubprocess(data, loadStaticData);
}
/**
* Run the workflow in current process
*
* @param {IWorkflowExecutionDataProcess} data
* @param {boolean} [loadStaticData] If set will the static data be loaded from
* the workflow and added to input data
* @returns {Promise<string>}
* @memberof WorkflowRunner
*/
async runMainProcess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
if (loadStaticData === true && data.workflowData.id) {
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(data.workflowData.id as string);
}
const nodeTypes = NodeTypes();
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);
// Register the active execution
const executionId = this.activeExecutions.add(data, undefined);
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);
let workflowExecution: PCancelable<IRun>;
if (data.executionData !== undefined) {
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) {
// 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 {
// 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);
}
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
return executionId;
}
/**
* Run the workflow
*
* @param {IWorkflowExecutionDataProcess} data
* @param {boolean} [loadStaticData] If set will the static data be loaded from
* the workflow and added to input data
* @returns {Promise<string>}
* @memberof WorkflowRunner
*/
async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
const startedAt = new Date();
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
@ -97,7 +166,7 @@ export class WorkflowRunner {
}
// Register the active execution
const executionId = this.activeExecutions.add(subprocess, data);
const executionId = this.activeExecutions.add(data, subprocess);
// Check if workflow contains a "executeWorkflow" Node as in this
// case we can not know which nodeTypes will be needed and so have

View File

@ -58,7 +58,7 @@ export class WorkflowRunnerProcess {
const nodeTypes = NodeTypes();
await nodeTypes.init(nodeTypesData);
this.workflow = new Workflow(this.data.workflowData.id as string | undefined, this.data.workflowData!.nodes, this.data.workflowData!.connections, this.data.workflowData!.active, nodeTypes, this.data.workflowData!.staticData);
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);
additionalData.hooks = this.getProcessForwardHooks();

View File

@ -1,9 +1,11 @@
import * as MongoDb from './mongodb';
import * as PostgresDb from './postgresdb';
import * as SQLite from './sqlite';
import * as MySQLDb from './mysqldb';
export {
MongoDb,
PostgresDb,
SQLite,
MySQLDb,
};

View File

@ -0,0 +1,44 @@
import {
ICredentialNodeAccess,
} from 'n8n-workflow';
import {
ICredentialsDb,
} from '../../';
import {
Column,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class CredentialsEntity implements ICredentialsDb {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 128
})
name: string;
@Column('text')
data: string;
@Index()
@Column({
length: 32
})
type: string;
@Column('json')
nodesAccess: ICredentialNodeAccess[];
@Column('datetime')
createdAt: Date;
@Column('datetime')
updatedAt: Date;
}

View File

@ -0,0 +1,51 @@
import {
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
IExecutionFlattedDb,
IWorkflowDb,
} from '../../';
import {
Column,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class ExecutionEntity implements IExecutionFlattedDb {
@PrimaryGeneratedColumn()
id: number;
@Column('text')
data: string;
@Column()
finished: boolean;
@Column('varchar')
mode: WorkflowExecuteMode;
@Column({ nullable: true })
retryOf: string;
@Column({ nullable: true })
retrySuccessId: string;
@Column('datetime')
startedAt: Date;
@Column('datetime')
stoppedAt: Date;
@Column('json')
workflowData: IWorkflowDb;
@Index()
@Column({ nullable: true })
workflowId: string;
}

View File

@ -0,0 +1,55 @@
import {
IConnections,
IDataObject,
INode,
IWorkflowSettings,
} from 'n8n-workflow';
import {
IWorkflowDb,
} from '../../';
import {
Column,
Entity,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class WorkflowEntity implements IWorkflowDb {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 128
})
name: string;
@Column()
active: boolean;
@Column('json')
nodes: INode[];
@Column('json')
connections: IConnections;
@Column('datetime')
createdAt: Date;
@Column('datetime')
updatedAt: Date;
@Column({
type: 'json',
nullable: true,
})
settings?: IWorkflowSettings;
@Column({
type: 'json',
nullable: true,
})
staticData?: IDataObject;
}

View File

@ -0,0 +1,3 @@
export * from './CredentialsEntity';
export * from './ExecutionEntity';
export * from './WorkflowEntity';

View File

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.20.0",
"version": "0.29.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -25,6 +25,7 @@
"dist"
],
"devDependencies": {
"@types/cron": "^1.7.1",
"@types/crypto-js": "^3.1.43",
"@types/express": "^4.16.1",
"@types/jest": "^24.0.18",
@ -41,10 +42,11 @@
"dependencies": {
"client-oauth2": "^4.2.5",
"cron": "^1.7.2",
"crypto-js": "^3.1.9-1",
"crypto-js": "3.1.9-1",
"lodash.get": "^4.4.2",
"mmmagic": "^0.5.2",
"n8n-workflow": "~0.20.0",
"n8n-workflow": "~0.26.0",
"p-cancelable": "^2.0.0",
"request-promise-native": "^1.0.7"
},
"jest": {

View File

@ -1,6 +1,7 @@
import {
IWebhookData,
WebhookHttpMethod,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
@ -29,29 +30,26 @@ export class ActiveWebhooks {
* @returns {Promise<void>}
* @memberof ActiveWebhooks
*/
async add(webhookData: IWebhookData, mode: WorkflowExecuteMode): Promise<void> {
if (webhookData.workflow.id === undefined) {
async add(workflow: Workflow, webhookData: IWebhookData, mode: WorkflowExecuteMode): Promise<void> {
if (workflow.id === undefined) {
throw new Error('Webhooks can only be added for saved workflows as an id is needed!');
}
if (this.workflowWebhooks[webhookData.workflow.id] === undefined) {
this.workflowWebhooks[webhookData.workflow.id] = [];
if (this.workflowWebhooks[webhookData.workflowId] === undefined) {
this.workflowWebhooks[webhookData.workflowId] = [];
}
// Make the webhook available directly because sometimes to create it successfully
// it gets called
this.webhookUrls[this.getWebhookKey(webhookData.httpMethod, webhookData.path)] = webhookData;
const webhookExists = await webhookData.workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
if (webhookExists === false) {
// If webhook does not exist yet create it
await webhookData.workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
}
// Run the "activate" hooks on the nodes
await webhookData.workflow.runNodeHooks('activate', webhookData, NodeExecuteFunctions, mode);
this.workflowWebhooks[webhookData.workflow.id].push(webhookData);
this.workflowWebhooks[webhookData.workflowId].push(webhookData);
}
@ -73,6 +71,17 @@ export class ActiveWebhooks {
}
/**
* Returns the ids of all the workflows which have active webhooks
*
* @returns {string[]}
* @memberof ActiveWebhooks
*/
getWorkflowIds(): string[] {
return Object.keys(this.workflowWebhooks);
}
/**
* Returns key to uniquely identify a webhook
*
@ -89,11 +98,13 @@ export class ActiveWebhooks {
/**
* Removes all webhooks of a workflow
*
* @param {string} workflowId
* @param {Workflow} workflow
* @returns {boolean}
* @memberof ActiveWebhooks
*/
async removeByWorkflowId(workflowId: string): Promise<boolean> {
async removeWorkflow(workflow: Workflow): Promise<boolean> {
const workflowId = workflow.id!.toString();
if (this.workflowWebhooks[workflowId] === undefined) {
// If it did not exist then there is nothing to remove
return false;
@ -105,10 +116,7 @@ export class ActiveWebhooks {
// Go through all the registered webhooks of the workflow and remove them
for (const webhookData of webhooks) {
await webhookData.workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
// Run the "deactivate" hooks on the nodes
await webhookData.workflow.runNodeHooks('deactivate', webhookData, NodeExecuteFunctions, mode);
await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
delete this.webhookUrls[this.getWebhookKey(webhookData.httpMethod, webhookData.path)];
}
@ -121,55 +129,16 @@ export class ActiveWebhooks {
/**
* Removes all the currently active webhooks
* Removes all the webhooks of the given workflow
*/
async removeAll(): Promise<void> {
const workflowIds = Object.keys(this.workflowWebhooks);
async removeAll(workflows: Workflow[]): Promise<void> {
const removePromises = [];
for (const workflowId of workflowIds) {
removePromises.push(this.removeByWorkflowId(workflowId));
for (const workflow of workflows) {
removePromises.push(this.removeWorkflow(workflow));
}
await Promise.all(removePromises);
return;
}
// /**
// * Removes a single webhook by its key.
// * Currently not used, runNodeHooks for "deactivate" is missing
// *
// * @param {string} webhookKey
// * @returns {boolean}
// * @memberof ActiveWebhooks
// */
// removeByWebhookKey(webhookKey: string): boolean {
// if (this.webhookUrls[webhookKey] === undefined) {
// // If it did not exist then there is nothing to remove
// return false;
// }
// const webhookData = this.webhookUrls[webhookKey];
// // Remove from workflow-webhooks
// const workflowWebhooks = this.workflowWebhooks[webhookData.workflowId];
// for (let index = 0; index < workflowWebhooks.length; index++) {
// if (workflowWebhooks[index].path === webhookData.path) {
// workflowWebhooks.splice(index, 1);
// break;
// }
// }
// if (workflowWebhooks.length === 0) {
// // When there are no webhooks left for any workflow remove it totally
// delete this.workflowWebhooks[webhookData.workflowId];
// }
// // Remove from webhook urls
// delete this.webhookUrls[webhookKey];
// return true;
// }
}

View File

@ -69,23 +69,25 @@ export class ActiveWorkflows {
async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise<void> {
console.log('ADD ID (active): ' + id);
this.workflowData[id] = {
workflow
};
this.workflowData[id] = {};
const triggerNodes = workflow.getTriggerNodes();
let triggerResponse: ITriggerResponse | undefined;
this.workflowData[id].triggerResponses = [];
for (const triggerNode of triggerNodes) {
triggerResponse = await workflow.runTrigger(triggerNode, getTriggerFunctions, additionalData, 'trigger');
if (triggerResponse !== undefined) {
// If a response was given save it
this.workflowData[id].triggerResponse = triggerResponse;
this.workflowData[id].triggerResponses!.push(triggerResponse);
}
}
const pollNodes = workflow.getPollNodes();
for (const pollNode of pollNodes) {
this.workflowData[id].pollResponse = await this.activatePolling(pollNode, workflow, additionalData, getPollFunctions);
if (pollNodes.length) {
this.workflowData[id].pollResponses = [];
for (const pollNode of pollNodes) {
this.workflowData[id].pollResponses!.push(await this.activatePolling(pollNode, workflow, additionalData, getPollFunctions));
}
}
}
@ -166,7 +168,6 @@ export class ActiveWorkflows {
const pollResponse = await workflow.runPoll(node, pollFunctions);
if (pollResponse !== null) {
// TODO: Run workflow
pollFunctions.__emit(pollResponse);
}
};
@ -212,12 +213,20 @@ export class ActiveWorkflows {
const workflowData = this.workflowData[id];
if (workflowData.triggerResponse && workflowData.triggerResponse.closeFunction) {
await workflowData.triggerResponse.closeFunction();
if (workflowData.triggerResponses) {
for (const triggerResponse of workflowData.triggerResponses) {
if (triggerResponse.closeFunction) {
await triggerResponse.closeFunction();
}
}
}
if (workflowData.pollResponse && workflowData.pollResponse.closeFunction) {
await workflowData.pollResponse.closeFunction();
if (workflowData.pollResponses) {
for (const pollResponse of workflowData.pollResponses) {
if (pollResponse.closeFunction) {
await pollResponse.closeFunction();
}
}
}
delete this.workflowData[id];

View File

@ -15,7 +15,6 @@ import {
ITriggerResponse,
IWebhookFunctions as IWebhookFunctionsBase,
IWorkflowSettings as IWorkflowSettingsWorkflow,
Workflow,
} from 'n8n-workflow';
@ -137,7 +136,6 @@ export interface INodeInputDataConnections {
export interface IWorkflowData {
pollResponse?: IPollResponse;
triggerResponse?: ITriggerResponse;
workflow: Workflow;
pollResponses?: IPollResponse[];
triggerResponses?: ITriggerResponse[];
}

View File

@ -50,7 +50,7 @@ export class LoadNodeParameterOptions {
connections: {},
};
this.workflow = new Workflow(undefined, workflowData.nodes, workflowData.connections, false, nodeTypes, undefined);
this.workflow = new Workflow({ nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes });
}

View File

@ -28,6 +28,7 @@ import {
IWebhookFunctions,
IWorkflowDataProxyData,
IWorkflowExecuteAdditionalData,
IWorkflowMetadata,
NodeHelpers,
NodeParameterValue,
Workflow,
@ -253,6 +254,19 @@ export function getCredentials(workflow: Workflow, node: INode, type: string, ad
/**
* Returns a copy of the node
*
* @export
* @param {INode} node
* @returns {INode}
*/
export function getNode(node: INode): INode {
return JSON.parse(JSON.stringify(node));
}
/**
* Returns the requested resolved (all expressions replaced) node parameters.
*
@ -292,6 +306,19 @@ export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecu
/**
* Returns if execution should be continued even if there was an error.
*
* @export
* @param {INode} node
* @returns {boolean}
*/
export function continueOnFail(node: INode): boolean {
return get(node, 'continueOnFail', false);
}
/**
* Returns the webhook URL of the webhook with the given name
*
@ -369,6 +396,23 @@ export function getWebhookDescription(name: string, workflow: Workflow, node: IN
/**
* Returns the workflow metadata
*
* @export
* @param {Workflow} workflow
* @returns {IWorkflowMetadata}
*/
export function getWorkflowMetadata(workflow: Workflow): IWorkflowMetadata {
return {
id: workflow.id,
name: workflow.name,
active: workflow.active,
};
}
/**
* Returns the execute functions the poll nodes have access to.
*
@ -392,6 +436,9 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio
getMode: (): WorkflowExecuteMode => {
return mode;
},
getNode: () => {
return getNode(node);
},
getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any
const runExecutionData: IRunExecutionData | null = null;
const itemIndex = 0;
@ -406,6 +453,9 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio
getTimezone: (): string => {
return getTimezone(workflow, additionalData);
},
getWorkflow: () => {
return getWorkflowMetadata(workflow);
},
getWorkflowStaticData(type: string): IDataObject {
return workflow.getStaticData(type, node);
},
@ -443,6 +493,9 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi
getCredentials(type: string): ICredentialDataDecryptedObject | undefined {
return getCredentials(workflow, node, type, additionalData);
},
getNode: () => {
return getNode(node);
},
getMode: (): WorkflowExecuteMode => {
return mode;
},
@ -460,6 +513,9 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi
getTimezone: (): string => {
return getTimezone(workflow, additionalData);
},
getWorkflow: () => {
return getWorkflowMetadata(workflow);
},
getWorkflowStaticData(type: string): IDataObject {
return workflow.getStaticData(type, node);
},
@ -494,6 +550,12 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi
export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteFunctions {
return ((workflow, runExecutionData, connectionInputData, inputData, node) => {
return {
continueOnFail: () => {
return continueOnFail(node);
},
evaluateExpression: (expression: string, itemIndex: number) => {
return workflow.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData);
},
async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any> { // tslint:disable-line:no-any
return additionalData.executeWorkflow(workflowInfo, additionalData, inputData);
},
@ -530,12 +592,18 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
getMode: (): WorkflowExecuteMode => {
return mode;
},
getNode: () => {
return getNode(node);
},
getRestApiUrl: (): string => {
return additionalData.restApiUrl;
},
getTimezone: (): string => {
return getTimezone(workflow, additionalData);
},
getWorkflow: () => {
return getWorkflowMetadata(workflow);
},
getWorkflowDataProxy: (itemIndex: number): IWorkflowDataProxyData => {
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData);
return dataProxy.getDataProxy();
@ -576,6 +644,13 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, itemIndex: number, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteSingleFunctions {
return ((workflow, runExecutionData, connectionInputData, inputData, node, itemIndex) => {
return {
continueOnFail: () => {
return continueOnFail(node);
},
evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => {
evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex;
return workflow.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData);
},
getContext(type: string): IContextObject {
return NodeHelpers.getContext(runExecutionData, type, node);
},
@ -610,6 +685,9 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
getMode: (): WorkflowExecuteMode => {
return mode;
},
getNode: () => {
return getNode(node);
},
getRestApiUrl: (): string => {
return additionalData.restApiUrl;
},
@ -619,6 +697,9 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, fallbackValue);
},
getWorkflow: () => {
return getWorkflowMetadata(workflow);
},
getWorkflowDataProxy: (): IWorkflowDataProxyData => {
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData);
return dataProxy.getDataProxy();
@ -663,6 +744,9 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio
getCurrentNodeParameters: (): INodeParameters | undefined => {
return JSON.parse('' + additionalData.currentNodeParameters);
},
getNode: () => {
return getNode(node);
},
getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any
const runExecutionData: IRunExecutionData | null = null;
const itemIndex = 0;
@ -709,6 +793,9 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio
getMode: (): WorkflowExecuteMode => {
return mode;
},
getNode: () => {
return getNode(node);
},
getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any
const runExecutionData: IRunExecutionData | null = null;
const itemIndex = 0;
@ -732,6 +819,9 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio
getWebhookDescription(name: string): IWebhookDescription | undefined {
return getWebhookDescription(name, workflow, node);
},
getWorkflow: () => {
return getWorkflowMetadata(workflow);
},
getWorkflowStaticData(type: string): IDataObject {
return workflow.getStaticData(type, node);
},
@ -780,6 +870,9 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi
getMode: (): WorkflowExecuteMode => {
return mode;
},
getNode: () => {
return getNode(node);
},
getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any
const runExecutionData: IRunExecutionData | null = null;
const itemIndex = 0;
@ -812,6 +905,9 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi
getTimezone: (): string => {
return getTimezone(workflow, additionalData);
},
getWorkflow: () => {
return getWorkflowMetadata(workflow);
},
getWorkflowStaticData(type: string): IDataObject {
return workflow.getStaticData(type, node);
},

View File

@ -1,3 +1,5 @@
import * as PCancelable from 'p-cancelable';
import {
IConnection,
IDataObject,
@ -54,7 +56,7 @@ export class WorkflowExecute {
* @returns {(Promise<string>)}
* @memberof WorkflowExecute
*/
async run(workflow: Workflow, startNode?: INode, destinationNode?: string): Promise<IRun> {
run(workflow: Workflow, startNode?: INode, destinationNode?: string): PCancelable<IRun> {
// Get the nodes to start workflow execution from
startNode = startNode || workflow.getStartNode(destinationNode);
@ -115,7 +117,8 @@ export class WorkflowExecute {
* @returns {(Promise<string>)}
* @memberof WorkflowExecute
*/
async runPartialWorkflow(workflow: Workflow, runData: IRunData, startNodes: string[], destinationNode: string): Promise<IRun> {
// @ts-ignore
async runPartialWorkflow(workflow: Workflow, runData: IRunData, startNodes: string[], destinationNode: string): PCancelable<IRun> {
let incomingNodeConnections: INodeConnections | undefined;
let connection: IConnection;
@ -209,7 +212,7 @@ export class WorkflowExecute {
},
};
return await this.processRunExecutionData(workflow);
return this.processRunExecutionData(workflow);
}
@ -444,7 +447,7 @@ export class WorkflowExecute {
* @returns {Promise<string>}
* @memberof WorkflowExecute
*/
async processRunExecutionData(workflow: Workflow): Promise<IRun> {
processRunExecutionData(workflow: Workflow): PCancelable<IRun> {
const startedAt = new Date();
const workflowIssues = workflow.checkReadyForExecution();
@ -470,231 +473,253 @@ export class WorkflowExecute {
let currentExecutionTry = '';
let lastExecutionTry = '';
return (async () => {
executionLoop:
while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) {
nodeSuccessData = null;
executionError = undefined;
executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
executionNode = executionData.node;
return new PCancelable((resolve, reject, onCancel) => {
let gotCancel = false;
this.executeHook('nodeExecuteBefore', [executionNode.name]);
onCancel.shouldReject = false;
onCancel(() => {
gotCancel = true;
});
// Get the index of the current run
runIndex = 0;
if (this.runExecutionData.resultData.runData.hasOwnProperty(executionNode.name)) {
runIndex = this.runExecutionData.resultData.runData[executionNode.name].length;
}
const returnPromise = (async () => {
currentExecutionTry = `${executionNode.name}:${runIndex}`;
executionLoop:
while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) {
if (currentExecutionTry === lastExecutionTry) {
throw new Error('Did stop execution because execution seems to be in endless loop.');
}
if (this.runExecutionData.startData!.runNodeFilter !== undefined && this.runExecutionData.startData!.runNodeFilter!.indexOf(executionNode.name) === -1) {
// If filter is set and node is not on filter skip it, that avoids the problem that it executes
// leafs that are parallel to a selected destinationNode. Normally it would execute them because
// they have the same parent and it executes all child nodes.
continue;
}
// Check if all the data which is needed to run the node is available
if (workflow.connectionsByDestinationNode.hasOwnProperty(executionNode.name)) {
// Check if the node has incoming connections
if (workflow.connectionsByDestinationNode[executionNode.name].hasOwnProperty('main')) {
let inputConnections: IConnection[][];
let connectionIndex: number;
inputConnections = workflow.connectionsByDestinationNode[executionNode.name]['main'];
for (connectionIndex = 0; connectionIndex < inputConnections.length; connectionIndex++) {
if (workflow.getHighestNode(executionNode.name, 'main', connectionIndex).length === 0) {
// If there is no valid incoming node (if all are disabled)
// then ignore that it has inputs and simply execute it as it is without
// any data
continue;
}
if (!executionData.data!.hasOwnProperty('main')) {
// ExecutionData does not even have the connection set up so can
// not have that data, so add it again to be executed later
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
lastExecutionTry = currentExecutionTry;
continue executionLoop;
}
// Check if it has the data for all the inputs
// The most nodes just have one but merge node for example has two and data
// of both inputs has to be available to be able to process the node.
if (executionData.data!.main!.length < connectionIndex || executionData.data!.main![connectionIndex] === null) {
// Does not have the data of the connections so add back to stack
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
lastExecutionTry = currentExecutionTry;
continue executionLoop;
}
}
// @ts-ignore
if (gotCancel === true) {
return Promise.resolve();
}
}
// Clone input data that nodes can not mess up data of parallel nodes which receive the same data
// TODO: Should only clone if multiple nodes get the same data or when it gets returned to frontned
// is very slow so only do if needed
startTime = new Date().getTime();
nodeSuccessData = null;
executionError = undefined;
executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
executionNode = executionData.node;
let maxTries = 1;
if (executionData.node.retryOnFail === true) {
// TODO: Remove the hardcoded default-values here and also in NodeSettings.vue
maxTries = Math.min(5, Math.max(2, executionData.node.maxTries || 3));
}
this.executeHook('nodeExecuteBefore', [executionNode.name]);
let waitBetweenTries = 0;
if (executionData.node.retryOnFail === true) {
// TODO: Remove the hardcoded default-values here and also in NodeSettings.vue
waitBetweenTries = Math.min(5000, Math.max(0, executionData.node.waitBetweenTries || 1000));
}
for (let tryIndex = 0; tryIndex < maxTries; tryIndex++) {
try {
if (tryIndex !== 0) {
// Reset executionError from previous error try
executionError = undefined;
if (waitBetweenTries !== 0) {
// TODO: Improve that in the future and check if other nodes can
// be executed in the meantime
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, waitBetweenTries);
});
}
}
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
if (nodeSuccessData === null) {
// If null gets returned it means that the node did succeed
// but did not have any data. So the branch should end
// (meaning the nodes afterwards should not be processed)
continue executionLoop;
}
break;
} catch (error) {
executionError = {
message: error.message,
stack: error.stack,
};
// Get the index of the current run
runIndex = 0;
if (this.runExecutionData.resultData.runData.hasOwnProperty(executionNode.name)) {
runIndex = this.runExecutionData.resultData.runData[executionNode.name].length;
}
}
// Add the data to return to the user
// (currently does not get cloned as data does not get changed, maybe later we should do that?!?!)
currentExecutionTry = `${executionNode.name}:${runIndex}`;
if (!this.runExecutionData.resultData.runData.hasOwnProperty(executionNode.name)) {
this.runExecutionData.resultData.runData[executionNode.name] = [];
}
taskData = {
startTime,
executionTime: (new Date().getTime()) - startTime
};
if (executionError !== undefined) {
taskData.error = executionError;
if (executionData.node.continueOnFail === true) {
// Workflow should continue running even if node errors
if (executionData.data.hasOwnProperty('main') && executionData.data.main.length > 0) {
// Simply get the input data of the node if it has any and pass it through
// to the next node
if (executionData.data.main[0] !== null) {
nodeSuccessData = [executionData.data.main[0] as INodeExecutionData[]];
}
}
} else {
// Node execution did fail so add error and stop execution
this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
// Add the execution data again so that it can get restarted
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
this.executeHook('nodeExecuteAfter', [executionNode.name, taskData]);
break;
if (currentExecutionTry === lastExecutionTry) {
throw new Error('Did stop execution because execution seems to be in endless loop.');
}
}
// Node executed successfully. So add data and go on.
taskData.data = ({
'main': nodeSuccessData
} as ITaskDataConnections);
if (this.runExecutionData.startData!.runNodeFilter !== undefined && this.runExecutionData.startData!.runNodeFilter!.indexOf(executionNode.name) === -1) {
// If filter is set and node is not on filter skip it, that avoids the problem that it executes
// leafs that are parallel to a selected destinationNode. Normally it would execute them because
// they have the same parent and it executes all child nodes.
continue;
}
this.executeHook('nodeExecuteAfter', [executionNode.name, taskData]);
// Check if all the data which is needed to run the node is available
if (workflow.connectionsByDestinationNode.hasOwnProperty(executionNode.name)) {
// Check if the node has incoming connections
if (workflow.connectionsByDestinationNode[executionNode.name].hasOwnProperty('main')) {
let inputConnections: IConnection[][];
let connectionIndex: number;
this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
inputConnections = workflow.connectionsByDestinationNode[executionNode.name]['main'];
if (this.runExecutionData.startData && this.runExecutionData.startData.destinationNode && this.runExecutionData.startData.destinationNode === executionNode.name) {
// If destination node is defined and got executed stop execution
continue;
}
// Add the nodes to which the current node has an output connection to that they can
// be executed next
if (workflow.connectionsBySourceNode.hasOwnProperty(executionNode.name)) {
if (workflow.connectionsBySourceNode[executionNode.name].hasOwnProperty('main')) {
let outputIndex: string, connectionData: IConnection;
// Go over all the different
// Add the nodes to be executed
for (outputIndex in workflow.connectionsBySourceNode[executionNode.name]['main']) {
if (!workflow.connectionsBySourceNode[executionNode.name]['main'].hasOwnProperty(outputIndex)) {
continue;
}
// Go through all the different outputs of this connection
for (connectionData of workflow.connectionsBySourceNode[executionNode.name]['main'][outputIndex]) {
if (!workflow.nodes.hasOwnProperty(connectionData.node)) {
return Promise.reject(new Error(`The node "${executionNode.name}" connects to not found node "${connectionData.node}"`));
for (connectionIndex = 0; connectionIndex < inputConnections.length; connectionIndex++) {
if (workflow.getHighestNode(executionNode.name, 'main', connectionIndex).length === 0) {
// If there is no valid incoming node (if all are disabled)
// then ignore that it has inputs and simply execute it as it is without
// any data
continue;
}
this.addNodeToBeExecuted(workflow, connectionData, parseInt(outputIndex, 10), executionNode.name, nodeSuccessData!, runIndex);
if (!executionData.data!.hasOwnProperty('main')) {
// ExecutionData does not even have the connection set up so can
// not have that data, so add it again to be executed later
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
lastExecutionTry = currentExecutionTry;
continue executionLoop;
}
// Check if it has the data for all the inputs
// The most nodes just have one but merge node for example has two and data
// of both inputs has to be available to be able to process the node.
if (executionData.data!.main!.length < connectionIndex || executionData.data!.main![connectionIndex] === null) {
// Does not have the data of the connections so add back to stack
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
lastExecutionTry = currentExecutionTry;
continue executionLoop;
}
}
}
}
// Clone input data that nodes can not mess up data of parallel nodes which receive the same data
// TODO: Should only clone if multiple nodes get the same data or when it gets returned to frontned
// is very slow so only do if needed
startTime = new Date().getTime();
let maxTries = 1;
if (executionData.node.retryOnFail === true) {
// TODO: Remove the hardcoded default-values here and also in NodeSettings.vue
maxTries = Math.min(5, Math.max(2, executionData.node.maxTries || 3));
}
let waitBetweenTries = 0;
if (executionData.node.retryOnFail === true) {
// TODO: Remove the hardcoded default-values here and also in NodeSettings.vue
waitBetweenTries = Math.min(5000, Math.max(0, executionData.node.waitBetweenTries || 1000));
}
for (let tryIndex = 0; tryIndex < maxTries; tryIndex++) {
// @ts-ignore
if (gotCancel === true) {
return Promise.resolve();
}
try {
if (tryIndex !== 0) {
// Reset executionError from previous error try
executionError = undefined;
if (waitBetweenTries !== 0) {
// TODO: Improve that in the future and check if other nodes can
// be executed in the meantime
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, waitBetweenTries);
});
}
}
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
if (nodeSuccessData === null) {
// If null gets returned it means that the node did succeed
// but did not have any data. So the branch should end
// (meaning the nodes afterwards should not be processed)
continue executionLoop;
}
break;
} catch (error) {
executionError = {
message: error.message,
stack: error.stack,
};
}
}
// Add the data to return to the user
// (currently does not get cloned as data does not get changed, maybe later we should do that?!?!)
if (!this.runExecutionData.resultData.runData.hasOwnProperty(executionNode.name)) {
this.runExecutionData.resultData.runData[executionNode.name] = [];
}
taskData = {
startTime,
executionTime: (new Date().getTime()) - startTime
};
if (executionError !== undefined) {
taskData.error = executionError;
if (executionData.node.continueOnFail === true) {
// Workflow should continue running even if node errors
if (executionData.data.hasOwnProperty('main') && executionData.data.main.length > 0) {
// Simply get the input data of the node if it has any and pass it through
// to the next node
if (executionData.data.main[0] !== null) {
nodeSuccessData = [executionData.data.main[0] as INodeExecutionData[]];
}
}
} else {
// Node execution did fail so add error and stop execution
this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
// Add the execution data again so that it can get restarted
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
this.executeHook('nodeExecuteAfter', [executionNode.name, taskData]);
break;
}
}
// Node executed successfully. So add data and go on.
taskData.data = ({
'main': nodeSuccessData
} as ITaskDataConnections);
this.executeHook('nodeExecuteAfter', [executionNode.name, taskData]);
this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
if (this.runExecutionData.startData && this.runExecutionData.startData.destinationNode && this.runExecutionData.startData.destinationNode === executionNode.name) {
// If destination node is defined and got executed stop execution
continue;
}
// Add the nodes to which the current node has an output connection to that they can
// be executed next
if (workflow.connectionsBySourceNode.hasOwnProperty(executionNode.name)) {
if (workflow.connectionsBySourceNode[executionNode.name].hasOwnProperty('main')) {
let outputIndex: string, connectionData: IConnection;
// Go over all the different
// Add the nodes to be executed
for (outputIndex in workflow.connectionsBySourceNode[executionNode.name]['main']) {
if (!workflow.connectionsBySourceNode[executionNode.name]['main'].hasOwnProperty(outputIndex)) {
continue;
}
// Go through all the different outputs of this connection
for (connectionData of workflow.connectionsBySourceNode[executionNode.name]['main'][outputIndex]) {
if (!workflow.nodes.hasOwnProperty(connectionData.node)) {
return Promise.reject(new Error(`The node "${executionNode.name}" connects to not found node "${connectionData.node}"`));
}
this.addNodeToBeExecuted(workflow, connectionData, parseInt(outputIndex, 10), executionNode.name, nodeSuccessData!, runIndex);
}
}
}
}
}
}
return Promise.resolve();
})()
.then(async () => {
return this.processSuccessExecution(startedAt, workflow, executionError);
})
.catch(async (error) => {
const fullRunData = this.getFullRunData(startedAt);
return Promise.resolve();
})()
.then(async () => {
return this.processSuccessExecution(startedAt, workflow, executionError);
})
.catch(async (error) => {
const fullRunData = this.getFullRunData(startedAt);
fullRunData.data.resultData.error = {
message: error.message,
stack: error.stack,
};
fullRunData.data.resultData.error = {
message: error.message,
stack: error.stack,
};
// Check if static data changed
let newStaticData: IDataObject | undefined;
if (workflow.staticData.__dataChanged === true) {
// Static data of workflow changed
newStaticData = workflow.staticData;
}
// Check if static data changed
let newStaticData: IDataObject | undefined;
if (workflow.staticData.__dataChanged === true) {
// Static data of workflow changed
newStaticData = workflow.staticData;
}
await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]);
await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]);
return fullRunData;
return fullRunData;
});
return returnPromise.then(resolve);
});
}
async processSuccessExecution(startedAt: Date, workflow: Workflow, executionError?: IExecutionError): Promise<IRun> {
// @ts-ignore
async processSuccessExecution(startedAt: Date, workflow: Workflow, executionError?: IExecutionError): PCancelable<IRun> {
const fullRunData = this.getFullRunData(startedAt);
if (executionError !== undefined) {

View File

@ -579,7 +579,7 @@ describe('WorkflowExecute', () => {
for (const testData of tests) {
test(testData.description, async () => {
const workflowInstance = new Workflow('test', testData.input.workflowData.nodes, testData.input.workflowData.connections, false, nodeTypes);
const workflowInstance = new Workflow({ id: 'test', nodes: testData.input.workflowData.nodes, connections: testData.input.workflowData.connections, active: false, nodeTypes });
const waitPromise = await createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = [];

View File

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.31.0",
"version": "0.40.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -17,7 +17,7 @@
"build": "vue-cli-service build",
"dev": "npm run serve",
"lint": "vue-cli-service lint",
"serve": "VUE_APP_URL_BASE_API=http://localhost:5678/ vue-cli-service serve",
"serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vue-cli-service serve",
"test": "npm run test:unit",
"tslint": "tslint -p tsconfig.json -c tslint.json",
"test:e2e": "vue-cli-service test:e2e",
@ -50,6 +50,7 @@
"axios": "^0.19.0",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"cross-env": "^7.0.2",
"dateformat": "^3.0.3",
"element-ui": "~2.13.0",
"eslint": "^6.8.0",
@ -63,7 +64,7 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.20.0",
"n8n-workflow": "~0.26.0",
"node-sass": "^4.12.0",
"prismjs": "^1.17.1",
"quill": "^2.0.0-dev.3",

View File

@ -0,0 +1,88 @@
<template>
<span>
<el-dialog class="n8n-about" :visible="dialogVisible" append-to-body width="50%" title="About n8n" :before-close="closeDialog">
<div>
<el-row>
<el-col :span="8" class="info-name">
n8n Version:
</el-col>
<el-col :span="16">
{{versionCli}}
</el-col>
</el-row>
<el-row>
<el-col :span="8" class="info-name">
Source Code:
</el-col>
<el-col :span="16">
<a href="https://github.com/n8n-io/n8n" target="_blank">https://github.com/n8n-io/n8n</a>
</el-col>
</el-row>
<el-row>
<el-col :span="8" class="info-name">
License:
</el-col>
<el-col :span="16">
<a href="https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md" target="_blank">Apache 2.0 with Commons Clause</a>
</el-col>
</el-row>
<div class="action-buttons">
<el-button type="success" @click="closeDialog">
Close
</el-button>
</div>
</div>
</el-dialog>
</span>
</template>
<script lang="ts">
import Vue from 'vue';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { showMessage } from '@/components/mixins/showMessage';
import mixins from 'vue-typed-mixins';
export default mixins(
genericHelpers,
showMessage,
).extend({
name: 'About',
props: [
'dialogVisible',
],
computed: {
versionCli (): string {
return this.$store.getters.versionCli;
},
},
methods: {
closeDialog () {
// Handle the close externally as the visible parameter is an external prop
// and is so not allowed to be changed here.
this.$emit('closeDialog');
return false;
},
},
});
</script>
<style scoped lang="scss">
.n8n-about {
.el-row {
padding: 0.25em 0;
}
}
.action-buttons {
margin-top: 1em;
text-align: right;
}
.info-name {
line-height: 32px;
}
</style>

View File

@ -124,7 +124,7 @@ export default mixins(
try {
this.credentials = JSON.parse(JSON.stringify(this.$store.getters.allCredentials));
} catch (error) {
this.$showError(error, 'Problem loading credentials', 'There was a problem loading the credentials:');
this.$showError(error, 'Proble loading credentials', 'There was a problem loading the credentials:');
this.isDataLoading = false;
return;
}

View File

@ -102,7 +102,7 @@ export default Vue.extend({
margin-top: 1em;
}
/deep/ .expression-dialog {
::v-deep .expression-dialog {
.el-dialog__header {
padding: 0;
}

View File

@ -166,11 +166,11 @@ export default mixins(
let returnValue;
try {
returnValue = this.resolveExpression(`=${variableName}`);
} catch (e) {
return 'invalid';
} catch (error) {
return `[invalid (${error.message})]`;
}
if (returnValue === undefined) {
return 'not found';
return '[not found]';
}
return returnValue;
@ -258,16 +258,19 @@ export default mixins(
} else if (value.charAt(0) === '^') {
// Is variable
let displayValue = `{{${value.slice(1)}}}` as string | number | boolean;
let displayValue = `{{${value.slice(1)}}}` as string | number | boolean | null;
if (this.resolvedValue) {
displayValue = this.resolveParameterString(displayValue.toString()) as NodeParameterValue;
displayValue = [null, undefined].includes(displayValue as null | undefined) ? '' : displayValue;
displayValue = this.resolveParameterString((displayValue as string).toString()) as NodeParameterValue;
}
displayValue = [null, undefined].includes(displayValue as null | undefined) ? '' : displayValue;
editorOperations.push({
attributes: {
variable: `{{${value.slice(1)}}}`,
},
insert: displayValue.toString(),
insert: (displayValue as string).toString(),
});
} else {
// Is text

View File

@ -1,5 +1,6 @@
<template>
<div id="side-menu">
<about :dialogVisible="aboutDialogVisible" @closeDialog="closeAboutDialog"></about>
<executions-list :dialogVisible="executionsListDialogVisible" @closeDialog="closeExecutionsListOpenDialog"></executions-list>
<credentials-list :dialogVisible="credentialOpenDialogVisible" @closeDialog="closeCredentialOpenDialog"></credentials-list>
<credentials-edit :dialogVisible="credentialNewDialogVisible" @closeDialog="closeCredentialNewDialog"></credentials-edit>
@ -14,15 +15,9 @@
<el-menu default-active="workflow" @select="handleSelect" :collapse="isCollapsed">
<el-menu-item index="logo" class="logo-item">
<el-tooltip placement="top" effect="light">
<div slot="content">
n8n.io - Currently installed version {{versionCli}}
</div>
<a href="https://n8n.io" target="_blank" class="logo">
<img src="/n8n-icon-small.png" class="icon" alt="n8n.io"/>
</el-tooltip>
<a href="https://n8n.io" class="logo-text" target="_blank" slot="title">
n8n.io
<span class="logo-text" slot="title">n8n.io</span>
</a>
</el-menu-item>
@ -149,6 +144,12 @@
</a>
</template>
</el-menu-item>
<el-menu-item index="help-about">
<template slot="title">
<font-awesome-icon class="about-icon" icon="info"/>
<span slot="title" class="item-title">About n8n</span>
</template>
</el-menu-item>
</el-submenu>
</el-menu>
@ -168,6 +169,7 @@ import {
IWorkflowDataUpdate,
} from '../Interface';
import About from '@/components/About.vue';
import CredentialsEdit from '@/components/CredentialsEdit.vue';
import CredentialsList from '@/components/CredentialsList.vue';
import ExecutionsList from '@/components/ExecutionsList.vue';
@ -196,6 +198,7 @@ export default mixins(
.extend({
name: 'MainHeader',
components: {
About,
CredentialsEdit,
CredentialsList,
ExecutionsList,
@ -204,6 +207,7 @@ export default mixins(
},
data () {
return {
aboutDialogVisible: false,
isCollapsed: true,
credentialNewDialogVisible: false,
credentialOpenDialogVisible: false,
@ -251,9 +255,6 @@ export default mixins(
currentWorkflow (): string {
return this.$route.params.name;
},
versionCli (): string {
return this.$store.getters.versionCli;
},
workflowExecution (): IExecutionResponse | null {
return this.$store.getters.getWorkflowExecution;
},
@ -269,6 +270,9 @@ export default mixins(
this.$store.commit('setWorkflowExecutionData', null);
this.updateNodesExecutionIssues();
},
closeAboutDialog () {
this.aboutDialogVisible = false;
},
closeWorkflowOpenDialog () {
this.workflowOpenDialogVisible = false;
},
@ -434,6 +438,8 @@ export default mixins(
this.saveCurrentWorkflow();
} else if (key === 'workflow-save-as') {
this.saveCurrentWorkflow(true);
} else if (key === 'help-about') {
this.aboutDialogVisible = true;
} else if (key === 'workflow-settings') {
this.workflowSettingsDialogVisible = true;
} else if (key === 'workflow-new') {
@ -466,6 +472,9 @@ export default mixins(
</script>
<style lang="scss">
.about-icon {
padding-left: 5px;
}
#collapse-change-button {
position: absolute;
@ -520,7 +529,11 @@ export default mixins(
}
}
a.logo-text {
a.logo {
text-decoration: none;
}
.logo-text {
position: relative;
top: -3px;
left: 5px;

View File

@ -138,7 +138,7 @@ export default mixins(genericHelpers)
}
}
/deep/ .duplicate-parameter-item {
::v-deep .duplicate-parameter-item {
position: relative;
margin-top: 0.5em;
padding-top: 0.5em;
@ -148,11 +148,11 @@ export default mixins(genericHelpers)
}
}
/deep/ .duplicate-parameter-input-item {
::v-deep .duplicate-parameter-input-item {
margin: 0.5em 0 0.25em 2em;
}
/deep/ .duplicate-parameter-item + .duplicate-parameter-item {
::v-deep .duplicate-parameter-item + .duplicate-parameter-item {
.collection-parameter-wrapper {
border-top: 1px dashed #999;
padding-top: 0.5em;

View File

@ -22,6 +22,9 @@
<div @click.stop.left="duplicateNode" class="option" title="Duplicate Node" >
<font-awesome-icon icon="clone" />
</div>
<div @click.stop.left="setNodeActive" class="option touch" title="Edit Node" v-if="!isReadOnly">
<font-awesome-icon class="execute-icon" icon="cog" />
</div>
<div @click.stop.left="executeNode" class="option" title="Execute Node" v-if="!isReadOnly && !workflowRunning">
<font-awesome-icon class="execute-icon" icon="play-circle" />
</div>
@ -103,6 +106,10 @@ export default mixins(nodeBase, workflowHelpers).extend({
classes.push('has-issues');
}
if (this.isTouchDevice) {
classes.push('is-touch-device');
}
return classes;
},
nodeIssues (): string {
@ -163,19 +170,12 @@ export default mixins(nodeBase, workflowHelpers).extend({
},
data () {
return {
isTouchDevice: 'ontouchstart' in window || navigator.msMaxTouchPoints,
};
},
methods: {
disableNode () {
// Toggle disabled flag
const updateInformation = {
name: this.data.name,
properties: {
disabled: !this.data.disabled,
},
};
this.$store.commit('updateNodeProperties', updateInformation);
this.disableNodes([this.data]);
},
executeNode () {
this.$emit('runWorkflow', this.data.name);
@ -331,6 +331,10 @@ export default mixins(nodeBase, workflowHelpers).extend({
display: inline-block;
padding: 0 0.3em;
&.touch {
display: none;
}
&:hover {
color: $--color-primary;
}
@ -343,6 +347,15 @@ export default mixins(nodeBase, workflowHelpers).extend({
}
}
&.is-touch-device .node-options {
left: -25px;
width: 150px;
.option.touch {
display: initial;
}
}
&.has-data .node-options,
&.has-issues .node-options {
top: -35px;

View File

@ -57,7 +57,7 @@ import ParameterInputFull from '@/components/ParameterInputFull.vue';
import ParameterInputList from '@/components/ParameterInputList.vue';
import NodeCredentials from '@/components/NodeCredentials.vue';
import NodeWebhooks from '@/components/NodeWebhooks.vue';
import { get, set } from 'lodash';
import { get, set, unset } from 'lodash';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
@ -288,20 +288,6 @@ export default mixins(
}
}
},
updateNodeCredentialIssues (node: INodeUi): void {
const fullNodeIssues: INodeIssues | null = this.getNodeCredentialIssues(node);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.credentials!;
}
this.$store.commit('setNodeIssue', {
node: node.name,
type: 'credentials',
value: newIssues,
} as INodeIssueData);
},
credentialSelected (updateInformation: INodeUpdatePropertiesInformation) {
// Update the values on the node
this.$store.commit('updateNodeProperties', updateInformation);
@ -369,8 +355,11 @@ export default mixins(
Vue.set(nodeParameters as object, path, data);
}
} else {
// For everything else
set(nodeParameters as object, parameterPath, newValue);
if (newValue === undefined) {
unset(nodeParameters as object, parameterPath);
} else {
set(nodeParameters as object, parameterPath, newValue);
}
}
// Get the parameters with the now new defaults according to the
@ -390,20 +379,7 @@ export default mixins(
};
this.$store.commit('setNodeParameters', updateInformation);
// All data got updated everywhere so update now the issues
const fullNodeIssues: INodeIssues | null = NodeHelpers.getNodeParametersIssues(nodeType.properties, node);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.parameters!;
}
this.$store.commit('setNodeIssue', {
node: node.name,
type: 'parameters',
value: newIssues,
} as INodeIssueData);
this.updateNodeParameterIssues(node, nodeType);
this.updateNodeCredentialIssues(node);
} else {
// A property on the node itself changed

View File

@ -276,7 +276,7 @@ export default mixins(
returnValue = this.expressionValueComputed;
}
if (returnValue !== undefined && this.parameter.type === 'string') {
if (returnValue !== undefined && returnValue !== null && this.parameter.type === 'string') {
const rows = this.getArgument('rows');
if (rows === undefined || rows === 1) {
returnValue = returnValue.toString().replace(/\n/, '|');

View File

@ -30,6 +30,9 @@ export default Vue
},
computed: {
isMultiLineParameter () {
if (this.level > 4) {
return true;
}
const rows = this.getArgument('rows');
if (rows !== undefined && rows > 1) {
return true;
@ -37,6 +40,9 @@ export default Vue
return false;
},
level (): number {
return this.path.split('.').length;
},
},
props: [
'displayOptions',

View File

@ -47,6 +47,9 @@ export default Vue.extend({
return false;
},
},
mounted () {
this.tempValue = this.value as string;
},
watch: {
dialogVisible () {
if (this.dialogVisible === true) {

View File

@ -168,6 +168,13 @@ export default mixins(
const returnData: IVariableSelectorOption[] = [];
if (inputData === null) {
returnData.push(
{
name: propertyName,
key: fullpath,
value: '[null]',
} as IVariableSelectorOption,
);
return returnData;
} else if (Array.isArray(inputData)) {
let newPropertyName = propertyName;
@ -286,7 +293,7 @@ export default mixins(
if (outputData.hasOwnProperty('json')) {
const jsonDataOptions: IVariableSelectorOption[] = [];
for (const propertyName of Object.keys(outputData.json)) {
jsonDataOptions.push.apply(jsonDataOptions, this.jsonDataToFilterOption(outputData.json[propertyName], `$node["${nodeName}"].data`, propertyName, filterText));
jsonDataOptions.push.apply(jsonDataOptions, this.jsonDataToFilterOption(outputData.json[propertyName], `$node["${nodeName}"].json`, propertyName, filterText));
}
if (jsonDataOptions.length) {

View File

@ -6,6 +6,7 @@ import {
INodeParameters,
INodeExecutionData,
INodeIssues,
INodeIssueData,
INodeIssueObjectProperty,
INodeProperties,
INodeTypeDescription,
@ -121,8 +122,55 @@ export const nodeHelpers = mixins(
}
},
// Updates the credential-issues of the node
updateNodeCredentialIssues(node: INodeUi): void {
const fullNodeIssues: INodeIssues | null = this.getNodeCredentialIssues(node);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.credentials!;
}
this.$store.commit('setNodeIssue', {
node: node.name,
type: 'credentials',
value: newIssues,
} as INodeIssueData);
},
// Updates the parameter-issues of the node
updateNodeParameterIssues(node: INodeUi, nodeType?: INodeTypeDescription): void {
if (nodeType === undefined) {
nodeType = this.$store.getters.nodeType(node.type);
}
if (nodeType === null) {
// Could not find nodeType so can not update issues
return;
}
// All data got updated everywhere so update now the issues
const fullNodeIssues: INodeIssues | null = NodeHelpers.getNodeParametersIssues(nodeType!.properties, node);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.parameters!;
}
this.$store.commit('setNodeIssue', {
node: node.name,
type: 'parameters',
value: newIssues,
} as INodeIssueData);
},
// Returns all the credential-issues of the node
getNodeCredentialIssues (node: INodeUi, nodeType?: INodeTypeDescription): INodeIssues | null {
if (node.disabled === true) {
// Node is disabled
return null;
}
if (nodeType === undefined) {
nodeType = this.$store.getters.nodeType(node.type);
}
@ -257,5 +305,21 @@ export const nodeHelpers = mixins(
return returnData;
},
disableNodes(nodes: INodeUi[]) {
for (const node of nodes) {
// Toggle disabled flag
const updateInformation = {
name: node.name,
properties: {
disabled: !node.disabled,
},
};
this.$store.commit('updateNodeProperties', updateInformation);
this.updateNodeParameterIssues(node);
this.updateNodeCredentialIssues(node);
}
},
},
});

View File

@ -192,14 +192,16 @@ export const workflowHelpers = mixins(
};
let workflowId = this.$store.getters.workflowId;
if (workflowId !== PLACEHOLDER_EMPTY_WORKFLOW_ID) {
if (workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
workflowId = undefined;
}
const workflowName = this.$store.getters.workflowName;
if (copyData === true) {
return new Workflow(workflowId, JSON.parse(JSON.stringify(nodes)), JSON.parse(JSON.stringify(connections)), false, nodeTypes);
return new Workflow({ id: workflowId, name: workflowName, nodes: JSON.parse(JSON.stringify(nodes)), connections: JSON.parse(JSON.stringify(connections)), active: false, nodeTypes});
} else {
return new Workflow(workflowId, nodes, connections, false, nodeTypes);
return new Workflow({ id: workflowId, name: workflowName, nodes, connections, active: false, nodeTypes});
}
},

View File

@ -376,7 +376,7 @@ export default mixins(
this.createNodeActive = false;
this.$store.commit('setActiveNode', null);
} else if (e.key === 'Tab') {
this.createNodeActive = !this.createNodeActive;
this.createNodeActive = !this.createNodeActive && !this.isReadOnly;
} else if (e.key === this.controlKeyCode) {
this.ctrlKeyPressed = true;
} else if (e.key === 'F2') {
@ -547,19 +547,7 @@ export default mixins(
if (this.editAllowedCheck() === false) {
return;
}
let updateInformation;
this.$store.getters.getSelectedNodes.forEach((node: INodeUi) => {
// Toggle disabled flag
updateInformation = {
name: node.name,
properties: {
disabled: !node.disabled,
},
};
this.$store.commit('updateNodeProperties', updateInformation);
});
this.disableNodes(this.$store.getters.getSelectedNodes);
},
deleteSelectedNodes () {

View File

@ -12,7 +12,6 @@ module.exports = {
},
},
configureWebpack: {
devtool: 'source-map',
plugins: [
new GoogleFontsPlugin({
fonts: [

View File

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "0.5.0",
"version": "0.6.0",
"description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -58,8 +58,8 @@
"change-case": "^3.1.0",
"copyfiles": "^2.1.1",
"inquirer": "^7.0.0",
"n8n-core": "^0.18.0",
"n8n-workflow": "^0.18.0",
"n8n-core": "^0.21.0",
"n8n-workflow": "^0.20.0",
"replace-in-file": "^4.1.0",
"request": "^2.88.0",
"tmp-promise": "^2.0.2",

View File

@ -43,7 +43,7 @@ export async function createCustomTsconfig () {
tsConfig.include = newIncludeFiles;
// Write new custom tsconfig file
const { fd, path, cleanup } = await file();
const { fd, path, cleanup } = await file({ dir: process.cwd() });
await fsWriteAsync(fd, Buffer.from(JSON.stringify(tsConfig, null, 2), 'utf8'));
return {
@ -64,7 +64,7 @@ export async function buildFiles (options?: IBuildOptions): Promise<string> {
options = options || {};
// Get the path of the TypeScript cli of this project
const tscPath = join(__dirname, '../../node_modules/typescript/bin/tsc');
const tscPath = join(__dirname, '../../node_modules/.bin/tsc');
const tsconfigData = await createCustomTsconfig();

View File

@ -0,0 +1,23 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class AcuitySchedulingApi implements ICredentialType {
name = 'acuitySchedulingApi';
displayName = 'Acuity Scheduling API';
properties = [
{
displayName: 'User ID',
name: 'userId',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class AffinityApi implements ICredentialType {
name = 'affinityApi';
displayName = 'Affinity API';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class BitlyApi implements ICredentialType {
name = 'bitlyApi';
displayName = 'Bitly API';
properties = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class CalendlyApi implements ICredentialType {
name = 'calendlyApi';
displayName = 'Calendly API';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class ClearbitApi implements ICredentialType {
name = 'clearbitApi';
displayName = 'Clearbit API';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class ClickUpApi implements ICredentialType {
name = 'clickUpApi';
displayName = 'ClickUp API';
properties = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,21 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class ClockifyApi implements ICredentialType {
name = 'clockifyApi';
displayName = 'Clockify API';
properties = [
// The credentials to get from user and save encrypted.
// Properties can be defined exactly in the same way
// as node properties.
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,25 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class CopperApi implements ICredentialType {
name = 'copperApi';
displayName = 'Copper API';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
required: true,
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Email',
name: 'email',
required: true,
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,18 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class DisqusApi implements ICredentialType {
name = 'disqusApi';
displayName = 'Disqus API';
properties = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Visit your account details page, and grab the Access Token. See <a href="https://disqus.com/api/docs/auth/">Disqus auth</a>.'
},
];
}

View File

@ -0,0 +1,18 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class DriftApi implements ICredentialType {
name = 'driftApi';
displayName = 'Drift API';
properties = [
{
displayName: 'Personal Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Visit your account details page, and grab the Access Token. See <a href="https://devdocs.drift.com/docs/quick-start">Drift auth</a>.'
},
];
}

View File

@ -18,7 +18,8 @@ export class FreshdeskApi implements ICredentialType {
displayName: 'Domain',
name: 'domain',
type: 'string' as NodePropertyTypes,
placeholder: 'https://domain.freshdesk.com',
placeholder: 'company',
description: 'If the URL you get displayed on Freshdesk is "https://company.freshdesk.com" enter "company"',
default: ''
}
];

View File

@ -3,11 +3,17 @@ import {
NodePropertyTypes,
} from 'n8n-workflow';
export class GithubApi implements ICredentialType {
name = 'githubApi';
displayName = 'Github API';
properties = [
{
displayName: 'Github Server',
name: 'server',
type: 'string' as NodePropertyTypes,
default: 'https://api.github.com',
description: 'The server to connect to. Does only have to get changed if Github Enterprise gets used.',
},
{
displayName: 'User',
name: 'user',

View File

@ -11,6 +11,13 @@ export class GithubOAuth2Api implements ICredentialType {
];
displayName = 'Github OAuth2 API';
properties = [
{
displayName: 'Github Server',
name: 'server',
type: 'string' as NodePropertyTypes,
default: 'https://api.github.com',
description: 'The server to connect to. Does only have to get changed if Github Enterprise gets used.',
},
{
displayName: 'Authorization URL',
name: 'authUrl',

View File

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class GumroadApi implements ICredentialType {
name = 'gumroadApi';
displayName = 'Gumroad API';
properties = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,25 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class HarvestApi implements ICredentialType {
name = 'harvestApi';
displayName = 'Harvest API';
properties = [
{
displayName: 'Account ID',
name: 'accountId',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Visit your account details page, and grab the Account ID. See <a href="https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/">Harvest Personal Access Tokens</a>.'
},
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Visit your account details page, and grab the Access Token. See <a href="https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/">Harvest Personal Access Tokens</a>.'
},
];
}

View File

@ -0,0 +1,23 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class HubspotDeveloperApi implements ICredentialType {
name = 'hubspotDeveloperApi';
displayName = 'Hubspot API';
properties = [
{
displayName: 'Developer API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Client Secret',
name: 'clientSecret',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class HunterApi implements ICredentialType {
name = 'hunterApi';
displayName = 'Hunter API';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,23 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class InvoiceNinjaApi implements ICredentialType {
name = 'invoiceNinjaApi';
displayName = 'Invoice Ninja API';
properties = [
{
displayName: 'URL',
name: 'url',
type: 'string' as NodePropertyTypes,
default: 'https://app.invoiceninja.com',
},
{
displayName: 'API Token',
name: 'apiToken',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -24,6 +24,7 @@ export class JiraSoftwareCloudApi implements ICredentialType {
name: 'domain',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'https://example.atlassian.net',
},
];
}

View File

@ -0,0 +1,33 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class JiraSoftwareServerApi implements ICredentialType {
name = 'jiraSoftwareServerApi';
displayName = 'Jira SW Server API';
properties = [
{
displayName: 'Email',
name: 'email',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Password',
name: 'password',
typeOptions: {
password: true,
},
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Domain',
name: 'domain',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'https://example.com',
},
];
}

View File

@ -0,0 +1,34 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class JotFormApi implements ICredentialType {
name = 'jotFormApi';
displayName = 'JotForm API';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'API Domain',
name: 'apiDomain',
type: 'options' as NodePropertyTypes,
options: [
{
name: 'api.jotform.com',
value: 'api.jotform.com',
},
{
name: 'eu-api.jotform.com',
value: 'eu-api.jotform.com',
},
],
default: 'api.jotform.com',
description: 'The API domain to use. Use "eu-api.jotform.com" if your account is in based in Europe.',
},
];
}

View File

@ -0,0 +1,23 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MailjetEmailApi implements ICredentialType {
name = 'mailjetEmailApi';
displayName = 'Mailjet Email API';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Secret Key',
name: 'secretKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MailjetSmsApi implements ICredentialType {
name = 'mailjetSmsApi';
displayName = 'Mailjet SMS API';
properties = [
{
displayName: 'Token',
name: 'token',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,33 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MauticApi implements ICredentialType {
name = 'mauticApi';
displayName = 'Mautic API';
properties = [
{
displayName: 'URL',
name: 'url',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'https://name.mautic.net',
},
{
displayName: 'Username',
name: 'username',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
default: '',
},
];
}

View File

@ -0,0 +1,27 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MoceanApi implements ICredentialType {
name = 'moceanApi';
displayName = 'Mocean Api';
properties = [
// The credentials to get from user and save encrypted.
// Properties can be defined exactly in the same way
// as node properties.
{
displayName: 'API Key',
name: 'mocean-api-key',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'API Secret',
name: 'mocean-api-secret',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MondayComApi implements ICredentialType {
name = 'mondayComApi';
displayName = 'Monday.com API';
properties = [
{
displayName: 'Token V2',
name: 'apiToken',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,19 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class Msg91Api implements ICredentialType {
name = 'msg91Api';
displayName = 'Msg91 Api';
properties = [
// User authentication key
{
displayName: 'Authentication Key',
name: 'authkey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -35,6 +35,34 @@ export class Postgres implements ICredentialType {
},
default: '',
},
{
displayName: 'SSL',
name: 'ssl',
type: 'options' as NodePropertyTypes,
options: [
{
name: 'disable',
value: 'disable',
},
{
name: 'allow',
value: 'allow',
},
{
name: 'require',
value: 'require',
},
{
name: 'verify (not implemented)',
value: 'verify',
},
{
name: 'verify-full (not implemented)',
value: 'verify-full',
}
],
default: 'disable',
},
{
displayName: 'Port',
name: 'port',

View File

@ -0,0 +1,25 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class RundeckApi implements ICredentialType {
name = 'rundeckApi';
displayName = 'Rundeck API';
properties = [
{
displayName: 'Url',
name: 'url',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'http://127.0.0.1:4440',
},
{
displayName: 'Token',
name: 'token',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@ -0,0 +1,24 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class SalesmateApi implements ICredentialType {
name = 'salesmateApi';
displayName = 'Salesmate API';
properties = [
{
displayName: 'Session Token',
name: 'sessionToken',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'URL',
name: 'url',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'n8n.salesmate.io',
},
];
}

View File

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class SegmentApi implements ICredentialType {
name = 'segmentApi';
displayName = 'Segment API';
properties = [
{
displayName: 'Write Key',
name: 'writekey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

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