Merge branch 'master' into save-changes-warning

This commit is contained in:
Jan
2020-10-25 11:47:47 +01:00
committed by GitHub
697 changed files with 34774 additions and 7712 deletions

View File

@@ -19,7 +19,7 @@ Condition notice.
Software: n8n Software: n8n
License: Apache 2.0 License: Apache 2.0 with Commons Clause
Licensor: n8n GmbH Licensor: n8n GmbH

View File

@@ -1,9 +0,0 @@
MONGO_INITDB_ROOT_USERNAME=changeUser
MONGO_INITDB_ROOT_PASSWORD=changePassword
MONGO_INITDB_DATABASE=n8n
MONGO_NON_ROOT_USERNAME=changeUser
MONGO_NON_ROOT_PASSWORD=changePassword
N8N_BASIC_AUTH_USER=changeUser
N8N_BASIC_AUTH_PASSWORD=changePassword

View File

@@ -1,26 +0,0 @@
# n8n with MongoDB
Starts n8n with MongoDB as database.
## Start
To start n8n with MongoDB 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

@@ -1,34 +0,0 @@
version: '3.1'
services:
mongo:
image: mongo:4.0
restart: always
environment:
- MONGO_INITDB_ROOT_USERNAME
- MONGO_INITDB_ROOT_PASSWORD
- MONGO_INITDB_DATABASE
- MONGO_NON_ROOT_USERNAME
- MONGO_NON_ROOT_PASSWORD
volumes:
- ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
n8n:
image: n8nio/n8n
restart: always
environment:
- DB_TYPE=mongodb
- DB_MONGODB_CONNECTION_URL=mongodb://${MONGO_NON_ROOT_USERNAME}:${MONGO_NON_ROOT_PASSWORD}@mongo:27017/${MONGO_INITDB_DATABASE}
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER
- N8N_BASIC_AUTH_PASSWORD
ports:
- 5678:5678
links:
- mongo
volumes:
- ~/.n8n:/root/.n8n
# Wait 5 seconds to start n8n to make sure that MongoDB is ready
# when n8n tries to connect to it
command: /bin/sh -c "sleep 5; n8n start"

View File

@@ -1,17 +0,0 @@
#!/bin/bash
set -e;
# Create a default non-root role
MONGO_NON_ROOT_ROLE="${MONGO_NON_ROOT_ROLE:-readWrite}"
if [ -n "${MONGO_NON_ROOT_USERNAME:-}" ] && [ -n "${MONGO_NON_ROOT_PASSWORD:-}" ]; then
"${mongo[@]}" "$MONGO_INITDB_DATABASE" <<-EOJS
db.createUser({
user: $(_js_escape "$MONGO_NON_ROOT_USERNAME"),
pwd: $(_js_escape "$MONGO_NON_ROOT_PASSWORD"),
roles: [ { role: $(_js_escape "$MONGO_NON_ROOT_ROLE"), db: $(_js_escape "$MONGO_INITDB_DATABASE") } ]
})
EOJS
else
echo "SETUP INFO: No Environment variables given!"
fi

View File

@@ -2,6 +2,43 @@
This list shows all the versions which include breaking changes and how to upgrade. This list shows all the versions which include breaking changes and how to upgrade.
## 0.87.0
### What changed?
The link.fish node got removed because the service is shutting down.
### When is action necessary?
If you are are actively using the link.fish node.
### How to upgrade:
Unfortunately, that's not possible. We'd recommend you to look for an alternative service.
## 0.83.0
### What changed?
In the Active Campaign Node, we have changed how the `getAll` operation works with various resources for the sake of consistency. To achieve this, a new parameter called 'Simple' has been added.
### When is action necessary?
When one of the following resources/operations is used:
| Resource | Operation |
|--|--|
| Deal | Get All |
| Connector | Get All |
| E-commerce Order | Get All |
| E-commerce Customer | Get All |
| E-commerce Order Products | Get All |
### How to upgrade:
Open the affected resource/operation and set the parameter `Simple` to false.
## 0.79.0 ## 0.79.0
### What changed? ### What changed?

View File

@@ -19,7 +19,7 @@ Condition notice.
Software: n8n Software: n8n
License: Apache 2.0 License: Apache 2.0 with Commons Clause
Licensor: n8n GmbH Licensor: n8n GmbH

View File

@@ -10,7 +10,7 @@ process.env.NODE_CONFIG_DIR = process.env.NODE_CONFIG_DIR || path.join(__dirname
var versionFlags = [ // tslint:disable-line:no-var-keyword var versionFlags = [ // tslint:disable-line:no-var-keyword
'-v', '-v',
'-V', '-V',
'--version' '--version',
]; ];
if (versionFlags.includes(process.argv.slice(-1)[0])) { if (versionFlags.includes(process.argv.slice(-1)[0])) {
console.log(require('../package').version); console.log(require('../package').version);
@@ -22,23 +22,10 @@ if (process.argv.length === 2) {
process.argv.push('start'); process.argv.push('start');
} }
var command = process.argv[2]; // tslint:disable-line:no-var-keyword var nodeVersion = process.versions.node.split('.');
// Check if the command the user did enter is supported else stop if (parseInt(nodeVersion[0], 10) < 12 || parseInt(nodeVersion[0], 10) === 12 && parseInt(nodeVersion[1], 10) < 9) {
var supportedCommands = [ // tslint:disable-line:no-var-keyword console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 12.9 or later!\n`);
'execute',
'help',
'start',
];
if (!supportedCommands.includes(command)) {
console.log('\nThe command "' + command + '" is not known!\n');
process.argv.pop();
process.argv.push('--help');
}
if (parseInt(process.versions.node.split('.')[0], 10) < 10) {
console.log('\nThe Node.js version is too old to run n8n. Please use version 10 or later!\n');
process.exit(0); process.exit(0);
} }

View File

@@ -0,0 +1,70 @@
import {
Command, flags,
} from '@oclif/command';
import {
IDataObject
} from 'n8n-workflow';
import {
Db,
GenericHelpers,
} from "../../../src";
export class DeactivateCommand extends Command {
static description = '\nDeactivates workflows';
static examples = [
`$ n8n config:workflow:deactivate --all`,
`$ n8n config:workflow:deactivate --id=5`,
];
static flags = {
help: flags.help({ char: 'h' }),
all: flags.boolean({
description: 'Deactivates all workflows',
}),
id: flags.string({
description: 'Deactivats the workflow with the given ID',
}),
};
async run() {
const { flags } = this.parse(DeactivateCommand);
if (!flags.all && !flags.id) {
GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`);
return;
}
if (flags.all && flags.id) {
GenericHelpers.logOutput(`Either "--all" or "--id" can be set never both!`);
return;
}
try {
await Db.init();
const findQuery: IDataObject = {};
if (flags.id) {
console.log(`Deactivating workflow with ID: ${flags.id}`);
findQuery.id = flags.id;
} else {
console.log('Deactivating all workflows');
findQuery.active = true;
}
await Db.collections.Workflow!.update(findQuery, { active: false });
console.log('Done');
} catch (e) {
console.error('\nGOT ERROR');
console.log('====================================');
console.error(e.message);
console.error(e.stack);
this.exit(1);
}
this.exit();
}
}

View File

@@ -10,6 +10,7 @@ import {
import { import {
ActiveExecutions, ActiveExecutions,
CredentialsOverwrites, CredentialsOverwrites,
CredentialTypes,
Db, Db,
ExternalHooks, ExternalHooks,
GenericHelpers, GenericHelpers,
@@ -116,6 +117,8 @@ export class Execute extends Command {
// Add the found types to an instance other parts of the application can use // Add the found types to an instance other parts of the application can use
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
await nodeTypes.init(loadNodesAndCredentials.nodeTypes); await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
const credentialTypes = CredentialTypes();
await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) { if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) {
workflowId = undefined; workflowId = undefined;

View File

@@ -8,12 +8,14 @@ const open = require('open');
import * as config from '../config'; import * as config from '../config';
import { import {
ActiveExecutions,
ActiveWorkflowRunner, ActiveWorkflowRunner,
CredentialTypes,
CredentialsOverwrites, CredentialsOverwrites,
CredentialTypes,
Db, Db,
ExternalHooks, ExternalHooks,
GenericHelpers, GenericHelpers,
IExecutionsCurrentSummary,
LoadNodesAndCredentials, LoadNodesAndCredentials,
NodeTypes, NodeTypes,
Server, Server,
@@ -68,23 +70,46 @@ export class Start extends Command {
static async stopProcess() { static async stopProcess() {
console.log(`\nStopping n8n...`); console.log(`\nStopping n8n...`);
setTimeout(() => { try {
// In case that something goes wrong with shutdown we const externalHooks = ExternalHooks();
// kill after max. 30 seconds no matter what await externalHooks.run('n8n.stop', []);
process.exit(processExistCode);
}, 30000);
const removePromises = []; setTimeout(() => {
if (activeWorkflowRunner !== undefined) { // In case that something goes wrong with shutdown we
removePromises.push(activeWorkflowRunner.removeAll()); // kill after max. 30 seconds no matter what
process.exit(processExistCode);
}, 30000);
const removePromises = [];
if (activeWorkflowRunner !== undefined) {
removePromises.push(activeWorkflowRunner.removeAll());
}
// Remove all test webhooks
const testWebhooks = TestWebhooks.getInstance();
removePromises.push(testWebhooks.removeAll());
await Promise.all(removePromises);
// Wait for active workflow executions to finish
const activeExecutionsInstance = ActiveExecutions.getInstance();
let executingWorkflows = activeExecutionsInstance.getActiveExecutions();
let count = 0;
while (executingWorkflows.length !== 0) {
if (count++ % 4 === 0) {
console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`);
}
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
executingWorkflows = activeExecutionsInstance.getActiveExecutions();
}
} catch (error) {
console.error('There was an error shutting down n8n.', error);
} }
// Remove all test webhooks
const testWebhooks = TestWebhooks.getInstance();
removePromises.push(testWebhooks.removeAll());
await Promise.all(removePromises);
process.exit(processExistCode); process.exit(processExistCode);
} }

View File

@@ -10,58 +10,58 @@ const config = convict({
doc: 'Type of database to use', doc: 'Type of database to use',
format: ['sqlite', 'mariadb', 'mongodb', 'mysqldb', 'postgresdb'], format: ['sqlite', 'mariadb', 'mongodb', 'mysqldb', 'postgresdb'],
default: 'sqlite', default: 'sqlite',
env: 'DB_TYPE' env: 'DB_TYPE',
}, },
mongodb: { mongodb: {
connectionUrl: { connectionUrl: {
doc: 'MongoDB Connection URL', doc: 'MongoDB Connection URL',
format: '*', format: '*',
default: 'mongodb://user:password@localhost:27017/database', default: 'mongodb://user:password@localhost:27017/database',
env: 'DB_MONGODB_CONNECTION_URL' env: 'DB_MONGODB_CONNECTION_URL',
} },
}, },
tablePrefix: { tablePrefix: {
doc: 'Prefix for table names', doc: 'Prefix for table names',
format: '*', format: '*',
default: '', default: '',
env: 'DB_TABLE_PREFIX' env: 'DB_TABLE_PREFIX',
}, },
postgresdb: { postgresdb: {
database: { database: {
doc: 'PostgresDB Database', doc: 'PostgresDB Database',
format: String, format: String,
default: 'n8n', default: 'n8n',
env: 'DB_POSTGRESDB_DATABASE' env: 'DB_POSTGRESDB_DATABASE',
}, },
host: { host: {
doc: 'PostgresDB Host', doc: 'PostgresDB Host',
format: String, format: String,
default: 'localhost', default: 'localhost',
env: 'DB_POSTGRESDB_HOST' env: 'DB_POSTGRESDB_HOST',
}, },
password: { password: {
doc: 'PostgresDB Password', doc: 'PostgresDB Password',
format: String, format: String,
default: '', default: '',
env: 'DB_POSTGRESDB_PASSWORD' env: 'DB_POSTGRESDB_PASSWORD',
}, },
port: { port: {
doc: 'PostgresDB Port', doc: 'PostgresDB Port',
format: Number, format: Number,
default: 5432, default: 5432,
env: 'DB_POSTGRESDB_PORT' env: 'DB_POSTGRESDB_PORT',
}, },
user: { user: {
doc: 'PostgresDB User', doc: 'PostgresDB User',
format: String, format: String,
default: 'root', default: 'root',
env: 'DB_POSTGRESDB_USER' env: 'DB_POSTGRESDB_USER',
}, },
schema: { schema: {
doc: 'PostgresDB Schema', doc: 'PostgresDB Schema',
format: String, format: String,
default: 'public', default: 'public',
env: 'DB_POSTGRESDB_SCHEMA' env: 'DB_POSTGRESDB_SCHEMA',
}, },
ssl: { ssl: {
@@ -89,7 +89,7 @@ const config = convict({
default: true, default: true,
env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED', env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED',
}, },
} },
}, },
mysqldb: { mysqldb: {
@@ -97,31 +97,31 @@ const config = convict({
doc: 'MySQL Database', doc: 'MySQL Database',
format: String, format: String,
default: 'n8n', default: 'n8n',
env: 'DB_MYSQLDB_DATABASE' env: 'DB_MYSQLDB_DATABASE',
}, },
host: { host: {
doc: 'MySQL Host', doc: 'MySQL Host',
format: String, format: String,
default: 'localhost', default: 'localhost',
env: 'DB_MYSQLDB_HOST' env: 'DB_MYSQLDB_HOST',
}, },
password: { password: {
doc: 'MySQL Password', doc: 'MySQL Password',
format: String, format: String,
default: '', default: '',
env: 'DB_MYSQLDB_PASSWORD' env: 'DB_MYSQLDB_PASSWORD',
}, },
port: { port: {
doc: 'MySQL Port', doc: 'MySQL Port',
format: Number, format: Number,
default: 3306, default: 3306,
env: 'DB_MYSQLDB_PORT' env: 'DB_MYSQLDB_PORT',
}, },
user: { user: {
doc: 'MySQL User', doc: 'MySQL User',
format: String, format: String,
default: 'root', default: 'root',
env: 'DB_MYSQLDB_USER' env: 'DB_MYSQLDB_USER',
}, },
}, },
}, },
@@ -136,7 +136,7 @@ const config = convict({
doc: 'Overwrites for credentials', doc: 'Overwrites for credentials',
format: '*', format: '*',
default: '{}', default: '{}',
env: 'CREDENTIALS_OVERWRITE_DATA' env: 'CREDENTIALS_OVERWRITE_DATA',
}, },
endpoint: { endpoint: {
doc: 'Fetch credentials from API', doc: 'Fetch credentials from API',
@@ -156,7 +156,7 @@ const config = convict({
doc: 'In what process workflows should be executed', doc: 'In what process workflows should be executed',
format: ['main', 'own'], format: ['main', 'own'],
default: 'own', default: 'own',
env: 'EXECUTIONS_PROCESS' env: 'EXECUTIONS_PROCESS',
}, },
// A Workflow times out and gets canceled after this time (seconds). // A Workflow times out and gets canceled after this time (seconds).
@@ -174,13 +174,13 @@ const config = convict({
doc: 'Max run time (seconds) before stopping the workflow execution', doc: 'Max run time (seconds) before stopping the workflow execution',
format: Number, format: Number,
default: -1, default: -1,
env: 'EXECUTIONS_TIMEOUT' env: 'EXECUTIONS_TIMEOUT',
}, },
maxTimeout: { maxTimeout: {
doc: 'Max execution time (seconds) that can be set for a workflow individually', doc: 'Max execution time (seconds) that can be set for a workflow individually',
format: Number, format: Number,
default: 3600, default: 3600,
env: 'EXECUTIONS_TIMEOUT_MAX' env: 'EXECUTIONS_TIMEOUT_MAX',
}, },
// If a workflow executes all the data gets saved by default. This // If a workflow executes all the data gets saved by default. This
@@ -193,13 +193,13 @@ const config = convict({
doc: 'What workflow execution data to save on error', doc: 'What workflow execution data to save on error',
format: ['all', 'none'], format: ['all', 'none'],
default: 'all', default: 'all',
env: 'EXECUTIONS_DATA_SAVE_ON_ERROR' env: 'EXECUTIONS_DATA_SAVE_ON_ERROR',
}, },
saveDataOnSuccess: { saveDataOnSuccess: {
doc: 'What workflow execution data to save on success', doc: 'What workflow execution data to save on success',
format: ['all', 'none'], format: ['all', 'none'],
default: 'all', default: 'all',
env: 'EXECUTIONS_DATA_SAVE_ON_SUCCESS' env: 'EXECUTIONS_DATA_SAVE_ON_SUCCESS',
}, },
// If the executions of workflows which got started via the editor // If the executions of workflows which got started via the editor
@@ -211,7 +211,7 @@ const config = convict({
doc: 'Save data of executions when started manually via editor', doc: 'Save data of executions when started manually via editor',
format: 'Boolean', format: 'Boolean',
default: false, default: false,
env: 'EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS' env: 'EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS',
}, },
// To not exceed the database's capacity and keep its size moderate // To not exceed the database's capacity and keep its size moderate
@@ -223,19 +223,19 @@ const config = convict({
doc: 'Delete data of past executions on a rolling basis', doc: 'Delete data of past executions on a rolling basis',
format: 'Boolean', format: 'Boolean',
default: false, default: false,
env: 'EXECUTIONS_DATA_PRUNE' env: 'EXECUTIONS_DATA_PRUNE',
}, },
pruneDataMaxAge: { pruneDataMaxAge: {
doc: 'How old (hours) the execution data has to be to get deleted', doc: 'How old (hours) the execution data has to be to get deleted',
format: Number, format: Number,
default: 336, default: 336,
env: 'EXECUTIONS_DATA_MAX_AGE' env: 'EXECUTIONS_DATA_MAX_AGE',
}, },
pruneDataTimeout: { pruneDataTimeout: {
doc: 'Timeout (seconds) after execution data has been pruned', doc: 'Timeout (seconds) after execution data has been pruned',
format: Number, format: Number,
default: 3600, default: 3600,
env: 'EXECUTIONS_DATA_PRUNE_TIMEOUT' env: 'EXECUTIONS_DATA_PRUNE_TIMEOUT',
}, },
}, },
@@ -248,7 +248,7 @@ const config = convict({
doc: 'The timezone to use', doc: 'The timezone to use',
format: '*', format: '*',
default: 'America/New_York', default: 'America/New_York',
env: 'GENERIC_TIMEZONE' env: 'GENERIC_TIMEZONE',
}, },
}, },
@@ -258,66 +258,78 @@ const config = convict({
default: '/', default: '/',
arg: 'path', arg: 'path',
env: 'N8N_PATH', env: 'N8N_PATH',
doc: 'Path n8n is deployed to' doc: 'Path n8n is deployed to',
}, },
host: { host: {
format: String, format: String,
default: 'localhost', default: 'localhost',
arg: 'host', arg: 'host',
env: 'N8N_HOST', env: 'N8N_HOST',
doc: 'Host name n8n can be reached' doc: 'Host name n8n can be reached',
}, },
port: { port: {
format: Number, format: Number,
default: 5678, default: 5678,
arg: 'port', arg: 'port',
env: 'N8N_PORT', env: 'N8N_PORT',
doc: 'HTTP port n8n can be reached' doc: 'HTTP port n8n can be reached',
}, },
listen_address: { listen_address: {
format: String, format: String,
default: '0.0.0.0', default: '0.0.0.0',
env: 'N8N_LISTEN_ADDRESS', env: 'N8N_LISTEN_ADDRESS',
doc: 'IP address n8n should listen on' doc: 'IP address n8n should listen on',
}, },
protocol: { protocol: {
format: ['http', 'https'], format: ['http', 'https'],
default: 'http', default: 'http',
env: 'N8N_PROTOCOL', env: 'N8N_PROTOCOL',
doc: 'HTTP Protocol via which n8n can be reached' doc: 'HTTP Protocol via which n8n can be reached',
}, },
ssl_key: { ssl_key: {
format: String, format: String,
default: '', default: '',
env: 'N8N_SSL_KEY', env: 'N8N_SSL_KEY',
doc: 'SSL Key for HTTPS Protocol' doc: 'SSL Key for HTTPS Protocol',
}, },
ssl_cert: { ssl_cert: {
format: String, format: String,
default: '', default: '',
env: 'N8N_SSL_CERT', env: 'N8N_SSL_CERT',
doc: 'SSL Cert for HTTPS Protocol' doc: 'SSL Cert for HTTPS Protocol',
}, },
security: { security: {
excludeEndpoints: {
doc: 'Additional endpoints to exclude auth checks. Multiple endpoints can be separated by colon (":")',
format: String,
default: '',
env: 'N8N_AUTH_EXCLUDE_ENDPOINTS',
},
basicAuth: { basicAuth: {
active: { active: {
format: 'Boolean', format: 'Boolean',
default: false, default: false,
env: 'N8N_BASIC_AUTH_ACTIVE', env: 'N8N_BASIC_AUTH_ACTIVE',
doc: 'If basic auth should be activated for editor and REST-API' doc: 'If basic auth should be activated for editor and REST-API',
}, },
user: { user: {
format: String, format: String,
default: '', default: '',
env: 'N8N_BASIC_AUTH_USER', env: 'N8N_BASIC_AUTH_USER',
doc: 'The name of the basic auth user' doc: 'The name of the basic auth user',
}, },
password: { password: {
format: String, format: String,
default: '', default: '',
env: 'N8N_BASIC_AUTH_PASSWORD', env: 'N8N_BASIC_AUTH_PASSWORD',
doc: 'The password of the basic auth user' doc: 'The password of the basic auth user',
},
hash: {
format: 'Boolean',
default: false,
env: 'N8N_BASIC_AUTH_HASH',
doc: 'If password for basic auth is hashed',
}, },
}, },
jwtAuth: { jwtAuth: {
@@ -325,49 +337,49 @@ const config = convict({
format: 'Boolean', format: 'Boolean',
default: false, default: false,
env: 'N8N_JWT_AUTH_ACTIVE', env: 'N8N_JWT_AUTH_ACTIVE',
doc: 'If JWT auth should be activated for editor and REST-API' doc: 'If JWT auth should be activated for editor and REST-API',
}, },
jwtHeader: { jwtHeader: {
format: String, format: String,
default: '', default: '',
env: 'N8N_JWT_AUTH_HEADER', env: 'N8N_JWT_AUTH_HEADER',
doc: 'The request header containing a signed JWT' doc: 'The request header containing a signed JWT',
}, },
jwtHeaderValuePrefix: { jwtHeaderValuePrefix: {
format: String, format: String,
default: '', default: '',
env: 'N8N_JWT_AUTH_HEADER_VALUE_PREFIX', env: 'N8N_JWT_AUTH_HEADER_VALUE_PREFIX',
doc: 'The request header value prefix to strip (optional)' doc: 'The request header value prefix to strip (optional)',
}, },
jwksUri: { jwksUri: {
format: String, format: String,
default: '', default: '',
env: 'N8N_JWKS_URI', env: 'N8N_JWKS_URI',
doc: 'The URI to fetch JWK Set for JWT authentication' doc: 'The URI to fetch JWK Set for JWT authentication',
}, },
jwtIssuer: { jwtIssuer: {
format: String, format: String,
default: '', default: '',
env: 'N8N_JWT_ISSUER', env: 'N8N_JWT_ISSUER',
doc: 'JWT issuer to expect (optional)' doc: 'JWT issuer to expect (optional)',
}, },
jwtNamespace: { jwtNamespace: {
format: String, format: String,
default: '', default: '',
env: 'N8N_JWT_NAMESPACE', env: 'N8N_JWT_NAMESPACE',
doc: 'JWT namespace to expect (optional)' doc: 'JWT namespace to expect (optional)',
}, },
jwtAllowedTenantKey: { jwtAllowedTenantKey: {
format: String, format: String,
default: '', default: '',
env: 'N8N_JWT_ALLOWED_TENANT_KEY', env: 'N8N_JWT_ALLOWED_TENANT_KEY',
doc: 'JWT tenant key name to inspect within JWT namespace (optional)' doc: 'JWT tenant key name to inspect within JWT namespace (optional)',
}, },
jwtAllowedTenant: { jwtAllowedTenant: {
format: String, format: String,
default: '', default: '',
env: 'N8N_JWT_ALLOWED_TENANT', env: 'N8N_JWT_ALLOWED_TENANT',
doc: 'JWT tenant to allow (optional)' doc: 'JWT tenant to allow (optional)',
}, },
}, },
}, },
@@ -377,19 +389,19 @@ const config = convict({
format: String, format: String,
default: 'rest', default: 'rest',
env: 'N8N_ENDPOINT_REST', env: 'N8N_ENDPOINT_REST',
doc: 'Path for rest endpoint' doc: 'Path for rest endpoint',
}, },
webhook: { webhook: {
format: String, format: String,
default: 'webhook', default: 'webhook',
env: 'N8N_ENDPOINT_WEBHOOK', env: 'N8N_ENDPOINT_WEBHOOK',
doc: 'Path for webhook endpoint' doc: 'Path for webhook endpoint',
}, },
webhookTest: { webhookTest: {
format: String, format: String,
default: 'webhook-test', default: 'webhook-test',
env: 'N8N_ENDPOINT_WEBHOOK_TEST', env: 'N8N_ENDPOINT_WEBHOOK_TEST',
doc: 'Path for test-webhook endpoint' doc: 'Path for test-webhook endpoint',
}, },
}, },
@@ -397,7 +409,7 @@ const config = convict({
doc: 'Files containing external hooks. Multiple files can be separated by colon (":")', doc: 'Files containing external hooks. Multiple files can be separated by colon (":")',
format: String, format: String,
default: '', default: '',
env: 'EXTERNAL_HOOK_FILES' env: 'EXTERNAL_HOOK_FILES',
}, },
nodes: { nodes: {
@@ -421,13 +433,13 @@ const config = convict({
} }
}, },
default: '[]', default: '[]',
env: 'NODES_EXCLUDE' env: 'NODES_EXCLUDE',
}, },
errorTriggerType: { errorTriggerType: {
doc: 'Node Type to use as Error Trigger', doc: 'Node Type to use as Error Trigger',
format: String, format: String,
default: 'n8n-nodes-base.errorTrigger', default: 'n8n-nodes-base.errorTrigger',
env: 'NODES_ERROR_TRIGGER_TYPE' env: 'NODES_ERROR_TRIGGER_TYPE',
}, },
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.80.0", "version": "0.89.2",
"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",
@@ -28,6 +28,7 @@
"start:windows": "cd bin && n8n", "start:windows": "cd bin && n8n",
"test": "jest", "test": "jest",
"tslint": "tslint -p tsconfig.json -c tslint.json", "tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"watch": "tsc --watch", "watch": "tsc --watch",
"typeorm": "ts-node ./node_modules/typeorm/cli.js" "typeorm": "ts-node ./node_modules/typeorm/cli.js"
}, },
@@ -54,33 +55,35 @@
"devDependencies": { "devDependencies": {
"@oclif/dev-cli": "^1.22.2", "@oclif/dev-cli": "^1.22.2",
"@types/basic-auth": "^1.1.2", "@types/basic-auth": "^1.1.2",
"@types/bcryptjs": "^2.4.1",
"@types/compression": "1.0.1", "@types/compression": "1.0.1",
"@types/connect-history-api-fallback": "^1.3.1", "@types/connect-history-api-fallback": "^1.3.1",
"@types/convict": "^4.2.1", "@types/convict": "^4.2.1",
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/jest": "^25.2.1", "@types/jest": "^26.0.13",
"@types/localtunnel": "^1.9.0", "@types/localtunnel": "^1.9.0",
"@types/lodash.get": "^4.4.6", "@types/lodash.get": "^4.4.6",
"@types/node": "^14.0.27", "@types/node": "14.0.27",
"@types/open": "^6.1.0", "@types/open": "^6.1.0",
"@types/parseurl": "^1.3.1", "@types/parseurl": "^1.3.1",
"@types/request-promise-native": "~1.0.15", "@types/request-promise-native": "~1.0.15",
"concurrently": "^5.1.0", "concurrently": "^5.1.0",
"jest": "^24.9.0", "jest": "^26.4.2",
"nodemon": "^2.0.2", "nodemon": "^2.0.2",
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"run-script-os": "^1.0.7", "run-script-os": "^1.0.7",
"ts-jest": "^25.4.0", "ts-jest": "^26.3.0",
"ts-node": "^8.9.1",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~3.7.4", "typescript": "~3.9.7"
"ts-node": "^8.9.1"
}, },
"dependencies": { "dependencies": {
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
"@oclif/errors": "^1.2.2", "@oclif/errors": "^1.2.2",
"@types/jsonwebtoken": "^8.3.4", "@types/jsonwebtoken": "^8.3.4",
"basic-auth": "^2.0.1", "basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"body-parser-xml": "^1.1.0", "body-parser-xml": "^1.1.0",
"client-oauth2": "^4.2.5", "client-oauth2": "^4.2.5",
@@ -95,15 +98,15 @@
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"jwks-rsa": "^1.6.0", "jwks-rsa": "~1.9.0",
"localtunnel": "^2.0.0", "localtunnel": "^2.0.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mongodb": "^3.5.5", "mongodb": "^3.5.5",
"mysql2": "^2.0.1", "mysql2": "~2.1.0",
"n8n-core": "~0.44.0", "n8n-core": "~0.48.0",
"n8n-editor-ui": "~0.55.0", "n8n-editor-ui": "~0.60.0",
"n8n-nodes-base": "~0.75.0", "n8n-nodes-base": "~0.85.0",
"n8n-workflow": "~0.39.0", "n8n-workflow": "~0.42.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"open": "^7.0.0", "open": "^7.0.0",
"pg": "^8.3.0", "pg": "^8.3.0",

View File

@@ -7,8 +7,8 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
IExecutionsCurrentSummary,
IExecutingWorkflowData, IExecutingWorkflowData,
IExecutionsCurrentSummary,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
} from '.'; } from '.';

