mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 12:35:46 +03:00
🔀 Merge branch 'master' into oauth-support
This commit is contained in:
commit
9dd9e0d8ba
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,3 +11,4 @@ google-generated-credentials.json
|
||||
_START_PACKAGE
|
||||
.env
|
||||
.vscode
|
||||
.idea
|
||||
|
16
README.md
16
README.md
@ -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)
|
||||
|
@ -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"]
|
||||
|
15
docker/images/n8n-ubuntu/docker-entrypoint.sh
Executable file
15
docker/images/n8n-ubuntu/docker-entrypoint.sh
Executable 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
|
@ -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"]
|
||||
|
@ -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)
|
||||
|
15
docker/images/n8n/docker-entrypoint.sh
Executable file
15
docker/images/n8n/docker-entrypoint.sh
Executable 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
|
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
49
docs/faq.md
49
docs/faq.md
@ -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.
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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!,
|
||||
};
|
||||
|
||||
|
@ -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 = '';
|
||||
|
@ -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',
|
||||
});
|
||||
|
@ -9,6 +9,6 @@
|
||||
"index.ts",
|
||||
"src"
|
||||
],
|
||||
"exec": "npm run build && npm start",
|
||||
"exec": "npm start",
|
||||
"ext": "ts"
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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, {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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}`);
|
||||
|
@ -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 {
|
||||
|
@ -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.'), {});
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
44
packages/cli/src/databases/mysqldb/CredentialsEntity.ts
Normal file
44
packages/cli/src/databases/mysqldb/CredentialsEntity.ts
Normal 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;
|
||||
}
|
51
packages/cli/src/databases/mysqldb/ExecutionEntity.ts
Normal file
51
packages/cli/src/databases/mysqldb/ExecutionEntity.ts
Normal 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;
|
||||
}
|
55
packages/cli/src/databases/mysqldb/WorkflowEntity.ts
Normal file
55
packages/cli/src/databases/mysqldb/WorkflowEntity.ts
Normal 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;
|
||||
}
|
3
packages/cli/src/databases/mysqldb/index.ts
Normal file
3
packages/cli/src/databases/mysqldb/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './CredentialsEntity';
|
||||
export * from './ExecutionEntity';
|
||||
export * from './WorkflowEntity';
|
@ -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": {
|
||||
|
@ -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;
|
||||
// }
|
||||
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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[];
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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) {
|
||||
|
@ -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[] = [];
|
||||
|
@ -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",
|
||||
|
88
packages/editor-ui/src/components/About.vue
Normal file
88
packages/editor-ui/src/components/About.vue
Normal 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>
|
@ -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;
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ export default Vue.extend({
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
/deep/ .expression-dialog {
|
||||
::v-deep .expression-dialog {
|
||||
.el-dialog__header {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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/, '|');
|
||||
|
@ -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',
|
||||
|
@ -47,6 +47,9 @@ export default Vue.extend({
|
||||
return false;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.tempValue = this.value as string;
|
||||
},
|
||||
watch: {
|
||||
dialogVisible () {
|
||||
if (this.dialogVisible === true) {
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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 () {
|
||||
|
@ -12,7 +12,6 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
configureWebpack: {
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
new GoogleFontsPlugin({
|
||||
fonts: [
|
||||
|
@ -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",
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
}
|
17
packages/nodes-base/credentials/AffinityApi.credentials.ts
Normal file
17
packages/nodes-base/credentials/AffinityApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
17
packages/nodes-base/credentials/BitlyApi.credentials.ts
Normal file
17
packages/nodes-base/credentials/BitlyApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
17
packages/nodes-base/credentials/CalendlyApi.credentials.ts
Normal file
17
packages/nodes-base/credentials/CalendlyApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
17
packages/nodes-base/credentials/ClearbitApi.credentials.ts
Normal file
17
packages/nodes-base/credentials/ClearbitApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
17
packages/nodes-base/credentials/ClickUpApi.credentials.ts
Normal file
17
packages/nodes-base/credentials/ClickUpApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
21
packages/nodes-base/credentials/ClockifyApi.credentials.ts
Normal file
21
packages/nodes-base/credentials/ClockifyApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
25
packages/nodes-base/credentials/CopperApi.credentials.ts
Normal file
25
packages/nodes-base/credentials/CopperApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
18
packages/nodes-base/credentials/DisqusApi.credentials.ts
Normal file
18
packages/nodes-base/credentials/DisqusApi.credentials.ts
Normal 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>.'
|
||||
},
|
||||
];
|
||||
}
|
18
packages/nodes-base/credentials/DriftApi.credentials.ts
Normal file
18
packages/nodes-base/credentials/DriftApi.credentials.ts
Normal 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>.'
|
||||
},
|
||||
];
|
||||
}
|
@ -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: ''
|
||||
}
|
||||
];
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
17
packages/nodes-base/credentials/GumroadApi.credentials.ts
Normal file
17
packages/nodes-base/credentials/GumroadApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
25
packages/nodes-base/credentials/HarvestApi.credentials.ts
Normal file
25
packages/nodes-base/credentials/HarvestApi.credentials.ts
Normal 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>.'
|
||||
},
|
||||
];
|
||||
}
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
}
|
17
packages/nodes-base/credentials/HunterApi.credentials.ts
Normal file
17
packages/nodes-base/credentials/HunterApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
}
|
@ -24,6 +24,7 @@ export class JiraSoftwareCloudApi implements ICredentialType {
|
||||
name: 'domain',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
placeholder: 'https://example.atlassian.net',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
34
packages/nodes-base/credentials/JotFormApi.credentials.ts
Normal file
34
packages/nodes-base/credentials/JotFormApi.credentials.ts
Normal 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.',
|
||||
},
|
||||
];
|
||||
}
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
}
|
17
packages/nodes-base/credentials/MailjetSmsApi.credentials.ts
Normal file
17
packages/nodes-base/credentials/MailjetSmsApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
33
packages/nodes-base/credentials/MauticApi.credentials.ts
Normal file
33
packages/nodes-base/credentials/MauticApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
27
packages/nodes-base/credentials/MoceanApi.credentials.ts
Normal file
27
packages/nodes-base/credentials/MoceanApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
17
packages/nodes-base/credentials/MondayComApi.credentials.ts
Normal file
17
packages/nodes-base/credentials/MondayComApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
19
packages/nodes-base/credentials/Msg91Api.credentials.ts
Normal file
19
packages/nodes-base/credentials/Msg91Api.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
@ -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',
|
||||
|
25
packages/nodes-base/credentials/RundeckApi.credentials.ts
Normal file
25
packages/nodes-base/credentials/RundeckApi.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
24
packages/nodes-base/credentials/SalesmateApi.credentials.ts
Normal file
24
packages/nodes-base/credentials/SalesmateApi.credentials.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
}
|
17
packages/nodes-base/credentials/SegmentApi.credentials.ts
Normal file
17
packages/nodes-base/credentials/SegmentApi.credentials.ts
Normal 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
Loading…
Reference in New Issue
Block a user