🔀 Merge branch 'master' into WorkflowExecutionMaintainance

This commit is contained in:
Jan Oberhauser 2020-07-12 12:20:59 +02:00
commit 78dc1e9449
33 changed files with 1209 additions and 262 deletions

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ _START_PACKAGE
.env .env
.vscode .vscode
.idea .idea
.prettierrc.js

View file

@ -0,0 +1,25 @@
# Folder where data should be saved
DATA_FOLDER=/root/n8n/
# The top level domain to serve from
DOMAIN_NAME=example.com
# The subfolder to serve from
SUBFOLDER=app1
N8N_PATH=/app1/
# DOMAIN_NAME and SUBDOMAIN combined decide where n8n will be reachable from
# above example would result in: https://example.com/n8n/
# The user name to use for autentication - IMPORTANT ALWAYS CHANGE!
N8N_BASIC_AUTH_USER=user
# The password to use for autentication - IMPORTANT ALWAYS CHANGE!
N8N_BASIC_AUTH_PASSWORD=password
# Optional timezone to set which gets used by Cron-Node by default
# If not set New York time will be used
GENERIC_TIMEZONE=Europe/Berlin
# The email address to use for the SSL certificate creation
SSL_EMAIL=user@example.com

View file

@ -0,0 +1,26 @@
# n8n on Subfolder with SSL
Starts n8n and deployes it on a subfolder
## Start
To start n8n in a subfolder simply start docker-compose by executing the following
command in the current folder.
**IMPORTANT:** But before you do that change the default users and passwords in the `.env` file!
```
docker-compose up -d
```
To stop it execute:
```
docker-compose stop
```
## Configuration
The default name of the database, user and password for MongoDB can be changed in the `.env` file in the current directory.

View file

@ -0,0 +1,57 @@
version: "3"
services:
traefik:
image: "traefik"
command:
- "--api=true"
- "--api.insecure=true"
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true"
- "--certificatesresolvers.mytlschallenge.acme.email=${SSL_EMAIL}"
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
- /home/jan/www/n8n/n8n:/data
ports:
- "443:443"
- "80:80"
volumes:
- ${DATA_FOLDER}/letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
n8n:
image: n8nio/n8n
ports:
- "127.0.0.1:5678:5678"
labels:
- traefik.enable=true
- traefik.http.routers.n8n.rule=Host(`${DOMAIN_NAME}`)
- traefik.http.routers.n8n.tls=true
- traefik.http.routers.n8n.entrypoints=websecure
- "traefik.http.routers.n8n.rule=PathPrefix(`/${SUBFOLDER}{regex:$$|/.*}`)"
- "traefik.http.middlewares.n8n-stripprefix.stripprefix.prefixes=/${SUBFOLDER}"
- "traefik.http.routers.n8n.middlewares=n8n-stripprefix"
- traefik.http.routers.n8n.tls.certresolver=mytlschallenge
- traefik.http.middlewares.n8n.headers.SSLRedirect=true
- traefik.http.middlewares.n8n.headers.STSSeconds=315360000
- traefik.http.middlewares.n8n.headers.browserXSSFilter=true
- traefik.http.middlewares.n8n.headers.contentTypeNosniff=true
- traefik.http.middlewares.n8n.headers.forceSTSHeader=true
- traefik.http.middlewares.n8n.headers.SSLHost=${DOMAIN_NAME}
- traefik.http.middlewares.n8n.headers.STSIncludeSubdomains=true
- traefik.http.middlewares.n8n.headers.STSPreload=true
environment:
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER
- N8N_BASIC_AUTH_PASSWORD
- N8N_HOST=${DOMAIN_NAME}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- NODE_ENV=production
- N8N_PATH
- WEBHOOK_TUNNEL_URL=http://${DOMAIN_NAME}${N8N_PATH}
- VUE_APP_URL_BASE_API=http://${DOMAIN_NAME}${N8N_PATH}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${DATA_FOLDER}/.n8n:/root/.n8n

View file