View File

@@ -1,17 +1,17 @@
import { import {
IActivationError,
Db, Db,
NodeTypes, IActivationError,
IResponseCallbackData, IResponseCallbackData,
IWebhookDb,
IWorkflowDb, IWorkflowDb,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
NodeTypes,
ResponseHelper, ResponseHelper,
WebhookHelpers, WebhookHelpers,
WorkflowCredentials, WorkflowCredentials,
WorkflowExecuteAdditionalData,
WorkflowHelpers, WorkflowHelpers,
WorkflowRunner, WorkflowRunner,
WorkflowExecuteAdditionalData,
IWebhookDb,
} from './'; } from './';
import { import {
@@ -26,8 +26,8 @@ import {
INode, INode,
INodeExecutionData, INodeExecutionData,
IRunExecutionData, IRunExecutionData,
NodeHelpers,
IWorkflowExecuteAdditionalData as IWorkflowExecuteAdditionalDataWorkflow, IWorkflowExecuteAdditionalData as IWorkflowExecuteAdditionalDataWorkflow,
NodeHelpers,
WebhookHttpMethod, WebhookHttpMethod,
Workflow, Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
@@ -181,8 +181,9 @@ export class ActiveWorkflowRunner {
* @returns {string[]} * @returns {string[]}
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
getActiveWorkflows(): Promise<IWorkflowDb[]> { async getActiveWorkflows(): Promise<IWorkflowDb[]> {
return Db.collections.Workflow?.find({ where: { active: true }, select: ['id'] }) as Promise<IWorkflowDb[]>; const activeWorkflows = await Db.collections.Workflow?.find({ where: { active: true }, select: ['id'] }) as IWorkflowDb[];
return activeWorkflows.filter(workflow => this.activationErrors[workflow.id.toString()] === undefined);
} }
@@ -234,7 +235,7 @@ export class ActiveWorkflowRunner {
path = node.parameters.path as string; path = node.parameters.path as string;
if (node.parameters.path === undefined) { if (node.parameters.path === undefined) {
path = workflow.getSimpleParameterValue(node, webhookData.webhookDescription['path']) as string | undefined; path = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['path']) as string | undefined;
if (path === undefined) { if (path === undefined) {
// TODO: Use a proper logger // TODO: Use a proper logger
@@ -243,7 +244,7 @@ export class ActiveWorkflowRunner {
} }
} }
const isFullPath: boolean = workflow.getSimpleParameterValue(node, webhookData.webhookDescription['isFullPath'], false) as boolean; const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['isFullPath'], false) as boolean;
const webhook = { const webhook = {
workflowId: webhookData.workflowId, workflowId: webhookData.workflowId,
@@ -257,17 +258,20 @@ export class ActiveWorkflowRunner {
await Db.collections.Webhook?.insert(webhook); await Db.collections.Webhook?.insert(webhook);
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, false); const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, false);
if (webhookExists === false) { if (webhookExists !== true) {
// If webhook does not exist yet create it // If webhook does not exist yet create it
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, false); await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, false);
} }
} catch (error) { } catch (error) {
try {
await this.removeWorkflowWebhooks(workflow.id as string);
} catch (error) {
console.error(`Could not remove webhooks of workflow "${workflow.id}" because of error: "${error.message}"`);
}
let errorMessage = ''; let errorMessage = '';
await Db.collections.Webhook?.delete({ workflowId: workflow.id });
// if it's a workflow from the the insert // if it's a workflow from the the insert
// TODO check if there is standard error code for deplicate key violation that works // TODO check if there is standard error code for deplicate key violation that works
// with all databases // with all databases
@@ -317,6 +321,8 @@ export class ActiveWorkflowRunner {
await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, false); await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, false);
} }
await WorkflowHelpers.saveStaticData(workflow);
// if it's a mongo objectId convert it to string // if it's a mongo objectId convert it to string
if (typeof workflowData.id === 'object') { if (typeof workflowData.id === 'object') {
workflowData.id = workflowData.id.toString(); workflowData.id = workflowData.id.toString();
@@ -346,8 +352,8 @@ export class ActiveWorkflowRunner {
node, node,
data: { data: {
main: data, main: data,
} },
} },
]; ];
const executionData: IRunExecutionData = { const executionData: IRunExecutionData = {
@@ -411,7 +417,7 @@ export class ActiveWorkflowRunner {
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode); const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode);
returnFunctions.emit = (data: INodeExecutionData[][]): void => { returnFunctions.emit = (data: INodeExecutionData[][]): void => {
WorkflowHelpers.saveStaticData(workflow); WorkflowHelpers.saveStaticData(workflow);
this.runWorkflow(workflowData, node, data, additionalData, mode); this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) => console.error(err));
}; };
return returnFunctions; return returnFunctions;
}); });
@@ -495,7 +501,11 @@ export class ActiveWorkflowRunner {
if (this.activeWorkflows !== null) { if (this.activeWorkflows !== null) {
// Remove all the webhooks of the workflow // Remove all the webhooks of the workflow
await this.removeWorkflowWebhooks(workflowId); try {
await this.removeWorkflowWebhooks(workflowId);
} catch (error) {
console.error(`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`);
}
if (this.activationErrors[workflowId] !== undefined) { if (this.activationErrors[workflowId] !== undefined) {
// If there were any activation errors delete them // If there were any activation errors delete them

View File

@@ -5,9 +5,14 @@ import {
import { import {
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject,
ICredentialsHelper, ICredentialsHelper,
INode,
INodeParameters, INodeParameters,
INodeProperties, INodeProperties,
INodeType,
INodeTypeData,
INodeTypes,
NodeHelpers, NodeHelpers,
Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@@ -18,6 +23,19 @@ import {
} from './'; } from './';
const mockNodeTypes: INodeTypes = {
nodeTypes: {},
init: async (nodeTypes?: INodeTypeData): Promise<void> => { },
getAll: (): INodeType[] => {
// Does not get used in Workflow so no need to return it
return [];
},
getByName: (nodeType: string): INodeType | undefined => {
return undefined;
},
};
export class CredentialsHelper extends ICredentialsHelper { export class CredentialsHelper extends ICredentialsHelper {
/** /**
@@ -107,7 +125,7 @@ export class CredentialsHelper extends ICredentialsHelper {
const credentialsProperties = this.getCredentialsProperties(type); const credentialsProperties = this.getCredentialsProperties(type);
// Add the default credential values // Add the default credential values
const decryptedData = NodeHelpers.getNodeParameters(credentialsProperties, decryptedDataOriginal as INodeParameters, true, false) as ICredentialDataDecryptedObject; let decryptedData = NodeHelpers.getNodeParameters(credentialsProperties, decryptedDataOriginal as INodeParameters, true, false) as ICredentialDataDecryptedObject;
if (decryptedDataOriginal.oauthTokenData !== undefined) { if (decryptedDataOriginal.oauthTokenData !== undefined) {
// The OAuth data gets removed as it is not defined specifically as a parameter // The OAuth data gets removed as it is not defined specifically as a parameter
@@ -115,6 +133,18 @@ export class CredentialsHelper extends ICredentialsHelper {
decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData; decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData;
} }
const mockNode: INode = {
name: '',
typeVersion: 1,
type: 'mock',
position: [0, 0],
parameters: decryptedData as INodeParameters,
};
const workflow = new Workflow({ nodes: [mockNode], connections: {}, active: false, nodeTypes: mockNodeTypes});
// Resolve expressions if any are set
decryptedData = workflow.expression.getComplexParameterValue(mockNode, decryptedData as INodeParameters, undefined) as ICredentialDataDecryptedObject;
// Load and apply the credentials overwrites if any exist // Load and apply the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites(); const credentialsOverwrites = CredentialsOverwrites();
return credentialsOverwrites.applyOverwrite(type, decryptedData); return credentialsOverwrites.applyOverwrite(type, decryptedData);

View File

@@ -3,32 +3,53 @@ import {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
ICredentialsOverwrite, CredentialTypes,
GenericHelpers, GenericHelpers,
ICredentialsOverwrite,
} from './'; } from './';
class CredentialsOverwritesClass { class CredentialsOverwritesClass {
private credentialTypes = CredentialTypes();
private overwriteData: ICredentialsOverwrite = {}; private overwriteData: ICredentialsOverwrite = {};
private resolvedTypes: string[] = [];
async init(overwriteData?: ICredentialsOverwrite) { async init(overwriteData?: ICredentialsOverwrite) {
if (overwriteData !== undefined) { if (overwriteData !== undefined) {
// If data is already given it can directly be set instead of // If data is already given it can directly be set instead of
// loaded from environment // loaded from environment
this.overwriteData = overwriteData; this.__setData(JSON.parse(JSON.stringify(overwriteData)));
return; return;
} }
const data = await GenericHelpers.getConfigValue('credentials.overwrite.data') as string; const data = await GenericHelpers.getConfigValue('credentials.overwrite.data') as string;
try { try {
this.overwriteData = JSON.parse(data); const overwriteData = JSON.parse(data);
this.__setData(overwriteData);
} catch (error) { } catch (error) {
throw new Error(`The credentials-overwrite is not valid JSON.`); throw new Error(`The credentials-overwrite is not valid JSON.`);
} }
} }
__setData(overwriteData: ICredentialsOverwrite) {
this.overwriteData = overwriteData;
for (const credentialTypeData of this.credentialTypes.getAll()) {
const type = credentialTypeData.name;
const overwrites = this.__getExtended(type);
if (overwrites && Object.keys(overwrites).length) {
this.overwriteData[type] = overwrites;
}
}
}
applyOverwrite(type: string, data: ICredentialDataDecryptedObject) { applyOverwrite(type: string, data: ICredentialDataDecryptedObject) {
const overwrites = this.get(type); const overwrites = this.get(type);
@@ -48,10 +69,45 @@ class CredentialsOverwritesClass {
return returnData; return returnData;
} }
__getExtended(type: string): ICredentialDataDecryptedObject | undefined {
if (this.resolvedTypes.includes(type)) {
// Type got already resolved and can so returned directly
return this.overwriteData[type];
}
const credentialTypeData = this.credentialTypes.getByName(type);
if (credentialTypeData === undefined) {
throw new Error(`The credentials of type "${type}" are not known.`);
}
if (credentialTypeData.extends === undefined) {
this.resolvedTypes.push(type);
return this.overwriteData[type];
}
const overwrites: ICredentialDataDecryptedObject = {};
for (const credentialsTypeName of credentialTypeData.extends) {
Object.assign(overwrites, this.__getExtended(credentialsTypeName));
}
if (this.overwriteData[type] !== undefined) {
Object.assign(overwrites, this.overwriteData[type]);
}
this.resolvedTypes.push(type);
return overwrites;
}
get(type: string): ICredentialDataDecryptedObject | undefined { get(type: string): ICredentialDataDecryptedObject | undefined {
return this.overwriteData[type]; return this.overwriteData[type];
} }
getAll(): ICredentialsOverwrite { getAll(): ICredentialsOverwrite {
return this.overwriteData; return this.overwriteData;
} }

View File

@@ -33,27 +33,27 @@ export let collections: IDatabaseCollections = {
}; };
import { import {
CreateIndexStoppedAt1594828256133,
InitialMigration1587669153312, InitialMigration1587669153312,
WebhookModel1589476000887, WebhookModel1589476000887,
CreateIndexStoppedAt1594828256133,
} from './databases/postgresdb/migrations'; } from './databases/postgresdb/migrations';
import { import {
CreateIndexStoppedAt1594910478695,
InitialMigration1587563438936, InitialMigration1587563438936,
WebhookModel1592679094242, WebhookModel1592679094242,
CreateIndexStoppedAt1594910478695,
} from './databases/mongodb/migrations'; } from './databases/mongodb/migrations';
import { import {
CreateIndexStoppedAt1594902918301,
InitialMigration1588157391238, InitialMigration1588157391238,
WebhookModel1592447867632, WebhookModel1592447867632,
CreateIndexStoppedAt1594902918301,
} from './databases/mysqldb/migrations'; } from './databases/mysqldb/migrations';
import { import {
CreateIndexStoppedAt1594825041918,
InitialMigration1588102412422, InitialMigration1588102412422,
WebhookModel1592445003908, WebhookModel1592445003908,
CreateIndexStoppedAt1594825041918,
} from './databases/sqlite/migrations'; } from './databases/sqlite/migrations';
import * as path from 'path'; import * as path from 'path';
@@ -154,7 +154,7 @@ export async function init(): Promise<IDatabaseCollections> {
migrations: [ migrations: [
InitialMigration1588102412422, InitialMigration1588102412422,
WebhookModel1592445003908, WebhookModel1592445003908,
CreateIndexStoppedAt1594825041918 CreateIndexStoppedAt1594825041918,
], ],
migrationsRun: true, migrationsRun: true,
migrationsTableName: `${entityPrefix}migrations`, migrationsTableName: `${entityPrefix}migrations`,

View File

@@ -1,7 +1,7 @@
import { import {
Db, Db,
IExternalHooksFunctions,
IExternalHooksClass, IExternalHooksClass,
IExternalHooksFunctions,
} from './'; } from './';
import * as config from '../config'; import * as config from '../config';
@@ -64,6 +64,10 @@ class ExternalHooksClass implements IExternalHooksClass {
} }
} }
exists(hookName: string): boolean {
return !!this.externalHooks[hookName];
}
} }

View File

@@ -288,6 +288,10 @@ export interface IN8nUISettings {
saveManualExecutions: boolean; saveManualExecutions: boolean;
executionTimeout: number; executionTimeout: number;
maxExecutionTimeout: number; maxExecutionTimeout: number;
oauthCallbackUrls: {
oauth1: string;
oauth2: string;
};
timezone: string; timezone: string;
urlBaseWebhook: string; urlBaseWebhook: string;
versionCli: string; versionCli: string;

View File

@@ -1,7 +1,7 @@
import { import {
INodeType, INodeType,
INodeTypes,
INodeTypeData, INodeTypeData,
INodeTypes,
NodeHelpers, NodeHelpers,
} from 'n8n-workflow'; } from 'n8n-workflow';

View File

@@ -67,7 +67,7 @@ export function sendSuccessResponse(res: Response, data: any, raw?: boolean, res
res.json(data); res.json(data);
} else { } else {
res.json({ res.json({
data data,
}); });
} }
} }
@@ -183,7 +183,7 @@ export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb):
mode: fullExecutionData.mode, mode: fullExecutionData.mode,
startedAt: fullExecutionData.startedAt, startedAt: fullExecutionData.startedAt,
stoppedAt: fullExecutionData.stoppedAt, stoppedAt: fullExecutionData.stoppedAt,
finished: fullExecutionData.finished ? fullExecutionData.finished : false finished: fullExecutionData.finished ? fullExecutionData.finished : false,
}); });
return returnData; return returnData;

View File

@@ -20,20 +20,24 @@ import { RequestOptions } from 'oauth-1.0a';
import * as csrf from 'csrf'; import * as csrf from 'csrf';
import * as requestPromise from 'request-promise-native'; import * as requestPromise from 'request-promise-native';
import { createHmac } from 'crypto'; import { createHmac } from 'crypto';
import { compare } from 'bcryptjs';
import { import {
ActiveExecutions, ActiveExecutions,
ActiveWorkflowRunner, ActiveWorkflowRunner,
CredentialsHelper, CredentialsHelper,
CredentialsOverwrites,
CredentialTypes, CredentialTypes,
Db, Db,
ExternalHooks, ExternalHooks,
GenericHelpers,
IActivationError, IActivationError,
ICustomRequest,
ICredentialsDb, ICredentialsDb,
ICredentialsDecryptedDb, ICredentialsDecryptedDb,
ICredentialsDecryptedResponse, ICredentialsDecryptedResponse,
ICredentialsOverwrite,
ICredentialsResponse, ICredentialsResponse,
ICustomRequest,
IExecutionDeleteFilter, IExecutionDeleteFilter,
IExecutionFlatted, IExecutionFlatted,
IExecutionFlattedDb, IExecutionFlattedDb,
@@ -46,21 +50,18 @@ import {
IN8nUISettings, IN8nUISettings,
IPackageVersions, IPackageVersions,
IWorkflowBase, IWorkflowBase,
IWorkflowShortResponse,
IWorkflowResponse,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
IWorkflowResponse,
IWorkflowShortResponse,
LoadNodesAndCredentials,
NodeTypes, NodeTypes,
Push, Push,
ResponseHelper, ResponseHelper,
TestWebhooks, TestWebhooks,
WorkflowCredentials,
WebhookHelpers, WebhookHelpers,
WorkflowCredentials,
WorkflowExecuteAdditionalData, WorkflowExecuteAdditionalData,
WorkflowRunner, WorkflowRunner,
GenericHelpers,
CredentialsOverwrites,
ICredentialsOverwrite,
LoadNodesAndCredentials,
} from './'; } from './';
import { import {
@@ -74,9 +75,9 @@ import {
ICredentialType, ICredentialType,
IDataObject, IDataObject,
INodeCredentials, INodeCredentials,
INodeTypeDescription,
INodeParameters, INodeParameters,
INodePropertyOptions, INodePropertyOptions,
INodeTypeDescription,
IRunData, IRunData,
IWorkflowCredentials, IWorkflowCredentials,
Workflow, Workflow,
@@ -120,7 +121,7 @@ class App {
push: Push.Push; push: Push.Push;
versions: IPackageVersions | undefined; versions: IPackageVersions | undefined;
restEndpoint: string; restEndpoint: string;
frontendSettings: IN8nUISettings;
protocol: string; protocol: string;
sslKey: string; sslKey: string;
sslCert: string; sslCert: string;
@@ -154,6 +155,25 @@ class App {
this.presetCredentialsLoaded = false; this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string; this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string;
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
this.frontendSettings = {
endpointWebhook: this.endpointWebhook,
endpointWebhookTest: this.endpointWebhookTest,
saveDataErrorExecution: this.saveDataErrorExecution,
saveDataSuccessExecution: this.saveDataSuccessExecution,
saveManualExecutions: this.saveManualExecutions,
executionTimeout: this.executionTimeout,
maxExecutionTimeout: this.maxExecutionTimeout,
timezone: this.timezone,
urlBaseWebhook,
versionCli: '',
oauthCallbackUrls: {
'oauth1': urlBaseWebhook + `${this.restEndpoint}/oauth1-credential/callback`,
'oauth2': urlBaseWebhook + `${this.restEndpoint}/oauth2-credential/callback`,
},
};
} }
@@ -171,7 +191,16 @@ class App {
async config(): Promise<void> { async config(): Promise<void> {
this.versions = await GenericHelpers.getVersions(); this.versions = await GenericHelpers.getVersions();
const authIgnoreRegex = new RegExp(`^\/(healthz|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`); this.frontendSettings.versionCli = this.versions.cli;
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
const excludeEndpoints = config.get('security.excludeEndpoints') as string;
const ignoredEndpoints = ['healthz', this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials];
ignoredEndpoints.push.apply(ignoredEndpoints, excludeEndpoints.split(':'));
const authIgnoreRegex = new RegExp(`^\/(${_(ignoredEndpoints).compact().join('|')})\/?.*$`);
// Check for basic auth credentials if activated // 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;
@@ -186,7 +215,11 @@ class App {
throw new Error('Basic auth is activated but no password got defined. Please set one!'); throw new Error('Basic auth is activated but no password got defined. Please set one!');
} }
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { const basicAuthHashEnabled = await GenericHelpers.getConfigValue('security.basicAuth.hash') as boolean;
let validPassword: null | string = null;
this.app.use(async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.url.match(authIgnoreRegex)) { if (req.url.match(authIgnoreRegex)) {
return next(); return next();
} }
@@ -198,12 +231,27 @@ class App {
return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization is required!'); return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization is required!');
} }
if (basicAuthData.name !== basicAuthUser || basicAuthData.pass !== basicAuthPassword) { if (basicAuthData.name === basicAuthUser) {
// Provided authentication data is wrong if (basicAuthHashEnabled === true) {
return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization data is wrong!'); if (validPassword === null && await compare(basicAuthData.pass, basicAuthPassword)) {
// Password is valid so save for future requests
validPassword = basicAuthData.pass;
}
if (validPassword === basicAuthData.pass && validPassword !== null) {
// Provided hash is correct
return next();
}
} else {
if (basicAuthData.pass === basicAuthPassword) {
// Provided password is correct
return next();
}
}
} }
next(); // Provided authentication data is wrong
return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization data is wrong!');
}); });
} }
@@ -265,7 +313,7 @@ class App {
const jwtVerifyOptions: jwt.VerifyOptions = { const jwtVerifyOptions: jwt.VerifyOptions = {
issuer: jwtIssuer !== '' ? jwtIssuer : undefined, issuer: jwtIssuer !== '' ? jwtIssuer : undefined,
ignoreExpiration: false ignoreExpiration: false,
}; };
jwt.verify(token, getKey, jwtVerifyOptions, (err: jwt.VerifyErrors, decoded: object) => { jwt.verify(token, getKey, jwtVerifyOptions, (err: jwt.VerifyErrors, decoded: object) => {
@@ -307,7 +355,7 @@ class App {
limit: '16mb', verify: (req, res, buf) => { limit: '16mb', verify: (req, res, buf) => {
// @ts-ignore // @ts-ignore
req.rawBody = buf; req.rawBody = buf;
} },
})); }));
// Support application/xml type post data // Support application/xml type post data
@@ -317,14 +365,14 @@ class App {
normalize: true, // Trim whitespace inside text nodes normalize: true, // Trim whitespace inside text nodes
normalizeTags: true, // Transform tags to lowercase 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({ this.app.use(bodyParser.text({
limit: '16mb', verify: (req, res, buf) => { limit: '16mb', verify: (req, res, buf) => {
// @ts-ignore // @ts-ignore
req.rawBody = buf; req.rawBody = buf;
} },
})); }));
// Make sure that Vue history mode works properly // Make sure that Vue history mode works properly
@@ -334,9 +382,9 @@ class App {
from: new RegExp(`^\/(${this.restEndpoint}|healthz|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`), from: new RegExp(`^\/(${this.restEndpoint}|healthz|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`),
to: (context) => { to: (context) => {
return context.parsedUrl!.pathname!.toString(); return context.parsedUrl!.pathname!.toString();
} },
} },
] ],
})); }));
//support application/x-www-form-urlencoded post data //support application/x-www-form-urlencoded post data
@@ -344,7 +392,7 @@ class App {
verify: (req, res, buf) => { verify: (req, res, buf) => {
// @ts-ignore // @ts-ignore
req.rawBody = buf; req.rawBody = buf;
} },
})); }));
if (process.env['NODE_ENV'] !== 'production') { if (process.env['NODE_ENV'] !== 'production') {
@@ -532,6 +580,7 @@ class App {
newWorkflowData.updatedAt = this.getCurrentDate(); newWorkflowData.updatedAt = this.getCurrentDate();
await Db.collections.Workflow!.update(id, newWorkflowData); await Db.collections.Workflow!.update(id, newWorkflowData);
await this.externalHooks.run('workflow.afterUpdate', [newWorkflowData]);
// We sadly get nothing back from "update". Neither if it updated a record // We sadly get nothing back from "update". Neither if it updated a record
// nor the new value. So query now the hopefully updated entry. // nor the new value. So query now the hopefully updated entry.
@@ -580,6 +629,7 @@ class App {
} }
await Db.collections.Workflow!.delete(id); await Db.collections.Workflow!.delete(id);
await this.externalHooks.run('workflow.afterDelete', [id]);
return true; return true;
})); }));
@@ -665,13 +715,36 @@ class App {
const allNodes = nodeTypes.getAll(); const allNodes = nodeTypes.getAll();
allNodes.forEach((nodeData) => { allNodes.forEach((nodeData) => {
returnData.push(nodeData.description); // Make a copy of the object. If we don't do this, then when
// The method below is called the properties are removed for good
// This happens because nodes are returned as reference.
const nodeInfo: INodeTypeDescription = {...nodeData.description};
if (req.query.includeProperties !== 'true') {
// @ts-ignore
delete nodeInfo.properties;
}
returnData.push(nodeInfo);
}); });
return returnData; return returnData;
})); }));
// Returns node information baesd on namese
this.app.post(`/${this.restEndpoint}/node-types`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<INodeTypeDescription[]> => {
const nodeNames = _.get(req, 'body.nodeNames', []) as string[];
const nodeTypes = NodeTypes();
return nodeNames.map(name => {
try {
return nodeTypes.getByName(name);
} catch (e) {
return undefined;
}
}).filter(nodeData => !!nodeData).map(nodeData => nodeData!.description);
}));
// ---------------------------------------- // ----------------------------------------
// Node-Types // Node-Types
@@ -1009,7 +1082,7 @@ class App {
const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string; const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string;
const oauth = new clientOAuth1({ const oAuthOptions: clientOAuth1.Options = {
consumer: { consumer: {
key: _.get(oauthCredentials, 'consumerKey') as string, key: _.get(oauthCredentials, 'consumerKey') as string,
secret: _.get(oauthCredentials, 'consumerSecret') as string, secret: _.get(oauthCredentials, 'consumerSecret') as string,
@@ -1021,16 +1094,20 @@ class App {
.update(base) .update(base)
.digest('base64'); .digest('base64');
}, },
}); };
const callback = `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth1-credential/callback?cid=${req.query.id}`; const oauthRequestData = {
oauth_callback: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth1-credential/callback?cid=${req.query.id}`,
};
await this.externalHooks.run('oauth1.authenticate', [oAuthOptions, oauthRequestData]);
const oauth = new clientOAuth1(oAuthOptions);
const options: RequestOptions = { const options: RequestOptions = {
method: 'POST', method: 'POST',
url: (_.get(oauthCredentials, 'requestTokenUrl') as string), url: (_.get(oauthCredentials, 'requestTokenUrl') as string),
data: { data: oauthRequestData,
oauth_callback: callback,
},
}; };
const data = oauth.toHeader(oauth.authorize(options as RequestOptions)); const data = oauth.toHeader(oauth.authorize(options as RequestOptions));
@@ -1099,7 +1176,7 @@ class App {
qs: { qs: {
oauth_token, oauth_token,
oauth_verifier, oauth_verifier,
} },
}; };
let oauthToken; let oauthToken;
@@ -1169,11 +1246,11 @@ class App {
const csrfSecret = token.secretSync(); const csrfSecret = token.secretSync();
const state = { const state = {
token: token.create(csrfSecret), token: token.create(csrfSecret),
cid: req.query.id cid: req.query.id,
}; };
const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64') as string; const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64') as string;
const oAuthObj = new clientOAuth2({ const oAuthOptions: clientOAuth2.Options = {
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,
@@ -1181,7 +1258,11 @@ class App {
redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`, redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`,
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','), scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','),
state: stateEncodedStr, state: stateEncodedStr,
}); };
await this.externalHooks.run('oauth2.authenticate', [oAuthOptions]);
const oAuthObj = new clientOAuth2(oAuthOptions);
// Encrypt the data // Encrypt the data
const credentials = new Credentials(result.name, result.type, result.nodesAccess); const credentials = new Credentials(result.name, result.type, result.nodesAccess);
@@ -1267,11 +1348,11 @@ class App {
const oAuth2Parameters = { const oAuth2Parameters = {
clientId: _.get(oauthCredentials, 'clientId') as string, clientId: _.get(oauthCredentials, 'clientId') as string,
clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string | undefined,
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: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`,
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',') scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','),
}; };
if (_.get(oauthCredentials, 'authentication', 'header') as string === 'body') { if (_.get(oauthCredentials, 'authentication', 'header') as string === 'body') {
@@ -1283,13 +1364,14 @@ class App {
}; };
delete oAuth2Parameters.clientSecret; delete oAuth2Parameters.clientSecret;
} }
const redirectUri = `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`;
await this.externalHooks.run('oauth2.callback', [oAuth2Parameters]);
const oAuthObj = new clientOAuth2(oAuth2Parameters); const oAuthObj = new clientOAuth2(oAuth2Parameters);
const queryParameters = req.originalUrl.split('?').splice(1, 1).join(''); const queryParameters = req.originalUrl.split('?').splice(1, 1).join('');
const oauthToken = await oAuthObj.code.getToken(`${redirectUri}?${queryParameters}`, options); const oauthToken = await oAuthObj.code.getToken(`${oAuth2Parameters.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);
@@ -1582,18 +1664,7 @@ class App {
// Returns the settings which are needed in the UI // Returns the settings which are needed in the UI
this.app.get(`/${this.restEndpoint}/settings`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IN8nUISettings> => { this.app.get(`/${this.restEndpoint}/settings`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IN8nUISettings> => {
return { return this.frontendSettings;
endpointWebhook: this.endpointWebhook,
endpointWebhookTest: this.endpointWebhookTest,
saveDataErrorExecution: this.saveDataErrorExecution,
saveDataSuccessExecution: this.saveDataSuccessExecution,
saveManualExecutions: this.saveManualExecutions,
executionTimeout: this.executionTimeout,
maxExecutionTimeout: this.maxExecutionTimeout,
timezone: this.timezone,
urlBaseWebhook: WebhookHelpers.getWebhookBaseUrl(),
versionCli: this.versions!.cli,
};
})); }));
@@ -1829,7 +1900,7 @@ class App {
// got used // got used
res.setHeader('Last-Modified', startTime); res.setHeader('Last-Modified', startTime);
} }
} },
})); }));
} }
@@ -1860,5 +1931,7 @@ export async function start(): Promise<void> {
const versions = await GenericHelpers.getVersions(); const versions = await GenericHelpers.getVersions();
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`); console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
console.log(`Version: ${versions.cli}`); console.log(`Version: ${versions.cli}`);
await app.externalHooks.run('n8n.ready', [app]);
}); });
} }

View File

@@ -3,11 +3,9 @@ import * as express from 'express';
import { import {
IResponseCallbackData, IResponseCallbackData,
IWorkflowDb, IWorkflowDb,
NodeTypes,
Push, Push,
ResponseHelper, ResponseHelper,
WebhookHelpers, WebhookHelpers,
WorkflowHelpers,
} from './'; } from './';
import { import {
@@ -31,6 +29,7 @@ export class TestWebhooks {
sessionId?: string; sessionId?: string;
timeout: NodeJS.Timeout, timeout: NodeJS.Timeout,
workflowData: IWorkflowDb; workflowData: IWorkflowDb;
workflow: Workflow;
}; };
} = {}; } = {};
private activeWebhooks: ActiveWebhooks | null = null; private activeWebhooks: ActiveWebhooks | null = null;
@@ -64,10 +63,13 @@ export class TestWebhooks {
const webhookKey = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path); const webhookKey = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path);
const workflowData = this.testWebhookData[webhookKey].workflowData; // TODO: Clean that duplication up one day and improve code generally
if (this.testWebhookData[webhookKey] === undefined) {
// The requested webhook is not registered
throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404);
}
const nodeTypes = NodeTypes(); const workflow = this.testWebhookData[webhookKey].workflow;
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 the node which has the webhook defined to know where to start from and to
// get additional data // get additional data
@@ -154,19 +156,26 @@ export class TestWebhooks {
}, 120000); }, 120000);
let key: string; let key: string;
const activatedKey: string[] = [];
for (const webhookData of webhooks) { for (const webhookData of webhooks) {
key = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path); key = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path);
await this.activeWebhooks!.add(workflow, webhookData, mode); activatedKey.push(key);
this.testWebhookData[key] = { this.testWebhookData[key] = {
sessionId, sessionId,
timeout, timeout,
workflow,
workflowData, workflowData,
}; };
// Save static data! try {
this.testWebhookData[key].workflowData.staticData = workflow.staticData; await this.activeWebhooks!.add(workflow, webhookData, mode);
} catch (error) {
activatedKey.forEach(deleteKey => delete this.testWebhookData[deleteKey] );
await this.activeWebhooks!.removeWorkflow(workflow);
throw error;
}
} }
return true; return true;
@@ -181,8 +190,6 @@ export class TestWebhooks {
* @memberof TestWebhooks * @memberof TestWebhooks
*/ */
cancelTestWebhook(workflowId: string): boolean { cancelTestWebhook(workflowId: string): boolean {
const nodeTypes = NodeTypes();
let foundWebhook = false; let foundWebhook = false;
for (const webhookKey of Object.keys(this.testWebhookData)) { for (const webhookKey of Object.keys(this.testWebhookData)) {
const webhookData = this.testWebhookData[webhookKey]; const webhookData = this.testWebhookData[webhookKey];
@@ -191,8 +198,6 @@ export class TestWebhooks {
continue; continue;
} }
foundWebhook = true;
clearTimeout(this.testWebhookData[webhookKey].timeout); clearTimeout(this.testWebhookData[webhookKey].timeout);
// Inform editor-ui that webhook got received // Inform editor-ui that webhook got received
@@ -205,12 +210,17 @@ export class TestWebhooks {
} }
} }
const workflowData = webhookData.workflowData; const workflow = this.testWebhookData[webhookKey].workflow;
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 // Remove the webhook
delete this.testWebhookData[webhookKey]; delete this.testWebhookData[webhookKey];
this.activeWebhooks!.removeWorkflow(workflow);
if (foundWebhook === false) {
// As it removes all webhooks of the workflow execute only once
this.activeWebhooks!.removeWorkflow(workflow);
}
foundWebhook = true;
} }
return foundWebhook; return foundWebhook;
@@ -225,14 +235,10 @@ export class TestWebhooks {
return; return;
} }
const nodeTypes = NodeTypes();
let workflowData: IWorkflowDb;
let workflow: Workflow; let workflow: Workflow;
const workflows: Workflow[] = []; const workflows: Workflow[] = [];
for (const webhookKey of Object.keys(this.testWebhookData)) { for (const webhookKey of Object.keys(this.testWebhookData)) {
workflowData = this.testWebhookData[webhookKey].workflowData; workflow = this.testWebhookData[webhookKey].workflow;
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); workflows.push(workflow);
} }

View File

@@ -3,16 +3,17 @@ import { get } from 'lodash';
import { import {
ActiveExecutions, ActiveExecutions,
ExternalHooks,
GenericHelpers, GenericHelpers,
IExecutionDb, IExecutionDb,
IResponseCallbackData, IResponseCallbackData,
IWorkflowDb, IWorkflowDb,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
ResponseHelper, ResponseHelper,
WorkflowHelpers,
WorkflowRunner,
WorkflowCredentials, WorkflowCredentials,
WorkflowExecuteAdditionalData, WorkflowExecuteAdditionalData,
WorkflowHelpers,
WorkflowRunner,
} from './'; } from './';
import { import {
@@ -114,8 +115,8 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
} }
// Get the responseMode // Get the responseMode
const responseMode = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], 'onReceived'); const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], 'onReceived');
const responseCode = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], 200) as number; const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], 200) as number;
if (!['onReceived', 'lastNode'].includes(responseMode as string)) { if (!['onReceived', 'lastNode'].includes(responseMode as string)) {
// If the mode is not known we error. Is probably best like that instead of using // If the mode is not known we error. Is probably best like that instead of using
@@ -173,7 +174,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
await WorkflowHelpers.saveStaticData(workflow); await WorkflowHelpers.saveStaticData(workflow);
if (webhookData.webhookDescription['responseHeaders'] !== undefined) { if (webhookData.webhookDescription['responseHeaders'] !== undefined) {
const responseHeaders = workflow.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], undefined) as { const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], undefined) as {
entries?: Array<{ entries?: Array<{
name: string; name: string;
value: string; value: string;
@@ -251,7 +252,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
data: { data: {
main: webhookResultData.workflowData, main: webhookResultData.workflowData,
}, },
}, }
); );
const runExecutionData: IRunExecutionData = { const runExecutionData: IRunExecutionData = {
@@ -325,7 +326,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
return data; return data;
} }
const responseData = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson'); const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson');
if (didSendResponse === false) { if (didSendResponse === false) {
let data: IDataObject | IDataObject[]; let data: IDataObject | IDataObject[];
@@ -340,13 +341,13 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
data = returnData.data!.main[0]![0].json; data = returnData.data!.main[0]![0].json;
const responsePropertyName = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], undefined); const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], undefined);
if (responsePropertyName !== undefined) { if (responsePropertyName !== undefined) {
data = get(data, responsePropertyName as string) as IDataObject; data = get(data, responsePropertyName as string) as IDataObject;
} }
const responseContentType = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], undefined); const responseContentType = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], undefined);
if (responseContentType !== undefined) { if (responseContentType !== undefined) {
// Send the webhook response manually to be able to set the content-type // Send the webhook response manually to be able to set the content-type
@@ -379,7 +380,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
didSendResponse = true; didSendResponse = true;
} }
const responseBinaryPropertyName = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], 'data'); const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], 'data');
if (responseBinaryPropertyName === undefined && didSendResponse === false) { if (responseBinaryPropertyName === undefined && didSendResponse === false) {
responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {}); responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {});

View File

@@ -25,8 +25,8 @@ import {
IExecuteData, IExecuteData,
IExecuteWorkflowInfo, IExecuteWorkflowInfo,
INode, INode,
INodeParameters,
INodeExecutionData, INodeExecutionData,
INodeParameters,
IRun, IRun,
IRunExecutionData, IRunExecutionData,
ITaskData, ITaskData,
@@ -74,7 +74,7 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo
workflow: { workflow: {
id: workflowData.id !== undefined ? workflowData.id.toString() as string : undefined, id: workflowData.id !== undefined ? workflowData.id.toString() as string : undefined,
name: workflowData.name, name: workflowData.name,
} },
}; };
// Run the error workflow // Run the error workflow
WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData); WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData);
@@ -191,13 +191,13 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
workflowId: this.workflowData.id as string, workflowId: this.workflowData.id as string,
workflowName: this.workflowData.name, workflowName: this.workflowData.name,
}); });
} },
], ],
workflowExecuteAfter: [ workflowExecuteAfter: [
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> { async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> {
pushExecutionFinished(this.mode, fullRunData, this.executionId, undefined, this.retryOf); pushExecutionFinished(this.mode, fullRunData, this.executionId, undefined, this.retryOf);
}, },
] ],
}; };
} }
@@ -298,7 +298,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
} }
} }
}, },
] ],
}; };
} }
@@ -374,8 +374,8 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
// Always start with empty data if no inputData got supplied // Always start with empty data if no inputData got supplied
inputData = inputData || [ inputData = inputData || [
{ {
json: {} json: {},
} },
]; ];
// Initialize the incoming data // Initialize the incoming data
@@ -386,7 +386,7 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
data: { data: {
main: [inputData], main: [inputData],
}, },
}, }
); );
const runExecutionData: IRunExecutionData = { const runExecutionData: IRunExecutionData = {
@@ -406,6 +406,8 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
const workflowExecute = new WorkflowExecute(additionalDataIntegrated, mode, runExecutionData); const workflowExecute = new WorkflowExecute(additionalDataIntegrated, mode, runExecutionData);
const data = await workflowExecute.processRunExecutionData(workflow); const data = await workflowExecute.processRunExecutionData(workflow);
await externalHooks.run('workflow.postExecute', [data, workflowData]);
if (data.finished === true) { if (data.finished === true) {
// Workflow did finish successfully // Workflow did finish successfully
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);

View File

@@ -3,8 +3,8 @@ import {
Db, Db,
ICredentialsTypeData, ICredentialsTypeData,
ITransferNodeTypes, ITransferNodeTypes,
IWorkflowExecutionDataProcess,
IWorkflowErrorData, IWorkflowErrorData,
IWorkflowExecutionDataProcess,
NodeTypes, NodeTypes,
WorkflowCredentials, WorkflowCredentials,
WorkflowRunner, WorkflowRunner,
@@ -120,12 +120,12 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
main: [ main: [
[ [
{ {
json: workflowErrorData json: workflowErrorData,
} },
] ],
], ],
}, },
}, }
); );
const runExecutionData: IRunExecutionData = { const runExecutionData: IRunExecutionData = {

View File

@@ -24,8 +24,8 @@ import {
IExecutionError, IExecutionError,
IRun, IRun,
Workflow, Workflow,
WorkflowHooks,
WorkflowExecuteMode, WorkflowExecuteMode,
WorkflowHooks,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as config from '../config'; import * as config from '../config';
@@ -104,11 +104,25 @@ export class WorkflowRunner {
await externalHooks.run('workflow.execute', [data.workflowData, data.executionMode]); await externalHooks.run('workflow.execute', [data.workflowData, data.executionMode]);
const executionsProcess = config.get('executions.process') as string; const executionsProcess = config.get('executions.process') as string;
let executionId: string;
if (executionsProcess === 'main') { if (executionsProcess === 'main') {
return this.runMainProcess(data, loadStaticData); executionId = await this.runMainProcess(data, loadStaticData);
} else {
executionId = await this.runSubprocess(data, loadStaticData);
} }
return this.runSubprocess(data, loadStaticData); if (externalHooks.exists('workflow.postExecute')) {
this.activeExecutions.getPostExecutePromise(executionId)
.then(async (executionData) => {
await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]);
})
.catch(error => {
console.error('There was a problem running hook "workflow.postExecute"', error);
});
}
return executionId;
} }
@@ -212,6 +226,7 @@ export class WorkflowRunner {
let nodeTypeData: ITransferNodeTypes; let nodeTypeData: ITransferNodeTypes;
let credentialTypeData: ICredentialsTypeData; let credentialTypeData: ICredentialsTypeData;
let credentialsOverwrites = this.credentialsOverwrites;
if (loadAllNodeTypes === true) { if (loadAllNodeTypes === true) {
// Supply all nodeTypes and credentialTypes // Supply all nodeTypes and credentialTypes
@@ -219,15 +234,22 @@ export class WorkflowRunner {
const credentialTypes = CredentialTypes(); const credentialTypes = CredentialTypes();
credentialTypeData = credentialTypes.credentialTypes; credentialTypeData = credentialTypes.credentialTypes;
} else { } else {
// Supply only nodeTypes and credentialTypes which the workflow needs // Supply only nodeTypes, credentialTypes and overwrites that the workflow needs
nodeTypeData = WorkflowHelpers.getNodeTypeData(data.workflowData.nodes); nodeTypeData = WorkflowHelpers.getNodeTypeData(data.workflowData.nodes);
credentialTypeData = WorkflowHelpers.getCredentialsData(data.credentials); credentialTypeData = WorkflowHelpers.getCredentialsData(data.credentials);
credentialsOverwrites = {};
for (const credentialName of Object.keys(credentialTypeData)) {
if (this.credentialsOverwrites[credentialName] !== undefined) {
credentialsOverwrites[credentialName] = this.credentialsOverwrites[credentialName];
}
}
} }
(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId; (data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData; (data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = this.credentialsOverwrites; (data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = credentialsOverwrites;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData = credentialTypeData; // TODO: Still needs correct value (data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData = credentialTypeData; // TODO: Still needs correct value
const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId); const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);

View File

@@ -66,7 +66,7 @@ export class WorkflowRunnerProcess {
// Load the credentials overwrites if any exist // Load the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites(); const credentialsOverwrites = CredentialsOverwrites();
await credentialsOverwrites.init(); await credentialsOverwrites.init(inputData.credentialsOverwrite);
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}); this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings});
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials); const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials);
@@ -135,13 +135,13 @@ export class WorkflowRunnerProcess {
workflowExecuteBefore: [ workflowExecuteBefore: [
async (): Promise<void> => { async (): Promise<void> => {
this.sendHookToParentProcess('workflowExecuteBefore', []); this.sendHookToParentProcess('workflowExecuteBefore', []);
} },
], ],
workflowExecuteAfter: [ workflowExecuteAfter: [
async (fullRunData: IRun, newStaticData?: IDataObject): Promise<void> => { async (fullRunData: IRun, newStaticData?: IDataObject): Promise<void> => {
this.sendHookToParentProcess('workflowExecuteAfter', [fullRunData, newStaticData]); this.sendHookToParentProcess('workflowExecuteAfter', [fullRunData, newStaticData]);
}, },
] ],
}; };
return new WorkflowHooks(hookFunctions, this.data!.executionMode, this.data!.executionId, this.data!.workflowData, { sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string }); return new WorkflowHooks(hookFunctions, this.data!.executionMode, this.data!.executionId, this.data!.workflowData, { sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string });