@ -6,25 +6,23 @@ n8n is a free and open [fair-code](http://faircode.io) licensed node based Workf
<a href="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png"><img src="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png" width="550" alt="n8n.io - Screenshot"></a> <a href="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png"><img src="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png" width="550" alt="n8n.io - Screenshot"></a>
## Contents ## Contents
- [Demo](#demo) - [Demo](#demo)
- [Available integrations](#available-integrations) - [Available integrations](#available-integrations)
- [Documentation](#documentation) - [Documentation](#documentation)
- [Start n8n in Docker](#start-n8n-in-docker) - [Start n8n in Docker](#start-n8n-in-docker)
- [Start with tunnel](#start-with-tunnel) - [Start with tunnel](#start-with-tunnel)
- [Securing n8n](#securing-n8n) - [Securing n8n](#securing-n8n)
- [Persist data](#persist-data) - [Persist data](#persist-data)
- [Passing Sensitive Data via File](#passing-sensitive-data-via-file) - [Passing Sensitive Data via File](#passing-sensitive-data-via-file)
- [Updating a Running docker-compose Instance](#updating-a-running-docker-compose-instance) - [Updating a Running docker-compose Instance](#updating-a-running-docker-compose-instance)
- [Example Setup with Lets Encrypt](#example-setup-with-lets-encrypt) - [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) - [What does n8n mean and how do you pronounce it](#what-does-n8n-mean-and-how-do-you-pronounce-it)
- [Support](#support) - [Support](#support)
- [Jobs](#jobs) - [Jobs](#jobs)
- [Upgrading](#upgrading) - [Upgrading](#upgrading)
- [License](#license) - [License](#license)
## Demo ## Demo
@ -49,9 +47,9 @@ Additional information and example workflows on the n8n.io website: [https://n8n
``` ```
docker run -it --rm \ docker run -it --rm \
--name n8n \ --name n8n \
-p 5678:5678 \ -p 5678:5678 \
n8nio/n8n n8nio/n8n
``` ```
You can then access n8n by opening: You can then access n8n by opening:
@ -71,14 +69,13 @@ To use it simply start n8n with `--tunnel`
``` ```
docker run -it --rm \ docker run -it --rm \
--name n8n \ --name n8n \
-p 5678:5678 \ -p 5678:5678 \
-v ~/.n8n:/root/.n8n \ -v ~/.n8n:/root/.n8n \
n8nio/n8n \ n8nio/n8n \
n8n start --tunnel n8n start --tunnel
``` ```
## Securing n8n ## Securing n8n
By default n8n can be accessed by everybody. This is OK if you have it only running By default n8n can be accessed by everybody. This is OK if you have it only running
@ -93,7 +90,6 @@ N8N_BASIC_AUTH_USER=<USER>
N8N_BASIC_AUTH_PASSWORD=<PASSWORD> N8N_BASIC_AUTH_PASSWORD=<PASSWORD>
``` ```
## Persist data ## Persist data
The workflow data gets by default saved in an SQLite database in the user The workflow data gets by default saved in an SQLite database in the user
@ -102,10 +98,10 @@ settings like webhook URL and encryption key.
``` ```
docker run -it --rm \ docker run -it --rm \
--name n8n \ --name n8n \
-p 5678:5678 \ -p 5678:5678 \
-v ~/.n8n:/root/.n8n \ -v ~/.n8n:/root/.n8n \
n8nio/n8n n8nio/n8n
``` ```
### Start with other Database ### Start with other Database
@ -121,7 +117,6 @@ for the credentials. If none gets found n8n creates automatically one on
startup. In case credentials are already saved with a different encryption key startup. In case credentials are already saved with a different encryption key
it can not be used anymore as encrypting it is not possible anymore. it can not be used anymore as encrypting it is not possible anymore.
#### Use with MongoDB #### Use with MongoDB
> **WARNING**: Use Postgres if possible! Mongo has problems with saving large > **WARNING**: Use Postgres if possible! Mongo has problems with saving large
@ -129,40 +124,39 @@ it can not be used anymore as encrypting it is not possible anymore.
> may be dropped in the future. > may be dropped in the future.
Replace the following placeholders with the actual data: Replace the following placeholders with the actual data:
- <MONGO_DATABASE> - MONGO_DATABASE
- <MONGO_HOST> - MONGO_HOST
- <MONGO_PORT> - MONGO_PORT
- <MONGO_USER> - MONGO_USER
- <MONGO_PASSWORD> - MONGO_PASSWORD
``` ```
docker run -it --rm \ docker run -it --rm \
--name n8n \ --name n8n \
-p 5678:5678 \ -p 5678:5678 \
-e DB_TYPE=mongodb \ -e DB_TYPE=mongodb \
-e DB_MONGODB_CONNECTION_URL="mongodb://<MONGO_USER>:<MONGO_PASSWORD>@<MONGO_SERVER>:<MONGO_PORT>/<MONGO_DATABASE>" \ -e DB_MONGODB_CONNECTION_URL="mongodb://<MONGO_USER>:<MONGO_PASSWORD>@<MONGO_SERVER>:<MONGO_PORT>/<MONGO_DATABASE>" \
-v ~/.n8n:/root/.n8n \ -v ~/.n8n:/root/.n8n \
n8nio/n8n \ n8nio/n8n \
n8n start n8n start
``` ```
A full working setup with docker-compose can be found [here](https://github.com/n8n-io/n8n/blob/master/docker/compose/withMongo/README.md) A full working setup with docker-compose can be found [here](https://github.com/n8n-io/n8n/blob/master/docker/compose/withMongo/README.md)
#### Use with PostgresDB #### Use with PostgresDB
Replace the following placeholders with the actual data: Replace the following placeholders with the actual data:
- <POSTGRES_DATABASE> - POSTGRES_DATABASE
- <POSTGRES_HOST> - POSTGRES_HOST
- <POSTGRES_PASSWORD> - POSTGRES_PASSWORD
- <POSTGRES_PORT> - POSTGRES_PORT
- <POSTGRES_USER> - POSTGRES_USER
- <POSTGRES_SCHEMA> - POSTGRES_SCHEMA
``` ```
docker run -it --rm \ docker run -it --rm \
--name n8n \ --name n8n \
-p 5678:5678 \ -p 5678:5678 \
-e DB_TYPE=postgresdb \ -e DB_TYPE=postgresdb \
-e DB_POSTGRESDB_DATABASE=<POSTGRES_DATABASE> \ -e DB_POSTGRESDB_DATABASE=<POSTGRES_DATABASE> \
-e DB_POSTGRESDB_HOST=<POSTGRES_HOST> \ -e DB_POSTGRESDB_HOST=<POSTGRES_HOST> \
@ -170,39 +164,37 @@ docker run -it --rm \
-e DB_POSTGRESDB_USER=<POSTGRES_USER> \ -e DB_POSTGRESDB_USER=<POSTGRES_USER> \
-e DB_POSTGRESDB_SCHEMA=<POSTGRES_SCHEMA> \ -e DB_POSTGRESDB_SCHEMA=<POSTGRES_SCHEMA> \
-e DB_POSTGRESDB_PASSWORD=<POSTGRES_PASSWORD> \ -e DB_POSTGRESDB_PASSWORD=<POSTGRES_PASSWORD> \
-v ~/.n8n:/root/.n8n \ -v ~/.n8n:/root/.n8n \
n8nio/n8n \ n8nio/n8n \
n8n start n8n start
``` ```
A full working setup with docker-compose can be found [here](https://github.com/n8n-io/n8n/blob/master/docker/compose/withPostgres/README.md) 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 #### Use with MySQL
Replace the following placeholders with the actual data: Replace the following placeholders with the actual data:
- <MYSQLDB_DATABASE> - MYSQLDB_DATABASE
- <MYSQLDB_HOST> - MYSQLDB_HOST
- <MYSQLDB_PASSWORD> - MYSQLDB_PASSWORD
- <MYSQLDB_PORT> - MYSQLDB_PORT
- <MYSQLDB_USER> - MYSQLDB_USER
``` ```
docker run -it --rm \ docker run -it --rm \
--name n8n \ --name n8n \
-p 5678:5678 \ -p 5678:5678 \
-e DB_TYPE=mysqldb \ -e DB_TYPE=mysqldb \
-e DB_MYSQLDB_DATABASE=<MYSQLDB_DATABASE> \ -e DB_MYSQLDB_DATABASE=<MYSQLDB_DATABASE> \
-e DB_MYSQLDB_HOST=<MYSQLDB_HOST> \ -e DB_MYSQLDB_HOST=<MYSQLDB_HOST> \
-e DB_MYSQLDB_PORT=<MYSQLDB_PORT> \ -e DB_MYSQLDB_PORT=<MYSQLDB_PORT> \
-e DB_MYSQLDB_USER=<MYSQLDB_USER> \ -e DB_MYSQLDB_USER=<MYSQLDB_USER> \
-e DB_MYSQLDB_PASSWORD=<MYSQLDB_PASSWORD> \ -e DB_MYSQLDB_PASSWORD=<MYSQLDB_PASSWORD> \
-v ~/.n8n:/root/.n8n \ -v ~/.n8n:/root/.n8n \
n8nio/n8n \ n8nio/n8n \
n8n start n8n start
``` ```
## Passing Sensitive Data via File ## Passing Sensitive Data via File
To avoid passing sensitive information via environment variables "_FILE" may be To avoid passing sensitive information via environment variables "_FILE" may be
@ -211,16 +203,15 @@ with the given name. That makes it possible to load data easily from
Docker- and Kubernetes-Secrets. Docker- and Kubernetes-Secrets.
The following environment variables support file input: The following environment variables support file input:
- DB_MONGODB_CONNECTION_URL_FILE - DB_MONGODB_CONNECTION_URL_FILE
- DB_POSTGRESDB_DATABASE_FILE - DB_POSTGRESDB_DATABASE_FILE
- DB_POSTGRESDB_HOST_FILE - DB_POSTGRESDB_HOST_FILE
- DB_POSTGRESDB_PASSWORD_FILE - DB_POSTGRESDB_PASSWORD_FILE
- DB_POSTGRESDB_PORT_FILE - DB_POSTGRESDB_PORT_FILE
- DB_POSTGRESDB_USER_FILE - DB_POSTGRESDB_USER_FILE
- DB_POSTGRESDB_SCHEMA_FILE - DB_POSTGRESDB_SCHEMA_FILE
- N8N_BASIC_AUTH_PASSWORD_FILE - N8N_BASIC_AUTH_PASSWORD_FILE
- N8N_BASIC_AUTH_USER_FILE - N8N_BASIC_AUTH_USER_FILE
## Example Setup with Lets Encrypt ## Example Setup with Lets Encrypt
@ -235,7 +226,7 @@ docker pull n8nio/n8n
# Stop current setup # Stop current setup
sudo docker-compose stop sudo docker-compose stop
# Delete it (will only delete the docker-containers, data is stored separately) # Delete it (will only delete the docker-containers, data is stored separately)
sudo docker-compose rm sudo docker-compose rm
# Then start it again # Then start it again
sudo docker-compose up -d sudo docker-compose up -d
``` ```
@ -251,11 +242,11 @@ the environment variable `TZ`.
Example to use the same timezone for both: Example to use the same timezone for both:
``` ```
docker run -it --rm \ docker run -it --rm \
--name n8n \ --name n8n \
-p 5678:5678 \ -p 5678:5678 \
-e GENERIC_TIMEZONE="Europe/Berlin" \ -e GENERIC_TIMEZONE="Europe/Berlin" \
-e TZ="Europe/Berlin" \ -e TZ="Europe/Berlin" \
n8nio/n8n n8nio/n8n
``` ```

View file

@ -225,6 +225,13 @@ const config = convict({
}, },
// How n8n can be reached (Editor & REST-API) // How n8n can be reached (Editor & REST-API)
path: {
format: String,
default: '/',
arg: 'path',
env: 'N8N_PATH',
doc: 'Path n8n is deployed to'
},
host: { host: {
format: String, format: String,
default: 'localhost', default: 'localhost',

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.73.0", "version": "0.73.1",
"description": "n8n Workflow Automation Tool", "description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -102,7 +102,7 @@
"mysql2": "^2.0.1", "mysql2": "^2.0.1",
"n8n-core": "~0.38.0", "n8n-core": "~0.38.0",
"n8n-editor-ui": "~0.49.0", "n8n-editor-ui": "~0.49.0",
"n8n-nodes-base": "~0.68.0", "n8n-nodes-base": "~0.68.1",
"n8n-workflow": "~0.34.0", "n8n-workflow": "~0.34.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"open": "^7.0.0", "open": "^7.0.0",

View file

@ -40,11 +40,12 @@ export function getBaseUrl(): string {
const protocol = config.get('protocol') as string; const protocol = config.get('protocol') as string;
const host = config.get('host') as string; const host = config.get('host') as string;
const port = config.get('port') as number; const port = config.get('port') as number;
const path = config.get('path') as string;
if (protocol === 'http' && port === 80 || protocol === 'https' && port === 443) { if (protocol === 'http' && port === 80 || protocol === 'https' && port === 443) {
return `${protocol}://${host}/`; return `${protocol}://${host}${path}`;
} }
return `${protocol}://${host}:${port}/`; return `${protocol}://${host}:${port}${path}`;
} }

View file

@ -931,7 +931,8 @@ class App {
// Authorize OAuth Data // Authorize OAuth Data
this.app.get(`/${this.restEndpoint}/oauth1-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => { this.app.get(`/${this.restEndpoint}/oauth1-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
if (req.query.id === undefined) { if (req.query.id === undefined) {
throw new Error('Required credential id is missing!'); res.status(500).send('Required credential id is missing!');
return '';
} }
const result = await Db.collections.Credentials!.findOne(req.query.id as string); const result = await Db.collections.Credentials!.findOne(req.query.id as string);
@ -943,7 +944,8 @@ class App {
let encryptionKey = undefined; let encryptionKey = undefined;
encryptionKey = await UserSettings.getEncryptionKey(); encryptionKey = await UserSettings.getEncryptionKey();
if (encryptionKey === undefined) { if (encryptionKey === undefined) {
throw new Error('No encryption key got found to decrypt the credentials!'); res.status(500).send('No encryption key got found to decrypt the credentials!');
return '';
} }
// Decrypt the currently saved credentials // Decrypt the currently saved credentials
@ -1015,7 +1017,8 @@ class App {
const { oauth_verifier, oauth_token, cid } = req.query; const { oauth_verifier, oauth_token, cid } = req.query;
if (oauth_verifier === undefined || oauth_token === undefined) { if (oauth_verifier === undefined || oauth_token === undefined) {
throw new Error('Insufficient parameters for OAuth1 callback'); const errorResponse = new ResponseHelper.ResponseError('Insufficient parameters for OAuth1 callback. Received following query parameters: ' + JSON.stringify(req.query), undefined, 503);
return ResponseHelper.sendErrorResponse(res, errorResponse);
} }
const result = await Db.collections.Credentials!.findOne(cid as any); // tslint:disable-line:no-any const result = await Db.collections.Credentials!.findOne(cid as any); // tslint:disable-line:no-any
@ -1085,7 +1088,8 @@ class App {
// Authorize OAuth Data // Authorize OAuth Data
this.app.get(`/${this.restEndpoint}/oauth2-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => { this.app.get(`/${this.restEndpoint}/oauth2-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
if (req.query.id === undefined) { if (req.query.id === undefined) {
throw new Error('Required credential id is missing!'); res.status(500).send('Required credential id is missing.');
return '';
} }
const result = await Db.collections.Credentials!.findOne(req.query.id as string); const result = await Db.collections.Credentials!.findOne(req.query.id as string);
@ -1097,7 +1101,8 @@ class App {
let encryptionKey = undefined; let encryptionKey = undefined;
encryptionKey = await UserSettings.getEncryptionKey(); encryptionKey = await UserSettings.getEncryptionKey();
if (encryptionKey === undefined) { if (encryptionKey === undefined) {
throw new Error('No encryption key got found to decrypt the credentials!'); res.status(500).send('No encryption key got found to decrypt the credentials!');
return '';
} }
// Decrypt the currently saved credentials // Decrypt the currently saved credentials
@ -1161,7 +1166,8 @@ class App {
const {code, state: stateEncoded } = req.query; const {code, state: stateEncoded } = req.query;
if (code === undefined || stateEncoded === undefined) { if (code === undefined || stateEncoded === undefined) {
throw new Error('Insufficient parameters for OAuth2 callback'); const errorResponse = new ResponseHelper.ResponseError('Insufficient parameters for OAuth2 callback. Received following query parameters: ' + JSON.stringify(req.query), undefined, 503);
return ResponseHelper.sendErrorResponse(res, errorResponse);
} }
let state; let state;
@ -1211,17 +1217,20 @@ class App {
}, },
}; };
} }
const redirectUri = `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`;
const oAuthObj = new clientOAuth2({ const oAuthObj = new clientOAuth2({
clientId: _.get(oauthCredentials, 'clientId') as string, clientId: _.get(oauthCredentials, 'clientId') as string,
clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string,
accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string, accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string,
authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string, authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string,
redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`, redirectUri,
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',') scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',')
}); });
const oauthToken = await oAuthObj.code.getToken(req.originalUrl, options); const queryParameters = req.originalUrl.split('?').splice(1, 1).join('');
const oauthToken = await oAuthObj.code.getToken(`${redirectUri}?${queryParameters}`, options);
if (oauthToken === undefined) { if (oauthToken === undefined) {
const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404);
@ -1693,9 +1702,21 @@ class App {
}); });
} }
// Read the index file and replace the path placeholder
const editorUiPath = require.resolve('n8n-editor-ui');
const filePath = pathJoin(pathDirname(editorUiPath), 'dist', 'index.html');
let readIndexFile = readFileSync(filePath, 'utf8');
const n8nPath = config.get('path');
readIndexFile = readIndexFile.replace(/\/%BASE_PATH%\//g, n8nPath);
// Serve the altered index.html file separately
this.app.get(`/index.html`, async (req: express.Request, res: express.Response) => {
res.send(readIndexFile);
});
// Serve the website // Serve the website
const startTime = (new Date()).toUTCString(); const startTime = (new Date()).toUTCString();
const editorUiPath = require.resolve('n8n-editor-ui');
this.app.use('/', express.static(pathJoin(pathDirname(editorUiPath), 'dist'), { this.app.use('/', express.static(pathJoin(pathDirname(editorUiPath), 'dist'), {
index: 'index.html', index: 'index.html',
setHeaders: (res, path) => { setHeaders: (res, path) => {

View file

@ -4,7 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="/%BASE_PATH%/favicon.ico">
<script type="text/javascript">window.BASE_PATH = "/%BASE_PATH%/";</script>
<title>n8n.io - Workflow Automation</title> <title>n8n.io - Workflow Automation</title>
</head> </head>
<body> <body>

View file

@ -16,7 +16,7 @@
<el-menu-item index="logo" class="logo-item"> <el-menu-item index="logo" class="logo-item">
<a href="https://n8n.io" target="_blank" class="logo"> <a href="https://n8n.io" target="_blank" class="logo">
<img src="/n8n-icon-small.png" class="icon" alt="n8n.io"/> <img :src="basePath + 'n8n-icon-small.png'" class="icon" alt="n8n.io"/>
<span class="logo-text" slot="title">n8n.io</span> <span class="logo-text" slot="title">n8n.io</span>
</a> </a>
</el-menu-item> </el-menu-item>
@ -208,6 +208,8 @@ export default mixins(
data () { data () {
return { return {
aboutDialogVisible: false, aboutDialogVisible: false,
// @ts-ignore
basePath: window.BASE_PATH,
isCollapsed: true, isCollapsed: true,
credentialNewDialogVisible: false, credentialNewDialogVisible: false,
credentialOpenDialogVisible: false, credentialOpenDialogVisible: false,

View file

@ -19,7 +19,7 @@
<div class="header"> <div class="header">
<div class="title-text"> <div class="title-text">
<strong v-if="dataCount < this.MAX_DISPLAY_ITEMS_AUTO_ALL && dataSize < MAX_DISPLAY_DATA_SIZE"> <strong v-if="dataCount < maxDisplayItems && dataSize < MAX_DISPLAY_DATA_SIZE">
Results: {{ dataCount }} Results: {{ dataCount }}
</strong> </strong>
<strong v-else>Results: <strong v-else>Results:
@ -248,7 +248,11 @@ export default mixins(
return executionData.resultData.runData; return executionData.resultData.runData;
}, },
maxDisplayItemsOptions (): number[] { maxDisplayItemsOptions (): number[] {
return [25, 50, 100, 250, 500, 1000, this.dataCount].filter(option => option <= this.dataCount); const options = [25, 50, 100, 250, 500, 1000].filter(option => option <= this.dataCount);
if (!options.includes(this.dataCount)) {
options.push(this.dataCount);
}
return options;
}, },
node (): INodeUi | null { node (): INodeUi | null {
return this.$store.getters.activeNode; return this.$store.getters.activeNode;

View file

@ -8,7 +8,8 @@ Vue.use(Router);
export default new Router({ export default new Router({
mode: 'history', mode: 'history',
base: process.env.BASE_URL, // @ts-ignore
base: window.BASE_PATH,
routes: [ routes: [
{ {
path: '/execution/:id', path: '/execution/:id',

View file

@ -38,7 +38,8 @@ export const store = new Vuex.Store({
activeWorkflows: [] as string[], activeWorkflows: [] as string[],
activeActions: [] as string[], activeActions: [] as string[],
activeNode: null as string | null, activeNode: null as string | null,
baseUrl: process.env.VUE_APP_URL_BASE_API ? process.env.VUE_APP_URL_BASE_API : '/', // @ts-ignore
baseUrl: window.BASE_PATH ? window.BASE_PATH : '/',
credentials: null as ICredentialsResponse[] | null, credentials: null as ICredentialsResponse[] | null,
credentialTypes: null as ICredentialType[] | null, credentialTypes: null as ICredentialType[] | null,
endpointWebhook: 'webhook', endpointWebhook: 'webhook',

View file

@ -29,5 +29,5 @@ module.exports = {
}, },
}, },
}, },
publicPath: process.env.VUE_APP_PUBLIC_PATH ? process.env.VUE_APP_PUBLIC_PATH : '/', publicPath: process.env.VUE_APP_PUBLIC_PATH ? process.env.VUE_APP_PUBLIC_PATH : '/%BASE_PATH%/',
}; };

View file

@ -0,0 +1,69 @@
import { ICredentialType, NodePropertyTypes } from 'n8n-workflow';
export class QuestDb implements ICredentialType {
name = 'questDb';
displayName = 'QuestDB';
properties = [
{
displayName: 'Host',
name: 'host',
type: 'string' as NodePropertyTypes,
default: 'localhost',
},
{
displayName: 'Database',
name: 'database',
type: 'string' as NodePropertyTypes,
default: 'qdb',
},
{
displayName: 'User',
name: 'user',
type: 'string' as NodePropertyTypes,
default: 'admin',
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
default: 'quest',
},
{
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',
type: 'number' as NodePropertyTypes,
default: 8812,
},
];
}

View file

@ -5,7 +5,7 @@ export class ZoomApi implements ICredentialType {
displayName = 'Zoom API'; displayName = 'Zoom API';
properties = [ properties = [
{ {
displayName: 'JTW Token', displayName: 'JWT Token',
name: 'accessToken', name: 'accessToken',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '' default: ''

View file

@ -52,10 +52,10 @@ export class AffinityTrigger implements INodeType {
options: [ options: [
{ {
name: 'file.created', name: 'file.created',
value: 'file.deleted', value: 'file.created',
}, },
{ {
name: 'file.created', name: 'file.deleted',
value: 'file.deleted', value: 'file.deleted',
}, },
{ {
@ -136,7 +136,7 @@ export class AffinityTrigger implements INodeType {
}, },
{ {
name: 'opportunity.deleted', name: 'opportunity.deleted',
value: 'organization.deleted', value: 'opportunity.deleted',
}, },
{ {
name: 'person.created', name: 'person.created',

View file

@ -802,7 +802,7 @@ export class HttpRequest implements INodeType {
if (oAuth2Api !== undefined) { if (oAuth2Api !== undefined) {
//@ts-ignore //@ts-ignore
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions); response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, 'Bearer');
} else { } else {
response = await this.helpers.request(requestOptions); response = await this.helpers.request(requestOptions);
} }

View file

@ -41,12 +41,12 @@ export const issueOperations = [
{ {
name: 'Notify', name: 'Notify',
value: 'notify', value: 'notify',
description: 'Creates an email notification for an issue and adds it to the mail queue.', description: 'Create an email notification for an issue and add it to the mail queue',
}, },
{ {
name: 'Status', name: 'Status',
value: 'transitions', value: 'transitions',
description: `Returns either all transitions or a transition that can be performed by the user on an issue, based on the issue's status.`, description: `Return either all transitions or a transition that can be performed by the user on an issue, based on the issue's status`,
}, },
{ {
name: 'Delete', name: 'Delete',

View file

@ -49,7 +49,6 @@ export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctio
const datacenter = (credentials.apiKey as string).split('-').pop(); const datacenter = (credentials.apiKey as string).split('-').pop();
options.url = `https://${datacenter}.${host}${endpoint}`; options.url = `https://${datacenter}.${host}${endpoint}`;
return await this.helpers.request!(options); return await this.helpers.request!(options);
} else { } else {
const credentials = this.getCredentials('mailchimpOAuth2Api') as IDataObject; const credentials = this.getCredentials('mailchimpOAuth2Api') as IDataObject;

View file

@ -47,6 +47,7 @@ interface ICreateMemberBody {
timestamp_opt?: string; timestamp_opt?: string;
tags?: string[]; tags?: string[];
merge_fields?: IDataObject; merge_fields?: IDataObject;
interests?: IDataObject;
} }
export class Mailchimp implements INodeType { export class Mailchimp implements INodeType {
@ -112,6 +113,10 @@ export class Mailchimp implements INodeType {
name: 'resource', name: 'resource',
type: 'options', type: 'options',
options: [ options: [
{
name: 'List Group',
value: 'listGroup',
},
{ {
name: 'Member', name: 'Member',
value: 'member', value: 'member',
@ -194,6 +199,28 @@ export class Mailchimp implements INodeType {
default: 'create', default: 'create',
description: 'The operation to perform.', description: 'The operation to perform.',
}, },
{
displayName: 'Operation',
name: 'operation',
type: 'options',
required: true,
displayOptions: {
show: {
resource: [
'listGroup',
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all groups',
},
],
default: 'getAll',
description: 'The operation to perform.',
},
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* member:create */ /* member:create */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
@ -256,27 +283,22 @@ export class Mailchimp implements INodeType {
{ {
name: 'Subscribed', name: 'Subscribed',
value: 'subscribed', value: 'subscribed',
description: '',
}, },
{ {
name: 'Unsubscribed', name: 'Unsubscribed',
value: 'unsubscribed', value: 'unsubscribed',
description: '',
}, },
{ {
name: 'Cleaned', name: 'Cleaned',
value: 'cleaned', value: 'cleaned',
description: '',
}, },
{ {
name: 'Pending', name: 'Pending',
value: 'pending', value: 'pending',
description: '',
}, },
{ {
name: 'Transactional', name: 'Transactional',
value: 'transactional', value: 'transactional',
description: '',
}, },
], ],
default: '', default: '',
@ -287,7 +309,6 @@ export class Mailchimp implements INodeType {
name: 'jsonParameters', name: 'jsonParameters',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: '',
displayOptions: { displayOptions: {
show: { show: {
resource:[ resource:[
@ -324,12 +345,10 @@ export class Mailchimp implements INodeType {
{ {
name: 'HTML', name: 'HTML',
value: 'html', value: 'html',
description: '',
}, },
{ {
name: 'Text', name: 'Text',
value: 'text', value: 'text',
description: '',
}, },
], ],
default: '', default: '',
@ -496,7 +515,6 @@ export class Mailchimp implements INodeType {
alwaysOpenEditWindow: true, alwaysOpenEditWindow: true,
}, },
default: '', default: '',
description: '',
displayOptions: { displayOptions: {
show: { show: {
resource:[ resource:[
@ -519,7 +537,86 @@ export class Mailchimp implements INodeType {
alwaysOpenEditWindow: true, alwaysOpenEditWindow: true,
}, },
default: '', default: '',
description: '', displayOptions: {
show: {
resource:[
'member',
],
operation: [
'create',
],
jsonParameters: [
true,
],
},
},
},
{
displayName: 'Interest Groups',
name: 'groupsUi',
placeholder: 'Add Interest Group',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource:[
'member'
],
operation: [
'create',
],
jsonParameters: [
false,
],
},
},
options: [
{
name: 'groupsValues',
displayName: 'Group',
typeOptions: {
multipleValueButtonText: 'Add Interest Group',
},
values: [
{
displayName: 'Category ID',
name: 'categoryId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getGroupCategories',
loadOptionsDependsOn: [
'list',
],
},
default: '',
},
{
displayName: 'Category Field ID',
name: 'categoryFieldId',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'boolean',
default: false,
},
],
},
],
},
{
displayName: 'Interest Groups',
name: 'groupJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: { displayOptions: {
show: { show: {
resource:[ resource:[
@ -772,12 +869,10 @@ export class Mailchimp implements INodeType {
{ {
name: 'HTML', name: 'HTML',
value: 'html', value: 'html',
description: '',
}, },
{ {
name: 'Text', name: 'Text',
value: 'text', value: 'text',
description: '',
}, },
], ],
default: '', default: '',
@ -791,27 +886,22 @@ export class Mailchimp implements INodeType {
{ {
name: 'Subscribed', name: 'Subscribed',
value: 'subscribed', value: 'subscribed',
description: '',
}, },
{ {
name: 'Unsubscribed', name: 'Unsubscribed',
value: 'unsubscribed', value: 'unsubscribed',
description: '',
}, },
{ {
name: 'Cleaned', name: 'Cleaned',
value: 'cleaned', value: 'cleaned',
description: '',
}, },
{ {
name: 'Pending', name: 'Pending',
value: 'pending', value: 'pending',
description: '',
}, },
{ {
name: 'Transactional', name: 'Transactional',
value: 'transactional', value: 'transactional',
description: '',
}, },
], ],
default: '', default: '',
@ -874,7 +964,6 @@ export class Mailchimp implements INodeType {
name: 'jsonParameters', name: 'jsonParameters',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: '',
displayOptions: { displayOptions: {
show: { show: {
resource:[ resource:[
@ -911,17 +1000,73 @@ export class Mailchimp implements INodeType {
{ {
name: 'HTML', name: 'HTML',
value: 'html', value: 'html',
description: '',
}, },
{ {
name: 'Text', name: 'Text',
value: 'text', value: 'text',
description: '',
}, },
], ],
default: '', default: '',
description: 'Type of email this member asked to get', description: 'Type of email this member asked to get',
}, },
{
displayName: 'Interest Groups',
name: 'groupsUi',
placeholder: 'Add Interest Group',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
'/resource':[
'member'
],
'/operation':[
'update',
],
'/jsonParameters': [
false,
],
},
},
options: [
{
name: 'groupsValues',
displayName: 'Group',
typeOptions: {
multipleValueButtonText: 'Add Interest Group',
},
values: [
{
displayName: 'Category ID',
name: 'categoryId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getGroupCategories',
loadOptionsDependsOn: [
'list',
],
},
default: '',
},
{
displayName: 'Category Field ID',
name: 'categoryFieldId',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'boolean',
default: false,
},
],
},
],
},
{ {
displayName: 'Language', displayName: 'Language',
name: 'language', name: 'language',
@ -1024,27 +1169,22 @@ export class Mailchimp implements INodeType {
{ {
name: 'Subscribed', name: 'Subscribed',
value: 'subscribed', value: 'subscribed',
description: '',
}, },
{ {
name: 'Unsubscribed', name: 'Unsubscribed',
value: 'unsubscribed', value: 'unsubscribed',
description: '',
}, },
{ {
name: 'Cleaned', name: 'Cleaned',
value: 'cleaned', value: 'cleaned',
description: '',
}, },
{ {
name: 'Pending', name: 'Pending',
value: 'pending', value: 'pending',
description: '',
}, },
{ {
name: 'Transactional', name: 'Transactional',
value: 'transactional', value: 'transactional',
description: '',
}, },
], ],
default: '', default: '',
@ -1119,7 +1259,6 @@ export class Mailchimp implements INodeType {
alwaysOpenEditWindow: true, alwaysOpenEditWindow: true,
}, },
default: '', default: '',
description: '',
displayOptions: { displayOptions: {
show: { show: {
resource:[ resource:[
@ -1142,7 +1281,28 @@ export class Mailchimp implements INodeType {
alwaysOpenEditWindow: true, alwaysOpenEditWindow: true,
}, },
default: '', default: '',
description: '', displayOptions: {
show: {
resource:[
'member',
],
operation: [
'update',
],
jsonParameters: [
true,
],
},
},
},
{
displayName: 'Interest Groups',
name: 'groupJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: { displayOptions: {
show: { show: {
resource:[ resource:[
@ -1250,6 +1410,96 @@ export class Mailchimp implements INodeType {
}, },
], ],
}, },
/* -------------------------------------------------------------------------- */
/* member:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'List',
name: 'list',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLists',
},
displayOptions: {
show: {
resource: [
'listGroup',
],
operation: [
'getAll',
],
},
},
default: '',
options: [],
required: true,
description: 'List of lists',
},
{
displayName: 'Group Category',
name: 'groupCategory',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getGroupCategories',
loadOptionsDependsOn: [
'list',
],
},
displayOptions: {
show: {
resource: [
'listGroup',
],
operation: [
'getAll',
],
},
},
default: '',
options: [],
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'listGroup',
],
operation: [
'getAll',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'listGroup',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 1000,
},
default: 500,
description: 'How many results to return.',
},
], ],
}; };
@ -1261,7 +1511,7 @@ export class Mailchimp implements INodeType {
// select them easily // select them easily
async getLists(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getLists(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
const { lists } = await mailchimpApiRequest.call(this, '/lists', 'GET'); const lists = await mailchimpApiRequestAllItems.call(this, '/lists', 'GET', 'lists');
for (const list of lists) { for (const list of lists) {
const listName = list.name; const listName = list.name;
const listId = list.id; const listId = list.id;
@ -1289,6 +1539,23 @@ export class Mailchimp implements INodeType {
} }
return returnData; return returnData;
}, },
// Get all the interest fields to display them to user so that he can
// select them easily
async getGroupCategories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const listId = this.getCurrentNodeParameter('list');
const { categories } = await mailchimpApiRequest.call(this, `/lists/${listId}/interest-categories`, 'GET');
for (const category of categories) {
const categoryName = category.title;
const categoryId = category.id;
returnData.push({
name: categoryName,
value: categoryId,
});
}
return returnData;
},
} }
}; };
@ -1302,6 +1569,22 @@ export class Mailchimp implements INodeType {
const operation = this.getNodeParameter('operation', 0) as string; const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
if (resource === 'listGroup') {
//https://mailchimp.com/developer/reference/lists/interest-categories/#get_/lists/-list_id-/interest-categories/-interest_category_id-
if (operation === 'getAll') {
const listId = this.getNodeParameter('list', i) as string;
const categoryId = this.getNodeParameter('groupCategory', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll === true) {
responseData = await mailchimpApiRequestAllItems.call(this, `/lists/${listId}/interest-categories/${categoryId}/interests`, 'GET', 'interests', {}, qs);
} else {
qs.count = this.getNodeParameter('limit', i) as number;
responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/interest-categories/${categoryId}/interests`, 'GET', {}, qs);
responseData = responseData.interests;
}
}
}
if (resource === 'member') { if (resource === 'member') {
//https://mailchimp.com/developer/reference/lists/list-members/#post_/lists/-list_id-/members //https://mailchimp.com/developer/reference/lists/list-members/#post_/lists/-list_id-/members
if (operation === 'create') { if (operation === 'create') {
@ -1363,15 +1646,29 @@ export class Mailchimp implements INodeType {
} }
body.merge_fields = mergeFields; body.merge_fields = mergeFields;
} }
const groupsValues = (this.getNodeParameter('groupsUi', i) as IDataObject).groupsValues as IDataObject[];
if (groupsValues) {
const groups = {};
for (let i = 0; i < groupsValues.length; i++) {
// @ts-ignore
groups[groupsValues[i].categoryFieldId] = groupsValues[i].value;
}
body.interests = groups;
}
} else { } else {
const locationJson = validateJSON(this.getNodeParameter('locationJson', i) as string); const locationJson = validateJSON(this.getNodeParameter('locationJson', i) as string);
const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson', i) as string); const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson', i) as string);
const groupJson = validateJSON(this.getNodeParameter('groupJson', i) as string);
if (locationJson) { if (locationJson) {
body.location = locationJson; body.location = locationJson;
} }
if (mergeFieldsJson) { if (mergeFieldsJson) {
body.merge_fields = mergeFieldsJson; body.merge_fields = mergeFieldsJson;
} }
if (groupJson) {
body.interests = groupJson;
}
} }
responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members`, 'POST', body); responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members`, 'POST', body);
} }
@ -1504,15 +1801,31 @@ export class Mailchimp implements INodeType {
body.merge_fields = mergeFields; body.merge_fields = mergeFields;
} }
} }
if (updateFields.groupsUi) {
const groupsValues = (updateFields.groupsUi as IDataObject).groupsValues as IDataObject[];
if (groupsValues) {
const groups = {};
for (let i = 0; i < groupsValues.length; i++) {
// @ts-ignore
groups[groupsValues[i].categoryFieldId] = groupsValues[i].value;
}
body.interests = groups;
}
}
} else { } else {
const locationJson = validateJSON(this.getNodeParameter('locationJson', i) as string); const locationJson = validateJSON(this.getNodeParameter('locationJson', i) as string);
const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson', i) as string); const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson', i) as string);
const groupJson = validateJSON(this.getNodeParameter('groupJson', i) as string);
if (locationJson) { if (locationJson) {
body.location = locationJson; body.location = locationJson;
} }
if (mergeFieldsJson) { if (mergeFieldsJson) {
body.merge_fields = mergeFieldsJson; body.merge_fields = mergeFieldsJson;
} }
if (groupJson) {
body.interests = groupJson;
}
} }
responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members/${email}`, 'PUT', body); responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members/${email}`, 'PUT', body);
} }

View file

@ -48,6 +48,9 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
query.api_token = credentials.apiToken; query.api_token = credentials.apiToken;
const options: OptionsWithUri = { const options: OptionsWithUri = {
headers: {
Accept: 'application/json',
},
method, method,
qs: query, qs: query,
uri: `https://api.pipedrive.com/v1${endpoint}`, uri: `https://api.pipedrive.com/v1${endpoint}`,
@ -93,7 +96,7 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
if (error.response && error.response.body && error.response.body.error) { if (error.response && error.response.body && error.response.body.error) {
// Try to return the error prettier // Try to return the error prettier
let errorMessage = `Pipedrive error response [${error.statusCode}]: ${error.response.body.error}`; let errorMessage = `Pipedrive error response [${error.statusCode}]: ${error.response.body.error.message}`;
if (error.response.body.error_info) { if (error.response.body.error_info) {
errorMessage += ` - ${error.response.body.error_info}`; errorMessage += ` - ${error.response.body.error_info}`;
} }
@ -124,7 +127,7 @@ export async function pipedriveApiRequestAllItems(this: IHookFunctions | IExecut
if (query === undefined) { if (query === undefined) {
query = {}; query = {};
} }
query.limit = 500; query.limit = 100;
query.start = 0; query.start = 0;
const returnData: IDataObject[] = []; const returnData: IDataObject[] = [];
@ -133,7 +136,12 @@ export async function pipedriveApiRequestAllItems(this: IHookFunctions | IExecut
do { do {
responseData = await pipedriveApiRequest.call(this, method, endpoint, body, query); responseData = await pipedriveApiRequest.call(this, method, endpoint, body, query);
returnData.push.apply(returnData, responseData.data); // the search path returns data diferently
if (responseData.data.items) {
returnData.push.apply(returnData, responseData.data.items);
} else {
returnData.push.apply(returnData, responseData.data);
}
query.start = responseData.additionalData.pagination.next_start; query.start = responseData.additionalData.pagination.next_start;
} while ( } while (

View file

@ -25,7 +25,6 @@ interface CustomProperty {
value: string; value: string;
} }
/** /**
* Add the additional fields to the body * Add the additional fields to the body
* *
@ -362,6 +361,11 @@ export class Pipedrive implements INodeType {
value: 'getAll', value: 'getAll',
description: 'Get data of all persons', description: 'Get data of all persons',
}, },
{
name: 'Search',
value: 'search',
description: 'Search all persons',
},
{ {
name: 'Update', name: 'Update',
value: 'update', value: 'update',
@ -2021,6 +2025,7 @@ export class Pipedrive implements INodeType {
show: { show: {
operation: [ operation: [
'getAll', 'getAll',
'search',
], ],
}, },
}, },
@ -2035,6 +2040,7 @@ export class Pipedrive implements INodeType {
show: { show: {
operation: [ operation: [
'getAll', 'getAll',
'search',
], ],
returnAll: [ returnAll: [
false, false,
@ -2088,6 +2094,81 @@ export class Pipedrive implements INodeType {
}, },
], ],
}, },
// ----------------------------------
// person:search
// ----------------------------------
{
displayName: 'Term',
name: 'term',
type: 'string',
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'person',
],
},
},
default: '',
description: 'The search term to look for. Minimum 2 characters (or 1 if using exact_match).',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'person',
],
},
},
default: {},
options: [
{
displayName: 'Exact Match',
name: 'exactMatch',
type: 'boolean',
default: false,
description: 'When enabled, only full exact matches against the given term are returned. It is not case sensitive.',
},
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'A comma-separated string array. The fields to perform the search from. Defaults to all of them.',
},
{
displayName: 'Include Fields',
name: 'includeFields',
type: 'string',
default: '',
description: 'Supports including optional fields in the results which are not provided by default.',
},
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'string',
default: '',
description: 'Will filter Deals by the provided Organization ID.',
},
{
displayName: 'RAW Data',
name: 'rawData',
type: 'boolean',
default: false,
description: `Returns the data exactly in the way it got received from the API.`,
},
],
},
], ],
}; };
@ -2526,6 +2607,39 @@ export class Pipedrive implements INodeType {
endpoint = `/persons`; endpoint = `/persons`;
} else if (operation === 'search') {
// ----------------------------------
// persons:search
// ----------------------------------
requestMethod = 'GET';
qs.term = this.getNodeParameter('term', i) as string;
returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll === false) {
qs.limit = this.getNodeParameter('limit', i) as number;
}
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.fields) {
qs.fields = additionalFields.fields as string;
}
if (additionalFields.exactMatch) {
qs.exact_match = additionalFields.exactMatch as boolean;
}
if (additionalFields.organizationId) {
qs.organization_id = parseInt(additionalFields.organizationId as string, 10);
}
if (additionalFields.includeFields) {
qs.include_fields = additionalFields.includeFields as string;
}
endpoint = `/persons/search`;
} else if (operation === 'update') { } else if (operation === 'update') {
// ---------------------------------- // ----------------------------------
// person:update // person:update
@ -2562,7 +2676,9 @@ export class Pipedrive implements INodeType {
let responseData; let responseData;
if (returnAll === true) { if (returnAll === true) {
responseData = await pipedriveApiRequestAllItems.call(this, requestMethod, endpoint, body, qs); responseData = await pipedriveApiRequestAllItems.call(this, requestMethod, endpoint, body, qs);
} else { } else {
if (customProperties !== undefined) { if (customProperties !== undefined) {
@ -2597,6 +2713,19 @@ export class Pipedrive implements INodeType {
responseData.data = []; responseData.data = [];
} }
if (operation === 'search' && responseData.data && responseData.data.items) {
responseData.data = responseData.data.items;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.rawData !== true) {
responseData.data = responseData.data.map((item: { result_score: number, item: object }) => {
return {
result_score: item.result_score,
...item.item,
};
});
}
}
if (Array.isArray(responseData.data)) { if (Array.isArray(responseData.data)) {
returnData.push.apply(returnData, responseData.data as IDataObject[]); returnData.push.apply(returnData, responseData.data as IDataObject[]);
} else { } else {

View file

@ -0,0 +1,129 @@
import { IDataObject, INodeExecutionData } from 'n8n-workflow';
import pgPromise = require('pg-promise');
import pg = require('pg-promise/typescript/pg-subset');
/**
* Returns of copy of the items which only contains the json data and
* of that only the define properties
*
* @param {INodeExecutionData[]} items The items to copy
* @param {string[]} properties The properties it should include
* @returns
*/
function getItemCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] {
// Prepare the data to insert and copy it to be returned
let newItem: IDataObject;
return items.map(item => {
newItem = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
}
}
return newItem;
});
}
/**
* Executes the given SQL query on the database.
*
* @param {Function} getNodeParam The getter for the Node's parameters
* @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
* @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
* @param {input[]} input The Node's input data
* @returns Promise<Array<object>>
*/
export function pgQuery(
getNodeParam: Function,
pgp: pgPromise.IMain<{}, pg.IClient>,
db: pgPromise.IDatabase<{}, pg.IClient>,
input: INodeExecutionData[],
): Promise<Array<object>> {
const queries: string[] = [];
for (let i = 0; i < input.length; i++) {
queries.push(getNodeParam('query', i) as string);
}
return db.any(pgp.helpers.concat(queries));
}
/**
* Inserts the given items into the database.
*
* @param {Function} getNodeParam The getter for the Node's parameters
* @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
* @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
* @param {INodeExecutionData[]} items The items to be inserted
* @returns Promise<Array<IDataObject>>
*/
export async function pgInsert(
getNodeParam: Function,
pgp: pgPromise.IMain<{}, pg.IClient>,
db: pgPromise.IDatabase<{}, pg.IClient>,
items: INodeExecutionData[],
): Promise<Array<IDataObject[]>> {
const table = getNodeParam('table', 0) as string;
const schema = getNodeParam('schema', 0) as string;
let returnFields = (getNodeParam('returnFields', 0) as string).split(',') as string[];
const columnString = getNodeParam('columns', 0) as string;
const columns = columnString.split(',').map(column => column.trim());
const cs = new pgp.helpers.ColumnSet(columns);
const te = new pgp.helpers.TableName({ table, schema });
// Prepare the data to insert and copy it to be returned
const insertItems = getItemCopy(items, columns);
// Generate the multi-row insert query and return the id of new row
returnFields = returnFields.map(value => value.trim()).filter(value => !!value);
const query =
pgp.helpers.insert(insertItems, cs, te) +
(returnFields.length ? ` RETURNING ${returnFields.join(',')}` : '');
// Executing the query to insert the data
const insertData = await db.manyOrNone(query);
return [insertData, insertItems];
}
/**
* Updates the given items in the database.
*
* @param {Function} getNodeParam The getter for the Node's parameters
* @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
* @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
* @param {INodeExecutionData[]} items The items to be updated
* @returns Promise<Array<IDataObject>>
*/
export async function pgUpdate(
getNodeParam: Function,
pgp: pgPromise.IMain<{}, pg.IClient>,
db: pgPromise.IDatabase<{}, pg.IClient>,
items: INodeExecutionData[],
): Promise<Array<IDataObject>> {
const table = getNodeParam('table', 0) as string;
const updateKey = getNodeParam('updateKey', 0) as string;
const columnString = getNodeParam('columns', 0) as string;
const columns = columnString.split(',').map(column => column.trim());
// Make sure that the updateKey does also get queried
if (!columns.includes(updateKey)) {
columns.unshift(updateKey);
}
// Prepare the data to update and copy it to be returned
const updateItems = getItemCopy(items, columns);
// Generate the multi-row update query
const query =
pgp.helpers.update(updateItems, columns, table) + ' WHERE v.' + updateKey + ' = t.' + updateKey;
// Executing the query to update the data
await db.none(query);
return updateItems;
}

View file

@ -3,36 +3,12 @@ import {
IDataObject, IDataObject,
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as pgPromise from 'pg-promise'; import * as pgPromise from 'pg-promise';
import { pgInsert, pgQuery, pgUpdate } from './Postgres.node.functions';
/**
* Returns of copy of the items which only contains the json data and
* of that only the define properties
*
* @param {INodeExecutionData[]} items The items to copy
* @param {string[]} properties The properties it should include
* @returns
*/
function getItemCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] {
// Prepare the data to insert and copy it to be returned
let newItem: IDataObject;
return items.map((item) => {
newItem = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
}
}
return newItem;
});
}
export class Postgres implements INodeType { export class Postgres implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -52,7 +28,7 @@ export class Postgres implements INodeType {
{ {
name: 'postgres', name: 'postgres',
required: true, required: true,
} },
], ],
properties: [ properties: [
{ {
@ -63,17 +39,17 @@ export class Postgres implements INodeType {
{ {
name: 'Execute Query', name: 'Execute Query',
value: 'executeQuery', value: 'executeQuery',
description: 'Executes a SQL query.', description: 'Execute an SQL query',
}, },
{ {
name: 'Insert', name: 'Insert',
value: 'insert', value: 'insert',
description: 'Insert rows in database.', description: 'Insert rows in database',
}, },
{ {
name: 'Update', name: 'Update',
value: 'update', value: 'update',
description: 'Updates rows in database.', description: 'Update rows in database',
}, },
], ],
default: 'insert', default: 'insert',
@ -92,9 +68,7 @@ export class Postgres implements INodeType {
}, },
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['executeQuery'],
'executeQuery'
],
}, },
}, },
default: '', default: '',
@ -103,7 +77,6 @@ export class Postgres implements INodeType {
description: 'The SQL query to execute.', description: 'The SQL query to execute.',
}, },
// ---------------------------------- // ----------------------------------
// insert // insert
// ---------------------------------- // ----------------------------------
@ -113,9 +86,7 @@ export class Postgres implements INodeType {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['insert'],
'insert'
],
}, },
}, },
default: 'public', default: 'public',
@ -128,9 +99,7 @@ export class Postgres implements INodeType {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['insert'],
'insert'
],
}, },
}, },
default: '', default: '',
@ -143,14 +112,13 @@ export class Postgres implements INodeType {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['insert'],
'insert'
],
}, },
}, },
default: '', default: '',
placeholder: 'id,name,description', placeholder: 'id,name,description',
description: 'Comma separated list of the properties which should used as columns for the new rows.', description:
'Comma separated list of the properties which should used as columns for the new rows.',
}, },
{ {
displayName: 'Return Fields', displayName: 'Return Fields',
@ -158,16 +126,13 @@ export class Postgres implements INodeType {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['insert'],
'insert'
],
}, },
}, },
default: '*', default: '*',
description: 'Comma separated list of the fields that the operation will return', description: 'Comma separated list of the fields that the operation will return',
}, },
// ---------------------------------- // ----------------------------------
// update // update
// ---------------------------------- // ----------------------------------
@ -177,9 +142,7 @@ export class Postgres implements INodeType {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['update'],
'update'
],
}, },
}, },
default: '', default: '',
@ -192,14 +155,13 @@ export class Postgres implements INodeType {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['update'],
'update'
],
}, },
}, },
default: 'id', default: 'id',
required: true, required: true,
description: 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".', description:
'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
}, },
{ {
displayName: 'Columns', displayName: 'Columns',
@ -207,22 +169,18 @@ export class Postgres implements INodeType {
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['update'],
'update'
],
}, },
}, },
default: '', default: '',
placeholder: 'name,description', placeholder: 'name,description',
description: 'Comma separated list of the properties which should used as columns for rows to update.', description:
'Comma separated list of the properties which should used as columns for rows to update.',
}, },
],
]
}; };
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const credentials = this.getCredentials('postgres'); const credentials = this.getCredentials('postgres');
if (credentials === undefined) { if (credentials === undefined) {
@ -238,7 +196,7 @@ export class Postgres implements INodeType {
user: credentials.user as string, user: credentials.user as string,
password: credentials.password as string, password: credentials.password as string,
ssl: !['disable', undefined].includes(credentials.ssl as string | undefined), ssl: !['disable', undefined].includes(credentials.ssl as string | undefined),
sslmode: credentials.ssl as string || 'disable', sslmode: (credentials.ssl as string) || 'disable',
}; };
const db = pgp(config); const db = pgp(config);
@ -253,39 +211,15 @@ export class Postgres implements INodeType {
// executeQuery // executeQuery
// ---------------------------------- // ----------------------------------
const queries: string[] = []; const queryResult = await pgQuery(this.getNodeParameter, pgp, db, items);
for (let i = 0; i < items.length; i++) {
queries.push(this.getNodeParameter('query', i) as string);
}
const queryResult = await db.any(pgp.helpers.concat(queries));
returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]); returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]);
} else if (operation === 'insert') { } else if (operation === 'insert') {
// ---------------------------------- // ----------------------------------
// insert // insert
// ---------------------------------- // ----------------------------------
const table = this.getNodeParameter('table', 0) as string; const [insertData, insertItems] = await pgInsert(this.getNodeParameter, pgp, db, items);
const schema = this.getNodeParameter('schema', 0) as string;
let returnFields = (this.getNodeParameter('returnFields', 0) as string).split(',') as string[];
const columnString = this.getNodeParameter('columns', 0) as string;
const columns = columnString.split(',').map(column => column.trim());
const cs = new pgp.helpers.ColumnSet(columns);
const te = new pgp.helpers.TableName({ table, schema });
// Prepare the data to insert and copy it to be returned
const insertItems = getItemCopy(items, columns);
// Generate the multi-row insert query and return the id of new row
returnFields = returnFields.map(value => value.trim()).filter(value => !!value);
const query = pgp.helpers.insert(insertItems, cs, te) + (returnFields.length ? ` RETURNING ${returnFields.join(',')}` : '');
// Executing the query to insert the data
const insertData = await db.manyOrNone(query);
// Add the id to the data // Add the id to the data
for (let i = 0; i < insertData.length; i++) { for (let i = 0; i < insertData.length; i++) {
@ -293,37 +227,17 @@ export class Postgres implements INodeType {
json: { json: {
...insertData[i], ...insertData[i],
...insertItems[i], ...insertItems[i],
} },
}); });
} }
} else if (operation === 'update') { } else if (operation === 'update') {
// ---------------------------------- // ----------------------------------
// update // update
// ---------------------------------- // ----------------------------------
const table = this.getNodeParameter('table', 0) as string; const updateItems = await pgUpdate(this.getNodeParameter, pgp, db, items);
const updateKey = this.getNodeParameter('updateKey', 0) as string;
const columnString = this.getNodeParameter('columns', 0) as string;
const columns = columnString.split(',').map(column => column.trim());
// Make sure that the updateKey does also get queried
if (!columns.includes(updateKey)) {
columns.unshift(updateKey);
}
// Prepare the data to update and copy it to be returned
const updateItems = getItemCopy(items, columns);
// Generate the multi-row update query
const query = pgp.helpers.update(updateItems, columns, table) + ' WHERE v.' + updateKey + ' = t.' + updateKey;
// Executing the query to update the data
await db.none(query);
returnItems = this.helpers.returnJsonArray(updateItems as IDataObject[]);
returnItems = this.helpers.returnJsonArray(updateItems);
} else { } else {
await pgp.end(); await pgp.end();
throw new Error(`The operation "${operation}" is not supported!`); throw new Error(`The operation "${operation}" is not supported!`);

View file

@ -0,0 +1,246 @@
import { IExecuteFunctions } from 'n8n-core';
import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
import * as pgPromise from 'pg-promise';
import { pgInsert, pgQuery, pgUpdate } from '../Postgres/Postgres.node.functions';
export class QuestDb implements INodeType {
description: INodeTypeDescription = {
displayName: 'QuestDB',
name: 'questDb',
icon: 'file:questdb.png',
group: ['input'],
version: 1,
description: 'Gets, add and update data in QuestDB.',
defaults: {
name: 'QuestDB',
color: '#2C4A79',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'questDb',
required: true,
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Execute Query',
value: 'executeQuery',
description: 'Executes a SQL query.',
},
{
name: 'Insert',
value: 'insert',
description: 'Insert rows in database.',
},
{
name: 'Update',
value: 'update',
description: 'Updates rows in database.',
},
],
default: 'insert',
description: 'The operation to perform.',
},
// ----------------------------------
// executeQuery
// ----------------------------------
{
displayName: 'Query',
name: 'query',
type: 'string',
typeOptions: {
rows: 5,
},
displayOptions: {
show: {
operation: ['executeQuery'],
},
},
default: '',
placeholder: 'SELECT id, name FROM product WHERE id < 40',
required: true,
description: 'The SQL query to execute.',
},
// ----------------------------------
// insert
// ----------------------------------
{
displayName: 'Schema',
name: 'schema',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: 'public',
required: true,
description: 'Name of the schema the table belongs to',
},
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '',
required: true,
description: 'Name of the table in which to insert data to.',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '',
placeholder: 'id,name,description',
description:
'Comma separated list of the properties which should used as columns for the new rows.',
},
{
displayName: 'Return Fields',
name: 'returnFields',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '*',
description: 'Comma separated list of the fields that the operation will return',
},
// ----------------------------------
// update
// ----------------------------------
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
required: true,
description: 'Name of the table in which to update data in',
},
{
displayName: 'Update Key',
name: 'updateKey',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: 'id',
required: true,
description:
'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
placeholder: 'name,description',
description:
'Comma separated list of the properties which should used as columns for rows to update.',
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const credentials = this.getCredentials('questDb');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const pgp = pgPromise();
const config = {
host: credentials.host as string,
port: credentials.port as number,
database: credentials.database as string,
user: credentials.user as string,
password: credentials.password as string,
ssl: !['disable', undefined].includes(credentials.ssl as string | undefined),
sslmode: (credentials.ssl as string) || 'disable',
};
const db = pgp(config);
let returnItems = [];
const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0) as string;
if (operation === 'executeQuery') {
// ----------------------------------
// executeQuery
// ----------------------------------
const queryResult = await pgQuery(this.getNodeParameter, pgp, db, items);
returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]);
} else if (operation === 'insert') {
// ----------------------------------
// insert
// ----------------------------------
const [insertData, insertItems] = await pgInsert(this.getNodeParameter, pgp, db, items);
// Add the id to the data
for (let i = 0; i < insertData.length; i++) {
returnItems.push({
json: {
...insertData[i],
...insertItems[i],
},
});
}
} else if (operation === 'update') {
// ----------------------------------
// update
// ----------------------------------
const updateItems = await pgUpdate(this.getNodeParameter, pgp, db, items);
returnItems = this.helpers.returnJsonArray(updateItems);
} else {
await pgp.end();
throw new Error(`The operation "${operation}" is not supported!`);
}
// Close the connection
await pgp.end();
return this.prepareOutputData(returnItems);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -48,15 +48,15 @@ interface IPostMessageBody {
export class Rocketchat implements INodeType { export class Rocketchat implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Rocketchat', displayName: 'RocketChat',
name: 'rocketchat', name: 'rocketchat',
icon: 'file:rocketchat.png', icon: 'file:rocketchat.png',
group: ['output'], group: ['output'],
version: 1, version: 1,
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Consume Rocketchat API', description: 'Consume RocketChat API',
defaults: { defaults: {
name: 'Rocketchat', name: 'RocketChat',
color: '#c02428', color: '#c02428',
}, },
inputs: ['main'], inputs: ['main'],

View file

@ -29,7 +29,7 @@ export const attachmentOperations = [
{ {
name: 'Get', name: 'Get',
value: 'get', value: 'get',
description: 'Get the data of an attachments', description: 'Get the data of an attachment',
}, },
{ {
name: 'Get All', name: 'Get All',

View file

@ -44,17 +44,17 @@ export const checklistOperations = [
{ {
name: 'Get Checklist Items', name: 'Get Checklist Items',
value: 'getCheckItem', value: 'getCheckItem',
description: 'Get a specific Checklist on a card', description: 'Get a specific checklist on a card',
}, },
{ {
name: 'Get Completed Checklist Items', name: 'Get Completed Checklist Items',
value: 'completedCheckItems', value: 'completedCheckItems',
description: 'Get the completed Checklist items on a card', description: 'Get the completed checklist items on a card',
}, },
{ {
name: 'Update Checklist Item', name: 'Update Checklist Item',
value: 'updateCheckItem', value: 'updateCheckItem',
description: 'Update an item in a checklist on a card.', description: 'Update an item in a checklist on a card',
}, },
], ],
default: 'getAll', default: 'getAll',

View file

@ -39,7 +39,7 @@ export const labelOperations = [
{ {
name: 'Get All', name: 'Get All',
value: 'getAll', value: 'getAll',
description: 'Returns all label for the board', description: 'Returns all labels for the board',
}, },
{ {
name: 'Remove From Card', name: 'Remove From Card',

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-nodes-base", "name": "n8n-nodes-base",
"version": "0.68.0", "version": "0.68.1",
"description": "Base nodes of n8n", "description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -112,6 +112,7 @@
"dist/credentials/PipedriveApi.credentials.js", "dist/credentials/PipedriveApi.credentials.js",
"dist/credentials/Postgres.credentials.js", "dist/credentials/Postgres.credentials.js",
"dist/credentials/PostmarkApi.credentials.js", "dist/credentials/PostmarkApi.credentials.js",
"dist/credentials/QuestDb.credentials.js",
"dist/credentials/Redis.credentials.js", "dist/credentials/Redis.credentials.js",
"dist/credentials/RocketchatApi.credentials.js", "dist/credentials/RocketchatApi.credentials.js",
"dist/credentials/RundeckApi.credentials.js", "dist/credentials/RundeckApi.credentials.js",
@ -260,6 +261,7 @@
"dist/nodes/Pipedrive/PipedriveTrigger.node.js", "dist/nodes/Pipedrive/PipedriveTrigger.node.js",
"dist/nodes/Postgres/Postgres.node.js", "dist/nodes/Postgres/Postgres.node.js",
"dist/nodes/Postmark/PostmarkTrigger.node.js", "dist/nodes/Postmark/PostmarkTrigger.node.js",
"dist/nodes/QuestDb/QuestDb.node.js",
"dist/nodes/ReadBinaryFile.node.js", "dist/nodes/ReadBinaryFile.node.js",
"dist/nodes/ReadBinaryFiles.node.js", "dist/nodes/ReadBinaryFiles.node.js",
"dist/nodes/ReadPdf.node.js", "dist/nodes/ReadPdf.node.js",