View File

@@ -20,7 +20,7 @@ export class CredentialsEntity implements ICredentialsDb {
id: number; id: number;
@Column({ @Column({
length: 128 length: 128,
}) })
name: string; name: string;
@@ -29,7 +29,7 @@ export class CredentialsEntity implements ICredentialsDb {
@Index() @Index()
@Column({ @Column({
length: 32 length: 32,
}) })
type: string; type: string;

View File

@@ -22,7 +22,7 @@ export class WorkflowEntity implements IWorkflowDb {
id: number; id: number;
@Column({ @Column({
length: 128 length: 128,
}) })
name: string; name: string;

View File

@@ -20,7 +20,7 @@ export class CredentialsEntity implements ICredentialsDb {
id: number; id: number;
@Column({ @Column({
length: 128 length: 128,
}) })
name: string; name: string;
@@ -29,7 +29,7 @@ export class CredentialsEntity implements ICredentialsDb {
@Index() @Index()
@Column({ @Column({
length: 32 length: 32,
}) })
type: string; type: string;

View File

@@ -22,7 +22,7 @@ export class WorkflowEntity implements IWorkflowDb {
id: number; id: number;
@Column({ @Column({
length: 128 length: 128,
}) })
name: string; name: string;

View File

@@ -20,7 +20,7 @@ export class CredentialsEntity implements ICredentialsDb {
id: number; id: number;
@Column({ @Column({
length: 128 length: 128,
}) })
name: string; name: string;
@@ -29,7 +29,7 @@ export class CredentialsEntity implements ICredentialsDb {
@Index() @Index()
@Column({ @Column({
length: 32 length: 32,
}) })
type: string; type: string;

View File

@@ -22,7 +22,7 @@ export class WorkflowEntity implements IWorkflowDb {
id: number; id: number;
@Column({ @Column({
length: 128 length: 128,
}) })
name: string; name: string;

View File

@@ -1,9 +1,9 @@
<html> <html>
<script> <script>
(function messageParent() { (function messageParent() {
window.opener.postMessage('success', '*'); window.opener.postMessage('success', '*');
}()); }());
</script> </script>
Got connected. The window can be closed now. Got connected. The window can be closed now.
</html> </html>

View File

@@ -46,6 +46,11 @@
"forin": true, "forin": true,
"jsdoc-format": true, "jsdoc-format": true,
"label-position": true, "label-position": true,
"indent": [
true,
"tabs",
2
],
"member-access": [ "member-access": [
true, true,
"no-public" "no-public"
@@ -60,6 +65,13 @@
"no-default-export": true, "no-default-export": true,
"no-duplicate-variable": true, "no-duplicate-variable": true,
"no-inferrable-types": true, "no-inferrable-types": true,
"ordered-imports": [
true,
{
"import-sources-order": "any",
"named-imports-order": "case-insensitive"
}
],
"no-namespace": [ "no-namespace": [
true, true,
"allow-declarations" "allow-declarations"
@@ -82,6 +94,18 @@
"ignore-bound-class-methods" "ignore-bound-class-methods"
], ],
"switch-default": true, "switch-default": true,
"trailing-comma": [
true,
{
"multiline": {
"objects": "always",
"arrays": "always",
"functions": "never",
"typeLiterals": "ignore"
},
"esSpecCompliant": true
}
],
"triple-equals": [ "triple-equals": [
true, true,
"allow-null-check" "allow-null-check"

View File

@@ -19,7 +19,7 @@ Condition notice.
Software: n8n Software: n8n
License: Apache 2.0 License: Apache 2.0 with Commons Clause
Licensor: n8n GmbH Licensor: n8n GmbH

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-core", "name": "n8n-core",
"version": "0.44.0", "version": "0.48.1",
"description": "Core functionality of n8n", "description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@@ -18,6 +18,7 @@
"build": "tsc", "build": "tsc",
"dev": "npm run watch", "dev": "npm run watch",
"tslint": "tslint -p tsconfig.json -c tslint.json", "tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"watch": "tsc --watch", "watch": "tsc --watch",
"test": "jest" "test": "jest"
}, },
@@ -26,27 +27,28 @@
], ],
"devDependencies": { "devDependencies": {
"@types/cron": "^1.7.1", "@types/cron": "^1.7.1",
"@types/crypto-js": "^3.1.43", "@types/crypto-js": "^4.0.1",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/jest": "^25.2.1", "@types/jest": "^26.0.13",
"@types/lodash.get": "^4.4.6", "@types/lodash.get": "^4.4.6",
"@types/mime-types": "^2.1.0", "@types/mime-types": "^2.1.0",
"@types/node": "^14.0.27", "@types/node": "14.0.27",
"@types/request-promise-native": "~1.0.15", "@types/request-promise-native": "~1.0.15",
"jest": "^24.9.0", "jest": "^26.4.2",
"source-map-support": "^0.5.9", "source-map-support": "^0.5.9",
"ts-jest": "^25.4.0", "ts-jest": "^26.3.0",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~3.7.4" "typescript": "~3.9.7"
}, },
"dependencies": { "dependencies": {
"client-oauth2": "^4.2.5", "client-oauth2": "^4.2.5",
"cron": "^1.7.2", "cron": "^1.7.2",
"crypto-js": "3.1.9-1", "crypto-js": "4.0.0",
"file-type": "^14.6.2", "file-type": "^14.6.2",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"n8n-workflow": "~0.39.0", "n8n-workflow": "~0.42.0",
"oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"request": "^2.88.2", "request": "^2.88.2",
"request-promise-native": "^1.0.7" "request-promise-native": "^1.0.7"

View File

@@ -52,7 +52,7 @@ export class ActiveWebhooks {
try { try {
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, this.testWebhooks); const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
if (webhookExists === false) { if (webhookExists !== true) {
// If webhook does not exist yet create it // If webhook does not exist yet create it
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, this.testWebhooks); await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
@@ -60,7 +60,6 @@ export class ActiveWebhooks {
} catch (error) { } catch (error) {
// If there was a problem unregister the webhook again // If there was a problem unregister the webhook again
delete this.webhookUrls[webhookKey]; delete this.webhookUrls[webhookKey];
delete this.workflowWebhooks[webhookData.workflowId];
throw error; throw error;
} }
@@ -159,7 +158,7 @@ export class ActiveWebhooks {
/** /**
* Removes all the webhooks of the given workflow * Removes all the webhooks of the given workflows
*/ */
async removeAll(workflows: Workflow[]): Promise<void> { async removeAll(workflows: Workflow[]): Promise<void> {
const removePromises = []; const removePromises = [];

View File

@@ -67,8 +67,6 @@ export class ActiveWorkflows {
* @memberof ActiveWorkflows * @memberof ActiveWorkflows
*/ */
async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise<void> { async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise<void> {
console.log('ADD ID (active): ' + id);
this.workflowData[id] = {}; this.workflowData[id] = {};
const triggerNodes = workflow.getTriggerNodes(); const triggerNodes = workflow.getTriggerNodes();
@@ -204,8 +202,6 @@ export class ActiveWorkflows {
* @memberof ActiveWorkflows * @memberof ActiveWorkflows
*/ */
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
console.log('REMOVE ID (active): ' + id);
if (!this.isActive(id)) { if (!this.isActive(id)) {
// Workflow is currently not registered // Workflow is currently not registered
throw new Error(`The workflow with the id "${id}" is currently not active and can so not be removed`); throw new Error(`The workflow with the id "${id}" is currently not active and can so not be removed`);

View File

@@ -5,7 +5,7 @@ import {
ICredentialsEncrypted, ICredentialsEncrypted,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { enc, AES } from 'crypto-js'; import { AES, enc } from 'crypto-js';
export class Credentials extends ICredentials { export class Credentials extends ICredentials {

View File

@@ -9,13 +9,13 @@ import {
ILoadOptionsFunctions as ILoadOptionsFunctionsBase, ILoadOptionsFunctions as ILoadOptionsFunctionsBase,
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
IOAuth2Options,
IPollFunctions as IPollFunctionsBase, IPollFunctions as IPollFunctionsBase,
IPollResponse, IPollResponse,
ITriggerFunctions as ITriggerFunctionsBase, ITriggerFunctions as ITriggerFunctionsBase,
ITriggerResponse, ITriggerResponse,
IWebhookFunctions as IWebhookFunctionsBase, IWebhookFunctions as IWebhookFunctionsBase,
IWorkflowSettings as IWorkflowSettingsWorkflow, IWorkflowSettings as IWorkflowSettingsWorkflow,
IOAuth2Options,
} from 'n8n-workflow'; } from 'n8n-workflow';

View File

@@ -36,7 +36,7 @@ export class LoadNodeParameterOptions {
position: [ position: [
0, 0,
0, 0,
] ],
}; };
if (credentials) { if (credentials) {

View File

@@ -1,9 +1,9 @@
import { import {
BINARY_ENCODING,
IHookFunctions, IHookFunctions,
ILoadOptionsFunctions, ILoadOptionsFunctions,
IResponseError, IResponseError,
IWorkflowSettings, IWorkflowSettings,
BINARY_ENCODING,
} from './'; } from './';
import { import {
@@ -19,6 +19,7 @@ import {
INodeExecutionData, INodeExecutionData,
INodeParameters, INodeParameters,
INodeType, INodeType,
IOAuth2Options,
IPollFunctions, IPollFunctions,
IRunExecutionData, IRunExecutionData,
ITaskDataConnections, ITaskDataConnections,
@@ -34,7 +35,6 @@ import {
Workflow, Workflow,
WorkflowDataProxy, WorkflowDataProxy,
WorkflowExecuteMode, WorkflowExecuteMode,
IOAuth2Options,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as clientOAuth1 from 'oauth-1.0a'; import * as clientOAuth1 from 'oauth-1.0a';
@@ -43,7 +43,7 @@ import * as clientOAuth2 from 'client-oauth2';
import { get } from 'lodash'; import { get } from 'lodash';
import * as express from 'express'; import * as express from 'express';
import * as path from 'path'; import * as path from 'path';
import { OptionsWithUrl, OptionsWithUri } from 'request'; import { OptionsWithUri, OptionsWithUrl } from 'request';
import * as requestPromise from 'request-promise-native'; import * as requestPromise from 'request-promise-native';
import { createHmac } from 'crypto'; import { createHmac } from 'crypto';
import { fromBuffer } from 'file-type'; import { fromBuffer } from 'file-type';
@@ -91,7 +91,7 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m
// TODO: Should program it in a way that it does not have to converted to base64 // TODO: Should program it in a way that it does not have to converted to base64
// It should only convert to and from base64 when saved in database because // It should only convert to and from base64 when saved in database because
// of for example an error or when there is a wait node. // of for example an error or when there is a wait node.
data: binaryData.toString(BINARY_ENCODING) data: binaryData.toString(BINARY_ENCODING),
}; };
if (filePath) { if (filePath) {
@@ -152,11 +152,16 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin
// on the token-type used. // on the token-type used.
const newRequestOptions = token.sign(requestOptions as clientOAuth2.RequestObject); const newRequestOptions = token.sign(requestOptions as clientOAuth2.RequestObject);
// If keep bearer is false remove the it from the authorization header
if (oAuth2Options?.keepBearer === false) {
//@ts-ignore
newRequestOptions?.headers?.Authorization = newRequestOptions?.headers?.Authorization.split(' ')[1];
}
return this.helpers.request!(newRequestOptions) return this.helpers.request!(newRequestOptions)
.catch(async (error: IResponseError) => { .catch(async (error: IResponseError) => {
// TODO: Check if also other codes are possible // TODO: Check if also other codes are possible
if (error.statusCode === 401) { if (error.statusCode === 401) {
// TODO: Whole refresh process is not tested yet
// Token is probably not valid anymore. So try refresh it. // Token is probably not valid anymore. So try refresh it.
const tokenRefreshOptions: IDataObject = {}; const tokenRefreshOptions: IDataObject = {};
@@ -388,7 +393,7 @@ export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecu
let returnData; let returnData;
try { try {
returnData = workflow.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); returnData = workflow.expression.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData);
} catch (e) { } catch (e) {
e.message += ` [Error in parameter: "${parameterName}"]`; e.message += ` [Error in parameter: "${parameterName}"]`;
throw e; throw e;
@@ -434,12 +439,12 @@ export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode,
return undefined; return undefined;
} }
const path = workflow.getSimpleParameterValue(node, webhookDescription['path']); const path = workflow.expression.getSimpleParameterValue(node, webhookDescription['path']);
if (path === undefined) { if (path === undefined) {
return undefined; return undefined;
} }
const isFullPath: boolean = workflow.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean;
return NodeHelpers.getNodeWebhookUrl(baseUrl, workflow.id!, node, path.toString(), isFullPath); return NodeHelpers.getNodeWebhookUrl(baseUrl, workflow.id!, node, path.toString(), isFullPath);
} }
@@ -654,7 +659,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
return continueOnFail(node); return continueOnFail(node);
}, },
evaluateExpression: (expression: string, itemIndex: number) => { evaluateExpression: (expression: string, itemIndex: number) => {
return workflow.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData);
}, },
async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any> { // tslint:disable-line:no-any async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any> { // tslint:disable-line:no-any
return additionalData.executeWorkflow(workflowInfo, additionalData, inputData); return additionalData.executeWorkflow(workflowInfo, additionalData, inputData);
@@ -752,7 +757,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
}, },
evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => { evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => {
evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex; evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex;
return workflow.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData); return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData);
}, },
getContext(type: string): IContextObject { getContext(type: string): IContextObject {
return NodeHelpers.getContext(runExecutionData, type, node); return NodeHelpers.getContext(runExecutionData, type, node);

View File

@@ -1,10 +1,10 @@
import { import {
ENCRYPTION_KEY_ENV_OVERWRITE, ENCRYPTION_KEY_ENV_OVERWRITE,
EXTENSIONS_SUBDIRECTORY, EXTENSIONS_SUBDIRECTORY,
IUserSettings,
USER_FOLDER_ENV_OVERWRITE, USER_FOLDER_ENV_OVERWRITE,
USER_SETTINGS_FILE_NAME, USER_SETTINGS_FILE_NAME,
USER_SETTINGS_SUBFOLDER, USER_SETTINGS_SUBFOLDER,
IUserSettings,
} from '.'; } from '.';

View File

@@ -15,8 +15,8 @@ import {
ITaskDataConnections, ITaskDataConnections,
IWaitingForExecution, IWaitingForExecution,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
WorkflowExecuteMode,
Workflow, Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
NodeExecuteFunctions, NodeExecuteFunctions,
@@ -84,7 +84,7 @@ export class WorkflowExecute {
], ],
], ],
}, },
} },
]; ];
this.runExecutionData = { this.runExecutionData = {
@@ -137,8 +137,8 @@ export class WorkflowExecute {
// If it has no incoming data add the default empty data // If it has no incoming data add the default empty data
incomingData.push([ incomingData.push([
{ {
json: {} json: {},
} },
]); ]);
} else { } else {
// Get the data of the incoming connections // Get the data of the incoming connections
@@ -156,7 +156,7 @@ export class WorkflowExecute {
node: workflow.getNode(startNode) as INode, node: workflow.getNode(startNode) as INode,
data: { data: {
main: incomingData, main: incomingData,
} },
}; };
nodeExecutionStack.push(executeData); nodeExecutionStack.push(executeData);
@@ -252,7 +252,7 @@ export class WorkflowExecute {
if (this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] === undefined) { if (this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] === undefined) {
// Node does not have data for runIndex yet so create also empty one and init it // Node does not have data for runIndex yet so create also empty one and init it
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = { this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
main: [] main: [],
}; };
for (let i = 0; i < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; i++) { for (let i = 0; i < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; i++) {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.push(null); this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.push(null);
@@ -282,7 +282,7 @@ export class WorkflowExecute {
// So add it to the execution stack // So add it to the execution stack
this.runExecutionData.executionData!.nodeExecutionStack.push({ this.runExecutionData.executionData!.nodeExecutionStack.push({
node: workflow.nodes[connectionData.node], node: workflow.nodes[connectionData.node],
data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex],
}); });
// Remove the data from waiting // Remove the data from waiting
@@ -426,15 +426,15 @@ export class WorkflowExecute {
this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {}; this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
} }
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = { this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
main: connectionDataArray main: connectionDataArray,
}; };
} else { } else {
// All data is there so add it directly to stack // All data is there so add it directly to stack
this.runExecutionData.executionData!.nodeExecutionStack.push({ this.runExecutionData.executionData!.nodeExecutionStack.push({
node: workflow.nodes[connectionData.node], node: workflow.nodes[connectionData.node],
data: { data: {
main: connectionDataArray main: connectionDataArray,
} },
}); });
} }
} }
@@ -608,7 +608,7 @@ export class WorkflowExecute {
nodeSuccessData[0] = [ nodeSuccessData[0] = [
{ {
json: {}, json: {},
} },
]; ];
} }
} }
@@ -622,6 +622,8 @@ export class WorkflowExecute {
break; break;
} catch (error) { } catch (error) {
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
executionError = { executionError = {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
@@ -637,7 +639,7 @@ export class WorkflowExecute {
} }
taskData = { taskData = {
startTime, startTime,
executionTime: (new Date().getTime()) - startTime executionTime: (new Date().getTime()) - startTime,
}; };
if (executionError !== undefined) { if (executionError !== undefined) {
@@ -667,7 +669,7 @@ export class WorkflowExecute {
// Node executed successfully. So add data and go on. // Node executed successfully. So add data and go on.
taskData.data = ({ taskData.data = ({
'main': nodeSuccessData 'main': nodeSuccessData,
} as ITaskDataConnections); } as ITaskDataConnections);
this.executeHook('nodeExecuteAfter', [executionNode.name, taskData]); this.executeHook('nodeExecuteAfter', [executionNode.name, taskData]);
@@ -698,7 +700,10 @@ export class WorkflowExecute {
return Promise.reject(new Error(`The node "${executionNode.name}" connects to not found node "${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); if (nodeSuccessData![outputIndex] && nodeSuccessData![outputIndex].length !== 0) {
// Add the node only if there is data for it to process
this.addNodeToBeExecuted(workflow, connectionData, parseInt(outputIndex, 10), executionNode.name, nodeSuccessData!, runIndex);
}
} }
} }
} }

View File

@@ -3,86 +3,86 @@ import { Credentials } from '../src';
describe('Credentials', () => { describe('Credentials', () => {
describe('without nodeType set', () => { describe('without nodeType set', () => {
test('should be able to set and read key data without initial data set', () => { test('should be able to set and read key data without initial data set', () => {
const credentials = new Credentials('testName', 'testType', []); const credentials = new Credentials('testName', 'testType', []);
const key = 'key1'; const key = 'key1';
const password = 'password'; const password = 'password';
// const nodeType = 'base.noOp'; // const nodeType = 'base.noOp';
const newData = 1234; const newData = 1234;
credentials.setDataKey(key, newData, password); credentials.setDataKey(key, newData, password);
expect(credentials.getDataKey(key, password)).toEqual(newData); expect(credentials.getDataKey(key, password)).toEqual(newData);
}); });
test('should be able to set and read key data with initial data set', () => { test('should be able to set and read key data with initial data set', () => {
const key = 'key2'; const key = 'key2';
const password = 'password'; const password = 'password';
// Saved under "key1" // Saved under "key1"
const initialData = 4321; const initialData = 4321;
const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ='; const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ=';
const credentials = new Credentials('testName', 'testType', [], initialDataEncoded); const credentials = new Credentials('testName', 'testType', [], initialDataEncoded);
const newData = 1234; const newData = 1234;
// Set and read new data // Set and read new data
credentials.setDataKey(key, newData, password); credentials.setDataKey(key, newData, password);
expect(credentials.getDataKey(key, password)).toEqual(newData); expect(credentials.getDataKey(key, password)).toEqual(newData);
// Read the data which got provided encrypted on init // Read the data which got provided encrypted on init
expect(credentials.getDataKey('key1', password)).toEqual(initialData); expect(credentials.getDataKey('key1', password)).toEqual(initialData);
}); });
}); });
describe('with nodeType set', () => { describe('with nodeType set', () => {
test('should be able to set and read key data without initial data set', () => { test('should be able to set and read key data without initial data set', () => {
const nodeAccess = [ const nodeAccess = [
{ {
nodeType: 'base.noOp', nodeType: 'base.noOp',
user: 'userName', user: 'userName',
date: new Date(), date: new Date(),
} },
]; ];
const credentials = new Credentials('testName', 'testType', nodeAccess); const credentials = new Credentials('testName', 'testType', nodeAccess);
const key = 'key1'; const key = 'key1';
const password = 'password'; const password = 'password';
const nodeType = 'base.noOp'; const nodeType = 'base.noOp';
const newData = 1234; const newData = 1234;
credentials.setDataKey(key, newData, password); credentials.setDataKey(key, newData, password);
// Should be able to read with nodeType which has access // Should be able to read with nodeType which has access
expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData); expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData);
// Should not be able to read with nodeType which does NOT have access // Should not be able to read with nodeType which does NOT have access
// expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error); // expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error);
try { try {
credentials.getDataKey(key, password, 'base.otherNode'); credentials.getDataKey(key, password, 'base.otherNode');
expect(true).toBe(false); expect(true).toBe(false);
} catch (e) { } catch (e) {
expect(e.message).toBe('The node of type "base.otherNode" does not have access to credentials "testName" of type "testType".'); expect(e.message).toBe('The node of type "base.otherNode" does not have access to credentials "testName" of type "testType".');
} }
// Get the data which will be saved in database // Get the data which will be saved in database
const dbData = credentials.getDataToSave(); const dbData = credentials.getDataToSave();
expect(dbData.name).toEqual('testName'); expect(dbData.name).toEqual('testName');
expect(dbData.type).toEqual('testType'); expect(dbData.type).toEqual('testType');
expect(dbData.nodesAccess).toEqual(nodeAccess); expect(dbData.nodesAccess).toEqual(nodeAccess);
// Compare only the first 6 characters as the rest seems to change with each execution // Compare only the first 6 characters as the rest seems to change with each execution
expect(dbData.data!.slice(0, 6)).toEqual('U2FsdGVkX1+wpQWkj+YTzaPSNTFATjnlmFKIsUTZdhk='.slice(0, 6)); expect(dbData.data!.slice(0, 6)).toEqual('U2FsdGVkX1+wpQWkj+YTzaPSNTFATjnlmFKIsUTZdhk='.slice(0, 6));
}); });
}); });
}); });

View File

@@ -7,8 +7,8 @@ import {
INodeExecutionData, INodeExecutionData,
INodeParameters, INodeParameters,
INodeType, INodeType,
INodeTypes,
INodeTypeData, INodeTypeData,
INodeTypes,
IRun, IRun,
ITaskData, ITaskData,
IWorkflowBase, IWorkflowBase,
@@ -87,7 +87,7 @@ class NodeTypesClass implements INodeTypes {
displayOptions: { displayOptions: {
show: { show: {
mode: [ mode: [
'passThrough' 'passThrough',
], ],
}, },
}, },
@@ -104,7 +104,7 @@ class NodeTypesClass implements INodeTypes {
default: 'input1', default: 'input1',
description: 'Defines of which input the data should be used as output of node.', description: 'Defines of which input the data should be used as output of node.',
}, },
] ],
}, },
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
// const itemsInput2 = this.getInputData(1); // const itemsInput2 = this.getInputData(1);
@@ -131,7 +131,7 @@ class NodeTypesClass implements INodeTypes {
} }
return [returnData]; return [returnData];
} },
}, },
}, },
'n8n-nodes-base.set': { 'n8n-nodes-base.set': {
@@ -186,11 +186,11 @@ class NodeTypesClass implements INodeTypes {
default: 0, default: 0,
description: 'The number value to write in the property.', description: 'The number value to write in the property.',
}, },
] ],
}, },
], ],
}, },
] ],
}, },
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();
@@ -213,7 +213,7 @@ class NodeTypesClass implements INodeTypes {
} }
return this.prepareOutputData(returnData); return this.prepareOutputData(returnData);
} },
}, },
}, },
'n8n-nodes-base.start': { 'n8n-nodes-base.start': {
@@ -231,7 +231,7 @@ class NodeTypesClass implements INodeTypes {
}, },
inputs: [], inputs: [],
outputs: ['main'], outputs: ['main'],
properties: [] properties: [],
}, },
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();

View File

@@ -47,8 +47,8 @@ describe('WorkflowExecute', () => {
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
100, 100,
300 300,
] ],
}, },
{ {
"parameters": { "parameters": {
@@ -56,19 +56,19 @@ describe('WorkflowExecute', () => {
"number": [ "number": [
{ {
"name": "value1", "name": "value1",
"value": 1 "value": 1,
} },
] ],
} },
}, },
"name": "Set", "name": "Set",
"type": "n8n-nodes-base.set", "type": "n8n-nodes-base.set",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
280, 280,
300 300,
] ],
} },
], ],
"connections": { "connections": {
"Start": { "Start": {
@@ -77,12 +77,12 @@ describe('WorkflowExecute', () => {
{ {
"node": "Set", "node": "Set",
"type": "main", "type": "main",
"index": 0 "index": 0,
} },
] ],
] ],
} },
} },
}, },
}, },
output: { output: {
@@ -115,8 +115,8 @@ describe('WorkflowExecute', () => {
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
100, 100,
300 300,
] ],
}, },
{ {
"parameters": { "parameters": {
@@ -124,18 +124,18 @@ describe('WorkflowExecute', () => {
"number": [ "number": [
{ {
"name": "value1", "name": "value1",
"value": 1 "value": 1,
} },
] ],
} },
}, },
"name": "Set1", "name": "Set1",
"type": "n8n-nodes-base.set", "type": "n8n-nodes-base.set",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
300, 300,
250 250,
] ],
}, },
{ {
"parameters": { "parameters": {
@@ -143,19 +143,19 @@ describe('WorkflowExecute', () => {
"number": [ "number": [
{ {
"name": "value2", "name": "value2",
"value": 2 "value": 2,
} },
] ],
} },
}, },
"name": "Set2", "name": "Set2",
"type": "n8n-nodes-base.set", "type": "n8n-nodes-base.set",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
500, 500,
400 400,
] ],
} },
], ],
"connections": { "connections": {
"Start": { "Start": {
@@ -164,15 +164,15 @@ describe('WorkflowExecute', () => {
{ {
"node": "Set1", "node": "Set1",
"type": "main", "type": "main",
"index": 0 "index": 0,
}, },
{ {
"node": "Set2", "node": "Set2",
"type": "main", "type": "main",
"index": 0 "index": 0,
} },
] ],
] ],
}, },
"Set1": { "Set1": {
"main": [ "main": [
@@ -180,12 +180,12 @@ describe('WorkflowExecute', () => {
{ {
"node": "Set2", "node": "Set2",
"type": "main", "type": "main",
"index": 0 "index": 0,
} },
] ],
] ],
} },
} },
}, },
}, },
output: { output: {
@@ -201,7 +201,7 @@ describe('WorkflowExecute', () => {
{ {
value1: 1, value1: 1,
}, },
] ],
], ],
Set2: [ Set2: [
[ [
@@ -228,15 +228,15 @@ describe('WorkflowExecute', () => {
"nodes": [ "nodes": [
{ {
"parameters": { "parameters": {
"mode": "passThrough" "mode": "passThrough",
}, },
"name": "Merge4", "name": "Merge4",
"type": "n8n-nodes-base.merge", "type": "n8n-nodes-base.merge",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
1150, 1150,
500 500,
] ],
}, },
{ {
"parameters": { "parameters": {
@@ -244,18 +244,18 @@ describe('WorkflowExecute', () => {
"number": [ "number": [
{ {
"name": "value2", "name": "value2",
"value": 2 "value": 2,
} },
] ],
} },
}, },
"name": "Set2", "name": "Set2",
"type": "n8n-nodes-base.set", "type": "n8n-nodes-base.set",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
290, 290,
400 400,
] ],
}, },
{ {
"parameters": { "parameters": {
@@ -263,18 +263,18 @@ describe('WorkflowExecute', () => {
"number": [ "number": [
{ {
"name": "value4", "name": "value4",
"value": 4 "value": 4,
} },
] ],
} },
}, },
"name": "Set4", "name": "Set4",
"type": "n8n-nodes-base.set", "type": "n8n-nodes-base.set",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
850, 850,
200 200,
] ],
}, },
{ {
"parameters": { "parameters": {
@@ -282,30 +282,30 @@ describe('WorkflowExecute', () => {
"number": [ "number": [
{ {
"name": "value3", "name": "value3",
"value": 3 "value": 3,
} },
] ],
} },
}, },
"name": "Set3", "name": "Set3",
"type": "n8n-nodes-base.set", "type": "n8n-nodes-base.set",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
650, 650,
200 200,
] ],
}, },
{ {
"parameters": { "parameters": {
"mode": "passThrough" "mode": "passThrough",
}, },
"name": "Merge4", "name": "Merge4",
"type": "n8n-nodes-base.merge", "type": "n8n-nodes-base.merge",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
1150, 1150,
500 500,
] ],
}, },
{ {
"parameters": {}, "parameters": {},
@@ -314,21 +314,21 @@ describe('WorkflowExecute', () => {
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
1000, 1000,
400 400,
] ],
}, },
{ {
"parameters": { "parameters": {
"mode": "passThrough", "mode": "passThrough",
"output": "input2" "output": "input2",
}, },
"name": "Merge2", "name": "Merge2",
"type": "n8n-nodes-base.merge", "type": "n8n-nodes-base.merge",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
700, 700,
400 400,
] ],
}, },
{ {
"parameters": {}, "parameters": {},
@@ -337,8 +337,8 @@ describe('WorkflowExecute', () => {
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
500, 500,
300 300,
] ],
}, },
{ {
"parameters": { "parameters": {
@@ -346,18 +346,18 @@ describe('WorkflowExecute', () => {
"number": [ "number": [
{ {
"name": "value1", "name": "value1",
"value": 1 "value": 1,
} },
] ],
} },
}, },
"name": "Set1", "name": "Set1",
"type": "n8n-nodes-base.set", "type": "n8n-nodes-base.set",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
300, 300,
200 200,
] ],
}, },
{ {
"parameters": {}, "parameters": {},
@@ -366,9 +366,9 @@ describe('WorkflowExecute', () => {
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
100, 100,
300 300,
] ],
} },
], ],
"connections": { "connections": {
"Set2": { "Set2": {
@@ -377,15 +377,15 @@ describe('WorkflowExecute', () => {
{ {
"node": "Merge1", "node": "Merge1",
"type": "main", "type": "main",
"index": 1 "index": 1,
}, },
{ {
"node": "Merge2", "node": "Merge2",
"type": "main", "type": "main",
"index": 1 "index": 1,
} },
] ],
] ],
}, },
"Set4": { "Set4": {
"main": [ "main": [
@@ -393,10 +393,10 @@ describe('WorkflowExecute', () => {
{ {
"node": "Merge3", "node": "Merge3",
"type": "main", "type": "main",
"index": 0 "index": 0,
} },
] ],
] ],
}, },
"Set3": { "Set3": {
"main": [ "main": [
@@ -404,10 +404,10 @@ describe('WorkflowExecute', () => {
{ {
"node": "Set4", "node": "Set4",
"type": "main", "type": "main",
"index": 0 "index": 0,
} },
] ],
] ],
}, },
"Merge3": { "Merge3": {
"main": [ "main": [
@@ -415,10 +415,10 @@ describe('WorkflowExecute', () => {
{ {
"node": "Merge4", "node": "Merge4",
"type": "main", "type": "main",
"index": 0 "index": 0,
} },
] ],
] ],
}, },
"Merge2": { "Merge2": {
"main": [ "main": [
@@ -426,10 +426,10 @@ describe('WorkflowExecute', () => {
{ {
"node": "Merge3", "node": "Merge3",
"type": "main", "type": "main",
"index": 1 "index": 1,
} },
] ],
] ],
}, },
"Merge1": { "Merge1": {
"main": [ "main": [
@@ -437,10 +437,10 @@ describe('WorkflowExecute', () => {
{ {
"node": "Merge2", "node": "Merge2",
"type": "main", "type": "main",
"index": 0 "index": 0,
} },
] ],
] ],
}, },
"Set1": { "Set1": {
"main": [ "main": [
@@ -448,15 +448,15 @@ describe('WorkflowExecute', () => {
{ {
"node": "Merge1", "node": "Merge1",
"type": "main", "type": "main",
"index": 0 "index": 0,
}, },
{ {
"node": "Set3", "node": "Set3",
"type": "main", "type": "main",
"index": 0 "index": 0,
} },
] ],
] ],
}, },
"Start": { "Start": {
"main": [ "main": [
@@ -464,22 +464,22 @@ describe('WorkflowExecute', () => {
{ {
"node": "Set1", "node": "Set1",
"type": "main", "type": "main",
"index": 0 "index": 0,
}, },
{ {
"node": "Set2", "node": "Set2",
"type": "main", "type": "main",
"index": 0 "index": 0,
}, },
{ {
"node": "Merge4", "node": "Merge4",
"type": "main", "type": "main",
"index": 1 "index": 1,
} },
] ],
] ],
} },
} },
}, },
}, },
output: { output: {
@@ -534,14 +534,14 @@ describe('WorkflowExecute', () => {
{ {
value2: 2, value2: 2,
}, },
] ],
], ],
Merge2: [ Merge2: [
[ [
{ {
value2: 2, value2: 2,
}, },
] ],
], ],
Merge3: [ Merge3: [
[ [
@@ -553,7 +553,7 @@ describe('WorkflowExecute', () => {
{ {
value2: 2, value2: 2,
}, },
] ],
], ],
Merge4: [ Merge4: [
[ [
@@ -565,7 +565,7 @@ describe('WorkflowExecute', () => {
{ {
value2: 2, value2: 2,
}, },
] ],
], ],
}, },
}, },

View File

@@ -46,6 +46,11 @@
"forin": true, "forin": true,
"jsdoc-format": true, "jsdoc-format": true,
"label-position": true, "label-position": true,
"indent": [
true,
"tabs",
2
],
"member-access": [ "member-access": [
true, true,
"no-public" "no-public"
@@ -60,6 +65,13 @@
"no-default-export": true, "no-default-export": true,
"no-duplicate-variable": true, "no-duplicate-variable": true,
"no-inferrable-types": true, "no-inferrable-types": true,
"ordered-imports": [
true,
{
"import-sources-order": "any",
"named-imports-order": "case-insensitive"
}
],
"no-namespace": [ "no-namespace": [
true, true,
"allow-declarations" "allow-declarations"
@@ -82,6 +94,18 @@
"ignore-bound-class-methods" "ignore-bound-class-methods"
], ],
"switch-default": true, "switch-default": true,
"trailing-comma": [
true,
{
"multiline": {
"objects": "always",
"arrays": "always",
"functions": "always",
"typeLiterals": "ignore"
},
"esSpecCompliant": true
}
],
"triple-equals": [ "triple-equals": [
true, true,
"allow-null-check" "allow-null-check"

View File

@@ -19,7 +19,7 @@ Condition notice.
Software: n8n Software: n8n
License: Apache 2.0 License: Apache 2.0 with Commons Clause
Licensor: n8n GmbH Licensor: n8n GmbH

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-editor-ui", "name": "n8n-editor-ui",
"version": "0.55.0", "version": "0.60.0",
"description": "Workflow Editor UI for n8n", "description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@@ -20,12 +20,11 @@
"serve": "cross-env 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", "test": "npm run test:unit",
"tslint": "tslint -p tsconfig.json -c tslint.json", "tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"test:e2e": "vue-cli-service test:e2e", "test:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit" "test:unit": "vue-cli-service test:unit"
}, },
"dependencies": { "dependencies": {},
"uuid": "^8.1.0"
},
"devDependencies": { "devDependencies": {
"@beyonk/google-fonts-webpack-plugin": "^1.2.3", "@beyonk/google-fonts-webpack-plugin": "^1.2.3",
"@fortawesome/fontawesome-svg-core": "^1.2.19", "@fortawesome/fontawesome-svg-core": "^1.2.19",
@@ -34,16 +33,16 @@
"@types/dateformat": "^3.0.0", "@types/dateformat": "^3.0.0",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/file-saver": "^2.0.1", "@types/file-saver": "^2.0.1",
"@types/jest": "^25.2.1", "@types/jest": "^26.0.13",
"@types/lodash.get": "^4.4.6", "@types/lodash.get": "^4.4.6",
"@types/lodash.set": "^4.3.6", "@types/lodash.set": "^4.3.6",
"@types/node": "^14.0.27", "@types/node": "14.0.27",
"@types/quill": "^2.0.1", "@types/quill": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^2.13.0", "@typescript-eslint/eslint-plugin": "^2.13.0",
"@typescript-eslint/parser": "^2.13.0", "@typescript-eslint/parser": "^2.13.0",
"@vue/cli-plugin-babel": "^4.1.2", "@vue/cli-plugin-babel": "^4.1.2",
"@vue/cli-plugin-eslint": "^4.1.2", "@vue/cli-plugin-eslint": "^4.1.2",
"@vue/cli-plugin-typescript": "~4.1.2", "@vue/cli-plugin-typescript": "~4.5.6",
"@vue/cli-plugin-unit-jest": "^4.1.2", "@vue/cli-plugin-unit-jest": "^4.1.2",
"@vue/cli-service": "^3.11.0", "@vue/cli-service": "^3.11.0",
"@vue/eslint-config-standard": "^5.0.1", "@vue/eslint-config-standard": "^5.0.1",
@@ -66,16 +65,18 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"n8n-workflow": "~0.39.0", "n8n-workflow": "~0.42.0",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1", "prismjs": "^1.17.1",
"quill": "^2.0.0-dev.3", "quill": "^2.0.0-dev.3",
"quill-autoformat": "^0.1.1", "quill-autoformat": "^0.1.1",
"sass-loader": "^8.0.0", "sass-loader": "^8.0.0",
"string-template-parser": "^1.2.6", "string-template-parser": "^1.2.6",
"ts-jest": "^25.4.0", "ts-jest": "^26.3.0",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~3.7.4", "typescript": "~3.9.7",
"uuid": "^8.1.0",
"vue": "^2.6.9", "vue": "^2.6.9",
"vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0", "vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0",
"vue-json-tree": "^0.4.1", "vue-json-tree": "^0.4.1",
@@ -83,6 +84,7 @@
"vue-router": "^3.0.6", "vue-router": "^3.0.6",
"vue-template-compiler": "^2.5.17", "vue-template-compiler": "^2.5.17",
"vue-typed-mixins": "^0.2.0", "vue-typed-mixins": "^0.2.0",
"vue2-touch-events": "^2.3.2",
"vuex": "^3.1.1" "vuex": "^3.1.1"
} }
} }

View File

@@ -126,6 +126,7 @@ export interface IRestApi {
makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any
getSettings(): Promise<IN8nUISettings>; getSettings(): Promise<IN8nUISettings>;
getNodeTypes(): Promise<INodeTypeDescription[]>; getNodeTypes(): Promise<INodeTypeDescription[]>;
getNodesInformation(nodeList: string[]): Promise<INodeTypeDescription[]>;
getNodeParameterOptions(nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>; getNodeParameterOptions(nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
removeTestWebhook(workflowId: string): Promise<boolean>; removeTestWebhook(workflowId: string): Promise<boolean>;
runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>; runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>;
@@ -399,6 +400,10 @@ export interface IN8nUISettings {
timezone: string; timezone: string;
executionTimeout: number; executionTimeout: number;
maxExecutionTimeout: number; maxExecutionTimeout: number;
oauthCallbackUrls: {
oauth1: string;
oauth2: string;
};
urlBaseWebhook: string; urlBaseWebhook: string;
versionCli: string; versionCli: string;
} }

View File

@@ -4,7 +4,7 @@
<div name="title" class="title-container" slot="title"> <div name="title" class="title-container" slot="title">
<div class="title-left">{{title}}</div> <div class="title-left">{{title}}</div>
<div class="title-right"> <div class="title-right">
<div v-if="credentialType" class="docs-container"> <div v-if="credentialType && documentationUrl" class="docs-container">
<svg class="help-logo" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg class="help-logo" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Node Documentation</title> <title>Node Documentation</title>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
@@ -20,7 +20,7 @@
</g> </g>
</g> </g>
</svg> </svg>
<span v-if="credentialType" class="doc-link-text">Need help? <a class="doc-hyperlink" :href="'https://docs.n8n.io/credentials/' + documentationUrl + '/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal'" target="_blank">Open credential docs</a></span> <span class="doc-link-text">Need help? <a class="doc-hyperlink" :href="'https://docs.n8n.io/credentials/' + documentationUrl + '/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal'" target="_blank">Open credential docs</a></span>
</div> </div>
</div> </div>
</div> </div>
@@ -109,27 +109,19 @@ export default mixins(
} }
} }
}, },
documentationUrl (): string { documentationUrl (): string | undefined {
let credentialTypeName = '';
if (this.editCredentials) { if (this.editCredentials) {
const credentialType = this.$store.getters.credentialType(this.editCredentials.type); credentialTypeName = this.editCredentials.type as string;
if (credentialType.documentationUrl === undefined) {
return credentialType.name;
} else {
return `${credentialType.documentationUrl}`;
}
} else { } else {
if (this.credentialType) { credentialTypeName = this.credentialType as string;
const credentialType = this.$store.getters.credentialType(this.credentialType);
if (credentialType.documentationUrl === undefined) {
return credentialType.name;
} else {
return `${credentialType.documentationUrl}`;
}
} else {
return '';
}
} }
const credentialType = this.$store.getters.credentialType(credentialTypeName);
if (credentialType.documentationUrl !== undefined) {
return `${credentialType.documentationUrl}`;
}
return undefined;
}, },
node (): INodeUi { node (): INodeUi {
return this.$store.getters.activeNode; return this.$store.getters.activeNode;

View File

@@ -235,7 +235,7 @@ export default mixins(
oAuthCallbackUrl (): string { oAuthCallbackUrl (): string {
const types = this.parentTypes(this.credentialTypeData.name); const types = this.parentTypes(this.credentialTypeData.name);
const oauthType = (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) ? 'oauth2' : 'oauth1'; const oauthType = (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) ? 'oauth2' : 'oauth1';
return this.$store.getters.getWebhookBaseUrl + `rest/${oauthType}-credential/callback`; return this.$store.getters.oauthCallbackUrls[oauthType];
}, },
requiredPropertiesFilled (): boolean { requiredPropertiesFilled (): boolean {
for (const property of this.credentialProperties) { for (const property of this.credentialProperties) {
@@ -404,10 +404,11 @@ export default mixins(
message: 'Connected successfully!', message: 'Connected successfully!',
type: 'success', type: 'success',
}); });
// Make sure that the event gets removed again
window.removeEventListener('message', receiveMessage, false);
} }
// Make sure that the event gets removed again
window.removeEventListener('message', receiveMessage, false);
}; };
window.addEventListener('message', receiveMessage, false); window.addEventListener('message', receiveMessage, false);

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="node-wrapper" :style="nodePosition"> <div class="node-wrapper" :style="nodePosition">
<div class="node-default" :ref="data.name" :style="nodeStyle" :class="nodeClass" @dblclick="setNodeActive" @click.left="mouseLeftClick"> <div class="node-default" :ref="data.name" :style="nodeStyle" :class="nodeClass" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd">
<div v-if="hasIssues" class="node-info-icon node-issues"> <div v-if="hasIssues" class="node-info-icon node-issues">
<el-tooltip placement="top" effect="light"> <el-tooltip placement="top" effect="light">
<div slot="content" v-html="nodeIssues"></div> <div slot="content" v-html="nodeIssues"></div>
@@ -13,19 +13,19 @@
<font-awesome-icon icon="sync-alt" spin /> <font-awesome-icon icon="sync-alt" spin />
</div> </div>
<div class="node-options" v-if="!isReadOnly"> <div class="node-options" v-if="!isReadOnly">
<div @click.stop.left="deleteNode" class="option" title="Delete Node" > <div v-touch:tap="deleteNode" class="option" title="Delete Node" >
<font-awesome-icon icon="trash" /> <font-awesome-icon icon="trash" />
</div> </div>
<div @click.stop.left="disableNode" class="option" title="Activate/Deactivate Node" > <div v-touch:tap="disableNode" class="option" title="Activate/Deactivate Node" >
<font-awesome-icon :icon="nodeDisabledIcon" /> <font-awesome-icon :icon="nodeDisabledIcon" />
</div> </div>
<div @click.stop.left="duplicateNode" class="option" title="Duplicate Node" > <div v-touch:tap="duplicateNode" class="option" title="Duplicate Node" >
<font-awesome-icon icon="clone" /> <font-awesome-icon icon="clone" />
</div> </div>
<div @click.stop.left="setNodeActive" class="option touch" title="Edit Node" v-if="!isReadOnly"> <div v-touch:tap="setNodeActive" class="option touch" title="Edit Node" v-if="!isReadOnly">
<font-awesome-icon class="execute-icon" icon="cog" /> <font-awesome-icon class="execute-icon" icon="cog" />
</div> </div>
<div @click.stop.left="executeNode" class="option" title="Execute Node" v-if="!isReadOnly && !workflowRunning"> <div v-touch:tap="executeNode" class="option" title="Execute Node" v-if="!isReadOnly && !workflowRunning">
<font-awesome-icon class="execute-icon" icon="play-circle" /> <font-awesome-icon class="execute-icon" icon="play-circle" />
</div> </div>
</div> </div>
@@ -110,6 +110,10 @@ export default mixins(nodeBase, workflowHelpers).extend({
classes.push('is-touch-device'); classes.push('is-touch-device');
} }
if (this.isTouchActive) {
classes.push('touch-active');
}
return classes; return classes;
}, },
nodeIssues (): string { nodeIssues (): string {
@@ -134,7 +138,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
} }
if (this.nodeType !== null && this.nodeType.subtitle !== undefined) { if (this.nodeType !== null && this.nodeType.subtitle !== undefined) {
return this.workflow.getSimpleParameterValue(this.data as INode, this.nodeType.subtitle) as string | undefined; return this.workflow.expression.getSimpleParameterValue(this.data as INode, this.nodeType.subtitle) as string | undefined;
} }
if (this.data.parameters.operation !== undefined) { if (this.data.parameters.operation !== undefined) {
@@ -174,7 +178,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
}, },
data () { data () {
return { return {
isTouchDevice: 'ontouchstart' in window || navigator.msMaxTouchPoints, isTouchActive: false,
}; };
}, },
methods: { methods: {
@@ -199,6 +203,14 @@ export default mixins(nodeBase, workflowHelpers).extend({
setNodeActive () { setNodeActive () {
this.$store.commit('setActiveNode', this.data.name); this.$store.commit('setActiveNode', this.data.name);
}, },
touchStart () {
if (this.isTouchDevice === true && this.isMacOs === false && this.isTouchActive === false) {
this.isTouchActive = true;
setTimeout(() => {
this.isTouchActive = false;
}, 2000);
}
},
}, },
}); });
@@ -268,6 +280,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
} }
} }
&.touch-active,
&:hover { &:hover {
.node-execute { .node-execute {
display: initial; display: initial;

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="node-icon-wrapper" :style="iconStyleData"> <div class="node-icon-wrapper" :style="iconStyleData" :class="{full: isSvgIcon}">
<div v-if="nodeIconData !== null" class="icon"> <div v-if="nodeIconData !== null" class="icon">
<img :src="nodeIconData.path" style="width: 100%; height: 100%;" v-if="nodeIconData.type === 'file'"/> <img :src="nodeIconData.path" style="width: 100%; height: 100%;" v-if="nodeIconData.type === 'file'"/>
<font-awesome-icon :icon="nodeIconData.path" v-else-if="nodeIconData.type === 'fa'" /> <font-awesome-icon :icon="nodeIconData.path" v-else-if="nodeIconData.type === 'fa'" />
@@ -17,6 +17,7 @@ import Vue from 'vue';
interface NodeIconData { interface NodeIconData {
type: string; type: string;
path: string; path: string;
fileExtension?: string;
} }
export default Vue.extend({ export default Vue.extend({
@@ -41,6 +42,12 @@ export default Vue.extend({
'border-radius': Math.ceil(size / 2) + 'px', 'border-radius': Math.ceil(size / 2) + 'px',
}; };
}, },
isSvgIcon (): boolean {
if (this.nodeIconData && this.nodeIconData.type === 'file' && this.nodeIconData.fileExtension === 'svg') {
return true;
}
return false;
},
nodeIconData (): null | NodeIconData { nodeIconData (): null | NodeIconData {
if (this.nodeType === null) { if (this.nodeType === null) {
return null; return null;
@@ -51,13 +58,14 @@ export default Vue.extend({
if (this.nodeType.icon) { if (this.nodeType.icon) {
let type, path; let type, path;
[type, path] = this.nodeType.icon.split(':'); [type, path] = this.nodeType.icon.split(':');
const returnData = { const returnData: NodeIconData = {
type, type,
path, path,
}; };
if (type === 'file') { if (type === 'file') {
returnData.path = restUrl + '/node-icon/' + this.nodeType.name; returnData.path = restUrl + '/node-icon/' + this.nodeType.name;
returnData.fileExtension = path.split('.').slice(-1).join();
} }
return returnData; return returnData;
@@ -83,6 +91,10 @@ export default Vue.extend({
font-weight: bold; font-weight: bold;
font-size: 20px; font-size: 20px;
&.full .icon {
margin: 0.24em;
}
.node-icon-placeholder { .node-icon-placeholder {
text-align: center; text-align: center;
} }

View File

@@ -248,7 +248,7 @@ export default mixins(
type: 'boolean', type: 'boolean',
default: false, default: false,
noDataExpression: true, noDataExpression: true,
description: 'If active, the workflow continues even if this node\'s <br /execution fails. When this occurs, the node passes along input data from<br />previous nodes - so your workflow should account for unexpected output data.', description: 'If active, the workflow continues even if this node\'s <br />execution fails. When this occurs, the node passes along input data from<br />previous nodes - so your workflow should account for unexpected output data.',
}, },
] as INodeProperties[], ] as INodeProperties[],

View File

@@ -15,7 +15,7 @@ import Vue from 'vue';
export default Vue.extend( export default Vue.extend(
{ {
name: 'PageContentWrapper', name: 'PageContentWrapper',
} },
); );
</script> </script>

View File

@@ -250,7 +250,7 @@ export default mixins(
* @returns * @returns
* @memberof Workflow * @memberof Workflow
*/ */
getNodeOutputData (runData: IRunData, nodeName: string, filterText: string, itemIndex = 0, runIndex = 0, inputName = 'main', outputIndex = 0): IVariableSelectorOption[] | null { getNodeOutputData (runData: IRunData, nodeName: string, filterText: string, itemIndex = 0, runIndex = 0, inputName = 'main', outputIndex = 0, useShort = false): IVariableSelectorOption[] | null {
if (!runData.hasOwnProperty(nodeName)) { if (!runData.hasOwnProperty(nodeName)) {
// No data found for node // No data found for node
return null; return null;
@@ -291,9 +291,12 @@ export default mixins(
// Get json data // Get json data
if (outputData.hasOwnProperty('json')) { if (outputData.hasOwnProperty('json')) {
const jsonPropertyPrefix = useShort === true ? '$json' : `$node["${nodeName}"].json`;
const jsonDataOptions: IVariableSelectorOption[] = []; const jsonDataOptions: IVariableSelectorOption[] = [];
for (const propertyName of Object.keys(outputData.json)) { for (const propertyName of Object.keys(outputData.json)) {
jsonDataOptions.push.apply(jsonDataOptions, this.jsonDataToFilterOption(outputData.json[propertyName], `$node["${nodeName}"].json`, propertyName, filterText)); jsonDataOptions.push.apply(jsonDataOptions, this.jsonDataToFilterOption(outputData.json[propertyName], jsonPropertyPrefix, propertyName, filterText));
} }
if (jsonDataOptions.length) { if (jsonDataOptions.length) {
@@ -308,6 +311,9 @@ export default mixins(
// Get binary data // Get binary data
if (outputData.hasOwnProperty('binary')) { if (outputData.hasOwnProperty('binary')) {
const binaryPropertyPrefix = useShort === true ? '$binary' : `$node["${nodeName}"].binary`;
const binaryData = []; const binaryData = [];
let binaryPropertyData = []; let binaryPropertyData = [];
@@ -326,7 +332,7 @@ export default mixins(
binaryPropertyData.push( binaryPropertyData.push(
{ {
name: propertyName, name: propertyName,
key: `$node["${nodeName}"].binary.${dataPropertyName}.${propertyName}`, key: `${binaryPropertyPrefix}.${dataPropertyName}.${propertyName}`,
value: outputData.binary![dataPropertyName][propertyName], value: outputData.binary![dataPropertyName][propertyName],
}, },
); );
@@ -336,7 +342,7 @@ export default mixins(
binaryData.push( binaryData.push(
{ {
name: dataPropertyName, name: dataPropertyName,
key: `$node["${nodeName}"].binary.${dataPropertyName}`, key: `${binaryPropertyPrefix}.${dataPropertyName}`,
options: this.sortOptions(binaryPropertyData), options: this.sortOptions(binaryPropertyData),
allowParentSelect: true, allowParentSelect: true,
}, },
@@ -347,7 +353,7 @@ export default mixins(
returnData.push( returnData.push(
{ {
name: 'Binary', name: 'Binary',
key: `$node["${nodeName}"].binary`, key: binaryPropertyPrefix,
options: this.sortOptions(binaryData), options: this.sortOptions(binaryData),
allowParentSelect: true, allowParentSelect: true,
}, },
@@ -474,7 +480,7 @@ export default mixins(
// (example "IF" node. If node is connected to "true" or to "false" output) // (example "IF" node. If node is connected to "true" or to "false" output)
const outputIndex = this.workflow.getNodeConnectionOutputIndex(activeNode.name, parentNode[0], 'main'); const outputIndex = this.workflow.getNodeConnectionOutputIndex(activeNode.name, parentNode[0], 'main');
tempOutputData = this.getNodeOutputData(runData, parentNode[0], filterText, itemIndex, 0, 'main', outputIndex) as IVariableSelectorOption[]; tempOutputData = this.getNodeOutputData(runData, parentNode[0], filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[];
if (tempOutputData) { if (tempOutputData) {
if (JSON.stringify(tempOutputData).length < 102400) { if (JSON.stringify(tempOutputData).length < 102400) {

View File

@@ -0,0 +1,30 @@
import Vue from 'vue';
export const deviceSupportHelpers = Vue.extend({
data() {
return {
isTouchDevice: 'ontouchstart' in window || navigator.msMaxTouchPoints,
isMacOs: /(ipad|iphone|ipod|mac)/i.test(navigator.platform),
};
},
computed: {
// TODO: Check if used anywhere
controlKeyCode(): string {
if (this.isMacOs) {
return 'Meta';
}
return 'Control';
},
},
methods: {
isCtrlKeyPressed(e: MouseEvent | KeyboardEvent): boolean {
if (this.isTouchDevice === true) {
return true;
}
if (this.isMacOs) {
return e.metaKey;
}
return e.ctrlKey;
},
},
});

View File

@@ -2,20 +2,19 @@ import { INodeUi } from '@/Interface';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex'; import { nodeIndex } from '@/components/mixins/nodeIndex';
export const mouseSelect = mixins(nodeIndex).extend({ export const mouseSelect = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
data () { data () {
return { return {
selectActive: false, selectActive: false,
selectBox: document.createElement('span'), selectBox: document.createElement('span'),
}; };
}, },
computed: {
isMacOs (): boolean {
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
},
},
mounted () { mounted () {
this.createSelectBox(); this.createSelectBox();
}, },
@@ -34,6 +33,9 @@ export const mouseSelect = mixins(nodeIndex).extend({
this.$el.appendChild(this.selectBox); this.$el.appendChild(this.selectBox);
}, },
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean { isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
if (this.isTouchDevice === true) {
return true;
}
if (this.isMacOs) { if (this.isMacOs) {
return e.metaKey; return e.metaKey;
} }
@@ -125,6 +127,13 @@ export const mouseSelect = mixins(nodeIndex).extend({
}, },
mouseUpMouseSelect (e: MouseEvent) { mouseUpMouseSelect (e: MouseEvent) {
if (this.selectActive === false) { if (this.selectActive === false) {
if (this.isTouchDevice === true) {
// @ts-ignore
if (e.target && e.target.id.includes('node-view')) {
// Deselect all nodes
this.deselectAllNodes();
}
}
// If it is not active return direcly. // If it is not active return direcly.
// Else normal node dragging will not work. // Else normal node dragging will not work.
return; return;

View File

@@ -1,41 +1,43 @@
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
// @ts-ignore
import normalizeWheel from 'normalize-wheel';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex'; import { nodeIndex } from '@/components/mixins/nodeIndex';
export const moveNodeWorkflow = mixins(nodeIndex).extend({ export const moveNodeWorkflow = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
data () { data () {
return { return {
moveLastPosition: [0, 0], moveLastPosition: [0, 0],
}; };
}, },
computed: {
controlKeyCode (): string {
if (this.isMacOs) {
return 'Meta';
}
return 'Control';
},
isMacOs (): boolean {
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
},
},
methods: { methods: {
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean { getMousePosition(e: MouseEvent | TouchEvent) {
if (this.isMacOs) { // @ts-ignore
return e.metaKey; const x = e.pageX !== undefined ? e.pageX : (e.touches && e.touches[0] && e.touches[0].pageX ? e.touches[0].pageX : 0);
} // @ts-ignore
return e.ctrlKey; const y = e.pageY !== undefined ? e.pageY : (e.touches && e.touches[0] && e.touches[0].pageY ? e.touches[0].pageY : 0);
return {
x,
y,
};
}, },
moveWorkflow (e: MouseEvent) { moveWorkflow (e: MouseEvent) {
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition; const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
const nodeViewOffsetPositionX = offsetPosition[0] + (e.pageX - this.moveLastPosition[0]); const position = this.getMousePosition(e);
const nodeViewOffsetPositionY = offsetPosition[1] + (e.pageY - this.moveLastPosition[1]);
const nodeViewOffsetPositionX = offsetPosition[0] + (position.x - this.moveLastPosition[0]);
const nodeViewOffsetPositionY = offsetPosition[1] + (position.y - this.moveLastPosition[1]);
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true});
// Update the last position // Update the last position
this.moveLastPosition[0] = e.pageX; this.moveLastPosition[0] = position.x;
this.moveLastPosition[1] = e.pageY; this.moveLastPosition[1] = position.y;
}, },
mouseDownMoveWorkflow (e: MouseEvent) { mouseDownMoveWorkflow (e: MouseEvent) {
if (this.isCtrlKeyPressed(e) === false) { if (this.isCtrlKeyPressed(e) === false) {
@@ -51,8 +53,10 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({
this.$store.commit('setNodeViewMoveInProgress', true); this.$store.commit('setNodeViewMoveInProgress', true);
this.moveLastPosition[0] = e.pageX; const position = this.getMousePosition(e);
this.moveLastPosition[1] = e.pageY;
this.moveLastPosition[0] = position.x;
this.moveLastPosition[1] = position.y;
// @ts-ignore // @ts-ignore
this.$el.addEventListener('mousemove', this.mouseMoveNodeWorkflow); this.$el.addEventListener('mousemove', this.mouseMoveNodeWorkflow);
@@ -72,6 +76,15 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({
// Nothing else to do. Simply leave the node view at the current offset // Nothing else to do. Simply leave the node view at the current offset
}, },
mouseMoveNodeWorkflow (e: MouseEvent) { mouseMoveNodeWorkflow (e: MouseEvent) {
// @ts-ignore
if (e.target && !e.target.id.includes('node-view')) {
return;
}
if (this.$store.getters.isActionActive('dragActive')) {
return;
}
if (e.buttons === 0) { if (e.buttons === 0) {
// Mouse button is not pressed anymore so stop selection mode // Mouse button is not pressed anymore so stop selection mode
// Happens normally when mouse leave the view pressed and then // Happens normally when mouse leave the view pressed and then
@@ -84,9 +97,10 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({
this.moveWorkflow(e); this.moveWorkflow(e);
}, },
wheelMoveWorkflow (e: WheelEvent) { wheelMoveWorkflow (e: WheelEvent) {
const normalized = normalizeWheel(e);
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition; const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
const nodeViewOffsetPositionX = offsetPosition[0] - e.deltaX; const nodeViewOffsetPositionX = offsetPosition[0] - normalized.pixelX;
const nodeViewOffsetPositionY = offsetPosition[1] - e.deltaY; const nodeViewOffsetPositionY = offsetPosition[1] - normalized.pixelY;
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true});
}, },
}, },

View File

@@ -2,20 +2,20 @@ import { IConnectionsUi, IEndpointOptions, INodeUi, XYPositon } from '@/Interfac
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex'; import { nodeIndex } from '@/components/mixins/nodeIndex';
import { NODE_NAME_PREFIX } from '@/constants'; import { NODE_NAME_PREFIX } from '@/constants';
export const nodeBase = mixins(nodeIndex).extend({ export const nodeBase = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
mounted () { mounted () {
// Initialize the node // Initialize the node
if (this.data !== null) { if (this.data !== null) {
this.__addNode(this.data); this.__addNode(this.data);
} }
}, },
data () {
return {
};
},
computed: { computed: {
data (): INodeUi { data (): INodeUi {
return this.$store.getters.nodeByName(this.name); return this.$store.getters.nodeByName(this.name);
@@ -26,9 +26,6 @@ export const nodeBase = mixins(nodeIndex).extend({
} }
return false; return false;
}, },
isMacOs (): boolean {
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
},
nodeName (): string { nodeName (): string {
return NODE_NAME_PREFIX + this.nodeIndex; return NODE_NAME_PREFIX + this.nodeIndex;
}, },
@@ -336,26 +333,27 @@ export const nodeBase = mixins(nodeIndex).extend({
}); });
}, },
touchEnd(e: MouseEvent) {
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean { if (this.isTouchDevice) {
if (this.isMacOs) { if (this.$store.getters.isActionActive('dragActive')) {
return e.metaKey; this.$store.commit('removeActiveAction', 'dragActive');
}
return e.ctrlKey;
},
mouseLeftClick (e: MouseEvent) {
if (this.$store.getters.isActionActive('dragActive')) {
this.$store.commit('removeActiveAction', 'dragActive');
} else {
if (this.isCtrlKeyPressed(e) === false) {
this.$emit('deselectAllNodes');
} }
}
if (this.$store.getters.isNodeSelected(this.data.name)) { },
this.$emit('deselectNode', this.name); mouseLeftClick (e: MouseEvent) {
if (!this.isTouchDevice) {
if (this.$store.getters.isActionActive('dragActive')) {
this.$store.commit('removeActiveAction', 'dragActive');
} else { } else {
this.$emit('nodeSelected', this.name); if (this.isCtrlKeyPressed(e) === false) {
this.$emit('deselectAllNodes');
}
if (this.$store.getters.isNodeSelected(this.data.name)) {
this.$emit('deselectNode', this.name);
} else {
this.$emit('nodeSelected', this.name);
}
} }
} }
}, },

View File

@@ -152,6 +152,10 @@ export const restApi = Vue.extend({
return self.restApi().makeRestApiRequest('GET', `/node-types`); return self.restApi().makeRestApiRequest('GET', `/node-types`);
}, },
getNodesInformation: (nodeList: string[]): Promise<INodeTypeDescription[]> => {
return self.restApi().makeRestApiRequest('POST', `/node-types`, {nodeNames: nodeList});
},
// Returns all the parameter options from the server // Returns all the parameter options from the server
getNodeParameterOptions: (nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => { getNodeParameterOptions: (nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => {
const sendData = { const sendData = {

View File

@@ -360,7 +360,7 @@ export const workflowHelpers = mixins(
connectionInputData = []; connectionInputData = [];
} }
return workflow.getParameterValue(expression, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, true); return workflow.expression.getParameterValue(expression, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, true);
}, },
// Saves the currently loaded workflow to the database. // Saves the currently loaded workflow to the database.

View File

@@ -5,6 +5,7 @@ import Vue from 'vue';
import 'prismjs'; import 'prismjs';
import 'prismjs/themes/prism.css'; import 'prismjs/themes/prism.css';
import 'vue-prism-editor/dist/VuePrismEditor.css'; import 'vue-prism-editor/dist/VuePrismEditor.css';
import Vue2TouchEvents from 'vue2-touch-events';
import * as ElementUI from 'element-ui'; import * as ElementUI from 'element-ui';
// @ts-ignore // @ts-ignore
@@ -91,6 +92,9 @@ import {
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { store } from './store'; import { store } from './store';
Vue.use(Vue2TouchEvents);
Vue.use(ElementUI, { locale }); Vue.use(ElementUI, { locale });
library.add(faAngleDoubleLeft); library.add(faAngleDoubleLeft);

View File

@@ -8,6 +8,7 @@ import {
IConnection, IConnection,
IConnections, IConnections,
ICredentialType, ICredentialType,
IDataObject,
INodeConnections, INodeConnections,
INodeIssueData, INodeIssueData,
INodeTypeDescription, INodeTypeDescription,
@@ -56,6 +57,7 @@ export const store = new Vuex.Store({
executionTimeout: -1, executionTimeout: -1,
maxExecutionTimeout: Number.MAX_SAFE_INTEGER, maxExecutionTimeout: Number.MAX_SAFE_INTEGER,
versionCli: '0.0.0', versionCli: '0.0.0',
oauthCallbackUrls: {},
workflowExecutionData: null as IExecutionResponse | null, workflowExecutionData: null as IExecutionResponse | null,
lastSelectedNode: null as string | null, lastSelectedNode: null as string | null,
lastSelectedNodeOutputIndex: null as number | null, lastSelectedNodeOutputIndex: null as number | null,
@@ -535,6 +537,9 @@ export const store = new Vuex.Store({
setVersionCli (state, version: string) { setVersionCli (state, version: string) {
Vue.set(state, 'versionCli', version); Vue.set(state, 'versionCli', version);
}, },
setOauthCallbackUrls(state, urls: IDataObject) {
Vue.set(state, 'oauthCallbackUrls', urls);
},
addNodeType (state, typeData: INodeTypeDescription) { addNodeType (state, typeData: INodeTypeDescription) {
if (!typeData.hasOwnProperty('name')) { if (!typeData.hasOwnProperty('name')) {
@@ -602,6 +607,14 @@ export const store = new Vuex.Store({
Vue.set(state.workflow, 'settings', {}); Vue.set(state.workflow, 'settings', {});
} }
}, },
updateNodeTypes (state, nodeTypes: INodeTypeDescription[]) {
const updatedNodeNames = nodeTypes.map(node => node.name) as string[];
const oldNodesNotChanged = state.nodeTypes.filter(node => !updatedNodeNames.includes(node.name));
const updatedNodes = [...oldNodesNotChanged, ...nodeTypes];
Vue.set(state, 'nodeTypes', updatedNodes);
state.nodeTypes = updatedNodes;
},
}, },
getters: { getters: {
@@ -658,6 +671,9 @@ export const store = new Vuex.Store({
versionCli: (state): string => { versionCli: (state): string => {
return state.versionCli; return state.versionCli;
}, },
oauthCallbackUrls: (state): object => {
return state.oauthCallbackUrls;
},
// Push Connection // Push Connection
pushConnectionActive: (state): boolean => { pushConnectionActive: (state): boolean => {

View File

@@ -3,11 +3,15 @@
<div <div
class="node-view-wrapper" class="node-view-wrapper"
:class="workflowClasses" :class="workflowClasses"
@touchstart="mouseDown"
@touchend="mouseUp"
@touchmove="mouseMoveNodeWorkflow"
@mousedown="mouseDown" @mousedown="mouseDown"
v-touch:tap="touchTap"
@mouseup="mouseUp" @mouseup="mouseUp"
@wheel="wheelScroll" @wheel="wheelScroll"
> >
<div class="node-view-background" :style="backgroundStyle"></div> <div id="node-view-background" class="node-view-background" :style="backgroundStyle"></div>
<div id="node-view" class="node-view" :style="workflowStyle"> <div id="node-view" class="node-view" :style="workflowStyle">
<node <node
v-for="nodeData in nodes" v-for="nodeData in nodes"
@@ -356,14 +360,20 @@ export default mixins(
return data; return data;
}, },
mouseDown (e: MouseEvent) { touchTap (e: MouseEvent | TouchEvent) {
if (this.isTouchDevice) {
this.mouseDown(e);
}
},
mouseDown (e: MouseEvent | TouchEvent) {
// Save the location of the mouse click // Save the location of the mouse click
const position = this.getMousePosition(e);
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition; const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
this.lastClickPosition[0] = e.pageX - offsetPosition[0]; this.lastClickPosition[0] = position.x - offsetPosition[0];
this.lastClickPosition[1] = e.pageY - offsetPosition[1]; this.lastClickPosition[1] = position.y - offsetPosition[1];
this.mouseDownMouseSelect(e); this.mouseDownMouseSelect(e as MouseEvent);
this.mouseDownMoveWorkflow(e); this.mouseDownMoveWorkflow(e as MouseEvent);
// Hide the node-creator // Hide the node-creator
this.createNodeActive = false; this.createNodeActive = false;
@@ -962,7 +972,7 @@ export default mixins(
// If a node is active then add the new node directly after the current one // If a node is active then add the new node directly after the current one
// newNodeData.position = [activeNode.position[0], activeNode.position[1] + 60]; // newNodeData.position = [activeNode.position[0], activeNode.position[1] + 60];
newNodeData.position = this.getNewNodePosition( newNodeData.position = this.getNewNodePosition(
[lastSelectedNode.position[0] + 150, lastSelectedNode.position[1]], [lastSelectedNode.position[0] + 200, lastSelectedNode.position[1]],
[100, 0], [100, 0],
); );
} else { } else {
@@ -1456,6 +1466,11 @@ export default mixins(
[0, 150], [0, 150],
); );
if (newNodeData.webhookId) {
// Make sure that the node gets a new unique webhook-ID
newNodeData.webhookId = uuidv4();
}
await this.addNodes([newNodeData]); await this.addNodes([newNodeData]);
// Automatically deselect all nodes and select the current one and also active // Automatically deselect all nodes and select the current one and also active
@@ -1593,6 +1608,11 @@ export default mixins(
return; return;
} }
// Before proceeding we must check if all nodes contain the `properties` attribute.
// Nodes are loaded without this information so we must make sure that all nodes
// being added have this information.
await this.loadNodesProperties(nodes.map(node => node.type));
// Add the node to the node-list // Add the node to the node-list
let nodeType: INodeTypeDescription | null; let nodeType: INodeTypeDescription | null;
let foundNodeIssues: INodeIssues | null; let foundNodeIssues: INodeIssues | null;
@@ -1703,6 +1723,9 @@ export default mixins(
let oldName: string; let oldName: string;
let newName: string; let newName: string;
const createNodes: INode[] = []; const createNodes: INode[] = [];
await this.loadNodesProperties(data.nodes.map(node => node.type));
data.nodes.forEach(node => { data.nodes.forEach(node => {
if (nodeTypesCount[node.type] !== undefined) { if (nodeTypesCount[node.type] !== undefined) {
if (nodeTypesCount[node.type].exist >= nodeTypesCount[node.type].max) { if (nodeTypesCount[node.type].exist >= nodeTypesCount[node.type].max) {
@@ -1745,6 +1768,10 @@ export default mixins(
for (type of Object.keys(currentConnections[sourceNode])) { for (type of Object.keys(currentConnections[sourceNode])) {
connection[type] = []; connection[type] = [];
for (sourceIndex = 0; sourceIndex < currentConnections[sourceNode][type].length; sourceIndex++) { for (sourceIndex = 0; sourceIndex < currentConnections[sourceNode][type].length; sourceIndex++) {
if (!currentConnections[sourceNode][type][sourceIndex]) {
// There is so something wrong with the data so ignore
continue;
}
const nodeSourceConnections = []; const nodeSourceConnections = [];
for (connectionIndex = 0; connectionIndex < currentConnections[sourceNode][type][sourceIndex].length; connectionIndex++) { for (connectionIndex = 0; connectionIndex < currentConnections[sourceNode][type][sourceIndex].length; connectionIndex++) {
const nodeConnection: NodeInputConnections = []; const nodeConnection: NodeInputConnections = [];
@@ -1908,6 +1935,7 @@ export default mixins(
this.$store.commit('setExecutionTimeout', settings.executionTimeout); this.$store.commit('setExecutionTimeout', settings.executionTimeout);
this.$store.commit('setMaxExecutionTimeout', settings.maxExecutionTimeout); this.$store.commit('setMaxExecutionTimeout', settings.maxExecutionTimeout);
this.$store.commit('setVersionCli', settings.versionCli); this.$store.commit('setVersionCli', settings.versionCli);
this.$store.commit('setOauthCallbackUrls', settings.oauthCallbackUrls);
}, },
async loadNodeTypes (): Promise<void> { async loadNodeTypes (): Promise<void> {
const nodeTypes = await this.restApi().getNodeTypes(); const nodeTypes = await this.restApi().getNodeTypes();
@@ -1921,6 +1949,17 @@ export default mixins(
const credentials = await this.restApi().getAllCredentials(); const credentials = await this.restApi().getAllCredentials();
this.$store.commit('setCredentials', credentials); this.$store.commit('setCredentials', credentials);
}, },
async loadNodesProperties(nodeNames: string[]): Promise<void> {
const allNodes = this.$store.getters.allNodeTypes;
const nodesToBeFetched = allNodes.filter((node: INodeTypeDescription) => nodeNames.includes(node.name) && !node.hasOwnProperty('properties')).map((node: INodeTypeDescription) => node.name) as string[];
if (nodesToBeFetched.length > 0) {
// Only call API if node information is actually missing
this.startLoading();
const nodeInfo = await this.restApi().getNodesInformation(nodesToBeFetched);
this.$store.commit('updateNodeTypes', nodeInfo);
this.stopLoading();
}
},
}, },
async mounted () { async mounted () {

View File

@@ -47,6 +47,7 @@
"forin": true, "forin": true,
"jsdoc-format": true, "jsdoc-format": true,
"label-position": true, "label-position": true,
"indent": [true, "tabs", 2],
"member-access": [ "member-access": [
true, true,
"no-public" "no-public"
@@ -83,6 +84,18 @@
"ignore-bound-class-methods" "ignore-bound-class-methods"
], ],
"switch-default": true, "switch-default": true,
"trailing-comma": [
true,
{
"multiline": {
"objects": "always",
"arrays": "always",
"functions": "always",
"typeLiterals": "ignore"
},
"esSpecCompliant": true
}
],
"triple-equals": [ "triple-equals": [
true, true,
"allow-null-check" "allow-null-check"

View File

@@ -19,7 +19,7 @@ Condition notice.
Software: n8n Software: n8n
License: Apache 2.0 License: Apache 2.0 with Commons Clause
Licensor: n8n GmbH Licensor: n8n GmbH

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-node-dev", "name": "n8n-node-dev",
"version": "0.10.0", "version": "0.11.0",
"description": "CLI to simplify n8n credentials/node development", "description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@@ -24,6 +24,7 @@
"postpack": "rm -f oclif.manifest.json", "postpack": "rm -f oclif.manifest.json",
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest", "prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
"tslint": "tslint -p tsconfig.json -c tslint.json", "tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"watch": "tsc --watch" "watch": "tsc --watch"
}, },
"bin": { "bin": {
@@ -54,15 +55,16 @@
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
"@oclif/errors": "^1.2.2", "@oclif/errors": "^1.2.2",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/node": "^14.0.27", "@types/node": "14.0.27",
"change-case": "^4.1.1", "change-case": "^4.1.1",
"copyfiles": "^2.1.1", "copyfiles": "^2.1.1",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"n8n-core": "^0.44.0", "n8n-core": "^0.48.0",
"n8n-workflow": "^0.39.0", "n8n-workflow": "^0.42.0",
"oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0", "replace-in-file": "^6.0.0",
"request": "^2.88.2", "request": "^2.88.2",
"tmp-promise": "^2.0.2", "tmp-promise": "^2.0.2",
"typescript": "~3.7.4" "typescript": "~3.9.7"
} }
} }

View File

@@ -23,7 +23,7 @@ export async function createTemplate(sourceFilePath: string, destinationFilePath
// Replace the variables in the template file // Replace the variables in the template file
const options: ReplaceInFileConfig = { const options: ReplaceInFileConfig = {
files: [ files: [
destinationFilePath destinationFilePath,
], ],
from: [], from: [],
to: [], to: [],

View File

@@ -46,6 +46,11 @@
"forin": true, "forin": true,
"jsdoc-format": true, "jsdoc-format": true,
"label-position": true, "label-position": true,
"indent": [
true,
"tabs",
2
],
"member-access": [ "member-access": [
true, true,
"no-public" "no-public"
@@ -60,6 +65,13 @@
"no-default-export": true, "no-default-export": true,
"no-duplicate-variable": true, "no-duplicate-variable": true,
"no-inferrable-types": true, "no-inferrable-types": true,
"ordered-imports": [
true,
{
"import-sources-order": "any",
"named-imports-order": "case-insensitive"
}
],
"no-namespace": [ "no-namespace": [
true, true,
"allow-declarations" "allow-declarations"
@@ -82,6 +94,18 @@
"ignore-bound-class-methods" "ignore-bound-class-methods"
], ],
"switch-default": true, "switch-default": true,
"trailing-comma": [
true,
{
"multiline": {
"objects": "always",
"arrays": "always",
"functions": "always",
"typeLiterals": "ignore"
},
"esSpecCompliant": true
}
],
"triple-equals": [ "triple-equals": [
true, true,
"allow-null-check" "allow-null-check"

View File

@@ -19,7 +19,7 @@ Condition notice.
Software: n8n Software: n8n
License: Apache 2.0 License: Apache 2.0 with Commons Clause
Licensor: n8n GmbH Licensor: n8n GmbH

View File

@@ -31,7 +31,7 @@ export class AcuitySchedulingOAuth2Api implements ICredentialType {
name: 'scope', name: 'scope',
type: 'hidden' as NodePropertyTypes, type: 'hidden' as NodePropertyTypes,
default: 'api-v1', default: 'api-v1',
required: true required: true,
}, },
{ {
displayName: 'Auth URI Query Parameters', displayName: 'Auth URI Query Parameters',

View File

@@ -3,7 +3,6 @@ import {
NodePropertyTypes, NodePropertyTypes,
} from 'n8n-workflow'; } from 'n8n-workflow';
export class AsanaApi implements ICredentialType { export class AsanaApi implements ICredentialType {
name = 'asanaApi'; name = 'asanaApi';
displayName = 'Asana API'; displayName = 'Asana API';

View File

@@ -0,0 +1,48 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class AsanaOAuth2Api implements ICredentialType {
name = 'asanaOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Asana OAuth2 API';
documentationUrl = 'asana';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://app.asana.com/-/oauth_authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://app.asana.com/-/oauth_token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'body',
description: 'Resource to consume.',
},
];
}

View File

@@ -0,0 +1,18 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class AutomizyApi implements ICredentialType {
name = 'automizyApi';
displayName = 'Automizy API';
documentationUrl = 'automizy';
properties = [
{
displayName: 'API Token',
name: 'apiToken',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View File

@@ -6,9 +6,9 @@ import {
export class BitlyOAuth2Api implements ICredentialType { export class BitlyOAuth2Api implements ICredentialType {
name = 'bitlyOAuth2Api'; name = 'bitlyOAuth2Api';
displayName = 'Bitly OAuth2 API'; displayName = 'Bitly OAuth2 API';
documentationUrl = 'bitly'; documentationUrl = 'bitly';
extends = [ extends = [
'oAuth2Api', 'oAuth2Api',
]; ];
properties = [ properties = [

View File

@@ -0,0 +1,48 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class ClickUpOAuth2Api implements ICredentialType {
name = 'clickUpOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'ClickUp OAuth2 API';
documentationUrl = 'clickUp';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://app.clickup.com/api',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.clickup.com/api/v2/oauth/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'body',
description: 'Resource to consume.',
},
];
}

View File

@@ -7,6 +7,7 @@ import {
export class ContentfulApi implements ICredentialType { export class ContentfulApi implements ICredentialType {
name = 'contentfulApi'; name = 'contentfulApi';
displayName = 'Contenful API'; displayName = 'Contenful API';
documentationUrl = 'contentful';
properties = [ properties = [
{ {
displayName: 'Space ID', displayName: 'Space ID',

View File

@@ -7,6 +7,7 @@ import {
export class ConvertKitApi implements ICredentialType { export class ConvertKitApi implements ICredentialType {
name = 'convertKitApi'; name = 'convertKitApi';
displayName = 'ConvertKit API'; displayName = 'ConvertKit API';
documentationUrl = 'convertKit';
properties = [ properties = [
{ {
displayName: 'API Secret', displayName: 'API Secret',

View File

@@ -13,7 +13,7 @@ export class DisqusApi implements ICredentialType {
name: 'accessToken', name: 'accessToken',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '', default: '',
description: 'Visit your account details page, and grab the Access Token. See <a href="https://disqus.com/api/docs/auth/">Disqus auth</a>.' description: 'Visit your account details page, and grab the Access Token. See <a href="https://disqus.com/api/docs/auth/">Disqus auth</a>.',
}, },
]; ];
} }

View File

@@ -13,7 +13,7 @@ export class DriftApi implements ICredentialType {
name: 'accessToken', name: 'accessToken',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '', 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>.' description: 'Visit your account details page, and grab the Access Token. See <a href="https://devdocs.drift.com/docs/quick-start">Drift auth</a>.',
}, },
]; ];
} }

View File

@@ -25,8 +25,8 @@ export class DropboxOAuth2Api implements ICredentialType {
type: 'hidden' as NodePropertyTypes, type: 'hidden' as NodePropertyTypes,
default: 'https://api.dropboxapi.com/oauth2/token', default: 'https://api.dropboxapi.com/oauth2/token',
required: true, required: true,
}, },
{ {
displayName: 'Scope', displayName: 'Scope',
name: 'scope', name: 'scope',
type: 'hidden' as NodePropertyTypes, type: 'hidden' as NodePropertyTypes,

View File

@@ -42,7 +42,7 @@ export class EventbriteOAuth2Api implements ICredentialType {
displayName: 'Authentication', displayName: 'Authentication',
name: 'authentication', name: 'authentication',
type: 'hidden' as NodePropertyTypes, type: 'hidden' as NodePropertyTypes,
default: 'body' default: 'body',
}, },
]; ];
} }

View File

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

View File

@@ -14,7 +14,7 @@ export class Ftp implements ICredentialType {
required: true, required: true,
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '', default: '',
placeholder: 'localhost' placeholder: 'localhost',
}, },
{ {
displayName: 'Port', displayName: 'Port',

View File

@@ -0,0 +1,28 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'https://www.googleapis.com/auth/admin.directory.group',
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.domain.readonly',
'https://www.googleapis.com/auth/admin.directory.userschema.readonly',
];
export class GSuiteAdminOAuth2Api implements ICredentialType {
name = 'gSuiteAdminOAuth2Api';
extends = [
'googleOAuth2Api',
];
displayName = 'G Suite Admin OAuth2 API';
documentationUrl = 'google';
properties = [
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(' '),
},
];
}

View File

@@ -13,7 +13,7 @@ export class GitlabApi implements ICredentialType {
displayName: 'Gitlab Server', displayName: 'Gitlab Server',
name: 'server', name: 'server',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: 'https://gitlab.com' default: 'https://gitlab.com',
}, },
{ {
displayName: 'Access Token', displayName: 'Access Token',

View File

@@ -16,7 +16,7 @@ export class GitlabOAuth2Api implements ICredentialType {
displayName: 'Gitlab Server', displayName: 'Gitlab Server',
name: 'server', name: 'server',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: 'https://gitlab.com' default: 'https://gitlab.com',
}, },
{ {
displayName: 'Authorization URL', displayName: 'Authorization URL',

View File

@@ -27,7 +27,7 @@ export class GoogleOAuth2Api implements ICredentialType {
displayName: 'Auth URI Query Parameters', displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters', name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes, type: 'hidden' as NodePropertyTypes,
default: 'access_type=offline', default: 'access_type=offline&prompt=consent',
}, },
{ {
displayName: 'Authentication', displayName: 'Authentication',

View File

@@ -4,7 +4,6 @@ import {
} from 'n8n-workflow'; } from 'n8n-workflow';
const scopes = [ const scopes = [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/spreadsheets',
]; ];

View File

@@ -17,7 +17,7 @@ export class GoogleTasksOAuth2Api implements ICredentialType {
displayName: 'Scope', displayName: 'Scope',
name: 'scope', name: 'scope',
type: 'hidden' as NodePropertyTypes, type: 'hidden' as NodePropertyTypes,
default: scopes.join(' ') default: scopes.join(' '),
}, },
]; ];
} }

View File

@@ -0,0 +1,25 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'https://www.googleapis.com/auth/cloud-translation',
];
export class GoogleTranslateOAuth2Api implements ICredentialType {
name = 'googleTranslateOAuth2Api';
extends = [
'googleOAuth2Api',
];
displayName = 'Google Translate OAuth2 API';
documentationUrl = 'google';
properties = [
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(' '),
},
];
}

View File

@@ -13,14 +13,14 @@ export class HarvestApi implements ICredentialType {
name: 'accountId', name: 'accountId',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '', 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>.' 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', displayName: 'Access Token',
name: 'accessToken', name: 'accessToken',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '', 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>.' description: 'Visit your account details page, and grab the Access Token. See <a href="https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/">Harvest Personal Access Tokens</a>.',
}, },
]; ];
} }

View File

@@ -5,7 +5,7 @@ import {
export class HubspotDeveloperApi implements ICredentialType { export class HubspotDeveloperApi implements ICredentialType {
name = 'hubspotDeveloperApi'; name = 'hubspotDeveloperApi';
displayName = 'Hubspot API'; displayName = 'Hubspot Developer API';
documentationUrl = 'hubspot'; documentationUrl = 'hubspot';
properties = [ properties = [
{ {

View File

@@ -44,11 +44,11 @@ export class HubspotOAuth2Api implements ICredentialType {
default: 'grant_type=authorization_code', default: 'grant_type=authorization_code',
}, },
{ {
displayName: 'Authentication', displayName: 'Authentication',
name: 'authentication', name: 'authentication',
type: 'hidden' as NodePropertyTypes, type: 'hidden' as NodePropertyTypes,
default: 'body', default: 'body',
description: 'Resource to consume.', description: 'Resource to consume.',
}, },
]; ];
} }

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