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
License: Apache 2.0
License: Apache 2.0 with Commons Clause
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.
## 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
### What changed?

View File

@@ -19,7 +19,7 @@ Condition notice.
Software: n8n
License: Apache 2.0
License: Apache 2.0 with Commons Clause
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
'-v',
'-V',
'--version'
'--version',
];
if (versionFlags.includes(process.argv.slice(-1)[0])) {
console.log(require('../package').version);
@@ -22,23 +22,10 @@ if (process.argv.length === 2) {
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
var supportedCommands = [ // tslint:disable-line:no-var-keyword
'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');
if (parseInt(nodeVersion[0], 10) < 12 || parseInt(nodeVersion[0], 10) === 12 && parseInt(nodeVersion[1], 10) < 9) {
console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 12.9 or later!\n`);
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 {
ActiveExecutions,
CredentialsOverwrites,
CredentialTypes,
Db,
ExternalHooks,
GenericHelpers,
@@ -116,6 +117,8 @@ export class Execute extends Command {
// Add the found types to an instance other parts of the application can use
const nodeTypes = NodeTypes();
await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
const credentialTypes = CredentialTypes();
await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) {
workflowId = undefined;

View File

@@ -8,12 +8,14 @@ const open = require('open');
import * as config from '../config';
import {
ActiveExecutions,
ActiveWorkflowRunner,
CredentialTypes,
CredentialsOverwrites,
CredentialTypes,
Db,
ExternalHooks,
GenericHelpers,
IExecutionsCurrentSummary,
LoadNodesAndCredentials,
NodeTypes,
Server,
@@ -68,23 +70,46 @@ export class Start extends Command {
static async stopProcess() {
console.log(`\nStopping n8n...`);
setTimeout(() => {
// In case that something goes wrong with shutdown we
// kill after max. 30 seconds no matter what
process.exit(processExistCode);
}, 30000);
try {
const externalHooks = ExternalHooks();
await externalHooks.run('n8n.stop', []);
const removePromises = [];
if (activeWorkflowRunner !== undefined) {
removePromises.push(activeWorkflowRunner.removeAll());
setTimeout(() => {
// In case that something goes wrong with shutdown we
// 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);
}

View File

@@ -10,58 +10,58 @@ const config = convict({
doc: 'Type of database to use',
format: ['sqlite', 'mariadb', 'mongodb', 'mysqldb', 'postgresdb'],
default: 'sqlite',
env: 'DB_TYPE'
env: 'DB_TYPE',
},
mongodb: {
connectionUrl: {
doc: 'MongoDB Connection URL',
format: '*',
default: 'mongodb://user:password@localhost:27017/database',
env: 'DB_MONGODB_CONNECTION_URL'
}
env: 'DB_MONGODB_CONNECTION_URL',
},
},
tablePrefix: {
doc: 'Prefix for table names',
format: '*',
default: '',
env: 'DB_TABLE_PREFIX'
env: 'DB_TABLE_PREFIX',
},
postgresdb: {
database: {
doc: 'PostgresDB Database',
format: String,
default: 'n8n',
env: 'DB_POSTGRESDB_DATABASE'
env: 'DB_POSTGRESDB_DATABASE',
},
host: {
doc: 'PostgresDB Host',
format: String,
default: 'localhost',
env: 'DB_POSTGRESDB_HOST'
env: 'DB_POSTGRESDB_HOST',
},
password: {
doc: 'PostgresDB Password',
format: String,
default: '',
env: 'DB_POSTGRESDB_PASSWORD'
env: 'DB_POSTGRESDB_PASSWORD',
},
port: {
doc: 'PostgresDB Port',
format: Number,
default: 5432,
env: 'DB_POSTGRESDB_PORT'
env: 'DB_POSTGRESDB_PORT',
},
user: {
doc: 'PostgresDB User',
format: String,
default: 'root',
env: 'DB_POSTGRESDB_USER'
env: 'DB_POSTGRESDB_USER',
},
schema: {
doc: 'PostgresDB Schema',
format: String,
default: 'public',
env: 'DB_POSTGRESDB_SCHEMA'
env: 'DB_POSTGRESDB_SCHEMA',
},
ssl: {
@@ -89,7 +89,7 @@ const config = convict({
default: true,
env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED',
},
}
},
},
mysqldb: {
@@ -97,31 +97,31 @@ const config = convict({
doc: 'MySQL Database',
format: String,
default: 'n8n',
env: 'DB_MYSQLDB_DATABASE'
env: 'DB_MYSQLDB_DATABASE',
},
host: {
doc: 'MySQL Host',
format: String,
default: 'localhost',
env: 'DB_MYSQLDB_HOST'
env: 'DB_MYSQLDB_HOST',
},
password: {
doc: 'MySQL Password',
format: String,
default: '',
env: 'DB_MYSQLDB_PASSWORD'
env: 'DB_MYSQLDB_PASSWORD',
},
port: {
doc: 'MySQL Port',
format: Number,
default: 3306,
env: 'DB_MYSQLDB_PORT'
env: 'DB_MYSQLDB_PORT',
},
user: {
doc: 'MySQL User',
format: String,
default: 'root',
env: 'DB_MYSQLDB_USER'
env: 'DB_MYSQLDB_USER',
},
},
},
@@ -136,7 +136,7 @@ const config = convict({
doc: 'Overwrites for credentials',
format: '*',
default: '{}',
env: 'CREDENTIALS_OVERWRITE_DATA'
env: 'CREDENTIALS_OVERWRITE_DATA',
},
endpoint: {
doc: 'Fetch credentials from API',
@@ -156,7 +156,7 @@ const config = convict({
doc: 'In what process workflows should be executed',
format: ['main', 'own'],
default: 'own',
env: 'EXECUTIONS_PROCESS'
env: 'EXECUTIONS_PROCESS',
},
// 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',
format: Number,
default: -1,
env: 'EXECUTIONS_TIMEOUT'
env: 'EXECUTIONS_TIMEOUT',
},
maxTimeout: {
doc: 'Max execution time (seconds) that can be set for a workflow individually',
format: Number,
default: 3600,
env: 'EXECUTIONS_TIMEOUT_MAX'
env: 'EXECUTIONS_TIMEOUT_MAX',
},
// 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',
format: ['all', 'none'],
default: 'all',
env: 'EXECUTIONS_DATA_SAVE_ON_ERROR'
env: 'EXECUTIONS_DATA_SAVE_ON_ERROR',
},
saveDataOnSuccess: {
doc: 'What workflow execution data to save on success',
format: ['all', 'none'],
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
@@ -211,7 +211,7 @@ const config = convict({
doc: 'Save data of executions when started manually via editor',
format: 'Boolean',
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
@@ -223,19 +223,19 @@ const config = convict({
doc: 'Delete data of past executions on a rolling basis',
format: 'Boolean',
default: false,
env: 'EXECUTIONS_DATA_PRUNE'
env: 'EXECUTIONS_DATA_PRUNE',
},
pruneDataMaxAge: {
doc: 'How old (hours) the execution data has to be to get deleted',
format: Number,
default: 336,
env: 'EXECUTIONS_DATA_MAX_AGE'
env: 'EXECUTIONS_DATA_MAX_AGE',
},
pruneDataTimeout: {
doc: 'Timeout (seconds) after execution data has been pruned',
format: Number,
default: 3600,
env: 'EXECUTIONS_DATA_PRUNE_TIMEOUT'
env: 'EXECUTIONS_DATA_PRUNE_TIMEOUT',
},
},
@@ -248,7 +248,7 @@ const config = convict({
doc: 'The timezone to use',
format: '*',
default: 'America/New_York',
env: 'GENERIC_TIMEZONE'
env: 'GENERIC_TIMEZONE',
},
},
@@ -258,66 +258,78 @@ const config = convict({
default: '/',
arg: 'path',
env: 'N8N_PATH',
doc: 'Path n8n is deployed to'
doc: 'Path n8n is deployed to',
},
host: {
format: String,
default: 'localhost',
arg: 'host',
env: 'N8N_HOST',
doc: 'Host name n8n can be reached'
doc: 'Host name n8n can be reached',
},
port: {
format: Number,
default: 5678,
arg: 'port',
env: 'N8N_PORT',
doc: 'HTTP port n8n can be reached'
doc: 'HTTP port n8n can be reached',
},
listen_address: {
format: String,
default: '0.0.0.0',
env: 'N8N_LISTEN_ADDRESS',
doc: 'IP address n8n should listen on'
doc: 'IP address n8n should listen on',
},
protocol: {
format: ['http', 'https'],
default: 'http',
env: 'N8N_PROTOCOL',
doc: 'HTTP Protocol via which n8n can be reached'
doc: 'HTTP Protocol via which n8n can be reached',
},
ssl_key: {
format: String,
default: '',
env: 'N8N_SSL_KEY',
doc: 'SSL Key for HTTPS Protocol'
doc: 'SSL Key for HTTPS Protocol',
},
ssl_cert: {
format: String,
default: '',
env: 'N8N_SSL_CERT',
doc: 'SSL Cert for HTTPS Protocol'
doc: 'SSL Cert for HTTPS Protocol',
},
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: {
active: {
format: 'Boolean',
default: false,
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: {
format: String,
default: '',
env: 'N8N_BASIC_AUTH_USER',
doc: 'The name of the basic auth user'
doc: 'The name of the basic auth user',
},
password: {
format: String,
default: '',
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: {
@@ -325,49 +337,49 @@ const config = convict({
format: 'Boolean',
default: false,
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: {
format: String,
default: '',
env: 'N8N_JWT_AUTH_HEADER',
doc: 'The request header containing a signed JWT'
doc: 'The request header containing a signed JWT',
},
jwtHeaderValuePrefix: {
format: String,
default: '',
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: {
format: String,
default: '',
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: {
format: String,
default: '',
env: 'N8N_JWT_ISSUER',
doc: 'JWT issuer to expect (optional)'
doc: 'JWT issuer to expect (optional)',
},
jwtNamespace: {
format: String,
default: '',
env: 'N8N_JWT_NAMESPACE',
doc: 'JWT namespace to expect (optional)'
doc: 'JWT namespace to expect (optional)',
},
jwtAllowedTenantKey: {
format: String,
default: '',
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: {
format: String,
default: '',
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,
default: 'rest',
env: 'N8N_ENDPOINT_REST',
doc: 'Path for rest endpoint'
doc: 'Path for rest endpoint',
},
webhook: {
format: String,
default: 'webhook',
env: 'N8N_ENDPOINT_WEBHOOK',
doc: 'Path for webhook endpoint'
doc: 'Path for webhook endpoint',
},
webhookTest: {
format: String,
default: '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 (":")',
format: String,
default: '',
env: 'EXTERNAL_HOOK_FILES'
env: 'EXTERNAL_HOOK_FILES',
},
nodes: {
@@ -421,13 +433,13 @@ const config = convict({
}
},
default: '[]',
env: 'NODES_EXCLUDE'
env: 'NODES_EXCLUDE',
},
errorTriggerType: {
doc: 'Node Type to use as Error Trigger',
format: String,
default: 'n8n-nodes-base.errorTrigger',
env: 'NODES_ERROR_TRIGGER_TYPE'
env: 'NODES_ERROR_TRIGGER_TYPE',
},
},

View File

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

View File

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

View File

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

View File

@@ -5,9 +5,14 @@ import {
import {
ICredentialDataDecryptedObject,
ICredentialsHelper,
INode,
INodeParameters,
INodeProperties,
INodeType,
INodeTypeData,
INodeTypes,
NodeHelpers,
Workflow,
} from 'n8n-workflow';
import {
@@ -18,6 +23,19 @@ import {
} 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 {
/**
@@ -107,7 +125,7 @@ export class CredentialsHelper extends ICredentialsHelper {
const credentialsProperties = this.getCredentialsProperties(type);
// 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) {
// 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;
}
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
const credentialsOverwrites = CredentialsOverwrites();
return credentialsOverwrites.applyOverwrite(type, decryptedData);

View File

@@ -3,32 +3,53 @@ import {
} from 'n8n-workflow';
import {
ICredentialsOverwrite,
CredentialTypes,
GenericHelpers,
ICredentialsOverwrite,
} from './';
class CredentialsOverwritesClass {
private credentialTypes = CredentialTypes();
private overwriteData: ICredentialsOverwrite = {};
private resolvedTypes: string[] = [];
async init(overwriteData?: ICredentialsOverwrite) {
if (overwriteData !== undefined) {
// If data is already given it can directly be set instead of
// loaded from environment
this.overwriteData = overwriteData;
this.__setData(JSON.parse(JSON.stringify(overwriteData)));
return;
}
const data = await GenericHelpers.getConfigValue('credentials.overwrite.data') as string;
try {
this.overwriteData = JSON.parse(data);
const overwriteData = JSON.parse(data);
this.__setData(overwriteData);
} catch (error) {
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) {
const overwrites = this.get(type);
@@ -48,10 +69,45 @@ class CredentialsOverwritesClass {
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 {
return this.overwriteData[type];
}
getAll(): ICredentialsOverwrite {
return this.overwriteData;
}

View File

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

View File

@@ -1,7 +1,7 @@
import {
Db,
IExternalHooksFunctions,
IExternalHooksClass,
IExternalHooksFunctions,
} from './';
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;
executionTimeout: number;
maxExecutionTimeout: number;
oauthCallbackUrls: {
oauth1: string;
oauth2: string;
};
timezone: string;
urlBaseWebhook: string;
versionCli: string;

View File

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

View File

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

View File

@@ -20,20 +20,24 @@ import { RequestOptions } from 'oauth-1.0a';
import * as csrf from 'csrf';
import * as requestPromise from 'request-promise-native';
import { createHmac } from 'crypto';
import { compare } from 'bcryptjs';
import {
ActiveExecutions,
ActiveWorkflowRunner,
CredentialsHelper,
CredentialsOverwrites,
CredentialTypes,
Db,
ExternalHooks,
GenericHelpers,
IActivationError,
ICustomRequest,
ICredentialsDb,
ICredentialsDecryptedDb,
ICredentialsDecryptedResponse,
ICredentialsOverwrite,
ICredentialsResponse,
ICustomRequest,
IExecutionDeleteFilter,
IExecutionFlatted,
IExecutionFlattedDb,
@@ -46,21 +50,18 @@ import {
IN8nUISettings,
IPackageVersions,
IWorkflowBase,
IWorkflowShortResponse,
IWorkflowResponse,
IWorkflowExecutionDataProcess,
IWorkflowResponse,
IWorkflowShortResponse,
LoadNodesAndCredentials,
NodeTypes,
Push,
ResponseHelper,
TestWebhooks,
WorkflowCredentials,
WebhookHelpers,
WorkflowCredentials,
WorkflowExecuteAdditionalData,
WorkflowRunner,
GenericHelpers,
CredentialsOverwrites,
ICredentialsOverwrite,
LoadNodesAndCredentials,
} from './';
import {
@@ -74,9 +75,9 @@ import {
ICredentialType,
IDataObject,
INodeCredentials,
INodeTypeDescription,
INodeParameters,
INodePropertyOptions,
INodeTypeDescription,
IRunData,
IWorkflowCredentials,
Workflow,
@@ -120,7 +121,7 @@ class App {
push: Push.Push;
versions: IPackageVersions | undefined;
restEndpoint: string;
frontendSettings: IN8nUISettings;
protocol: string;
sslKey: string;
sslCert: string;
@@ -154,6 +155,25 @@ class App {
this.presetCredentialsLoaded = false;
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> {
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
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!');
}
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)) {
return next();
}
@@ -198,12 +231,27 @@ class App {
return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization is required!');
}
if (basicAuthData.name !== basicAuthUser || basicAuthData.pass !== basicAuthPassword) {
// Provided authentication data is wrong
return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization data is wrong!');
if (basicAuthData.name === basicAuthUser) {
if (basicAuthHashEnabled === true) {
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 = {
issuer: jwtIssuer !== '' ? jwtIssuer : undefined,
ignoreExpiration: false
ignoreExpiration: false,
};
jwt.verify(token, getKey, jwtVerifyOptions, (err: jwt.VerifyErrors, decoded: object) => {
@@ -307,7 +355,7 @@ class App {
limit: '16mb', verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
}
},
}));
// Support application/xml type post data
@@ -317,14 +365,14 @@ class App {
normalize: true, // Trim whitespace inside text nodes
normalizeTags: true, // Transform tags to lowercase
explicitArray: false, // Only put properties in array if length > 1
}
},
}));
this.app.use(bodyParser.text({
limit: '16mb', verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
}
},
}));
// Make sure that Vue history mode works properly
@@ -334,9 +382,9 @@ class App {
from: new RegExp(`^\/(${this.restEndpoint}|healthz|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`),
to: (context) => {
return context.parsedUrl!.pathname!.toString();
}
}
]
},
},
],
}));
//support application/x-www-form-urlencoded post data
@@ -344,7 +392,7 @@ class App {
verify: (req, res, buf) => {
// @ts-ignore
req.rawBody = buf;
}
},
}));
if (process.env['NODE_ENV'] !== 'production') {
@@ -532,6 +580,7 @@ class App {
newWorkflowData.updatedAt = this.getCurrentDate();
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
// nor the new value. So query now the hopefully updated entry.
@@ -580,6 +629,7 @@ class App {
}
await Db.collections.Workflow!.delete(id);
await this.externalHooks.run('workflow.afterDelete', [id]);
return true;
}));
@@ -665,13 +715,36 @@ class App {
const allNodes = nodeTypes.getAll();
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;
}));
// 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
@@ -1009,7 +1082,7 @@ class App {
const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string;
const oauth = new clientOAuth1({
const oAuthOptions: clientOAuth1.Options = {
consumer: {
key: _.get(oauthCredentials, 'consumerKey') as string,
secret: _.get(oauthCredentials, 'consumerSecret') as string,
@@ -1021,16 +1094,20 @@ class App {
.update(base)
.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 = {
method: 'POST',
url: (_.get(oauthCredentials, 'requestTokenUrl') as string),
data: {
oauth_callback: callback,
},
data: oauthRequestData,
};
const data = oauth.toHeader(oauth.authorize(options as RequestOptions));
@@ -1099,7 +1176,7 @@ class App {
qs: {
oauth_token,
oauth_verifier,
}
},
};
let oauthToken;
@@ -1169,11 +1246,11 @@ class App {
const csrfSecret = token.secretSync();
const state = {
token: token.create(csrfSecret),
cid: req.query.id
cid: req.query.id,
};
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,
clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string,
accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string,
@@ -1181,7 +1258,11 @@ class App {
redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`,
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','),
state: stateEncodedStr,
});
};
await this.externalHooks.run('oauth2.authenticate', [oAuthOptions]);
const oAuthObj = new clientOAuth2(oAuthOptions);
// Encrypt the data
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
@@ -1267,11 +1348,11 @@ class App {
const oAuth2Parameters = {
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,
authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string,
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') {
@@ -1283,13 +1364,14 @@ class App {
};
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 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) {
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
this.app.get(`/${this.restEndpoint}/settings`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IN8nUISettings> => {
return {
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,
};
return this.frontendSettings;
}));
@@ -1829,7 +1900,7 @@ class App {
// got used
res.setHeader('Last-Modified', startTime);
}
}
},
}));
}
@@ -1860,5 +1931,7 @@ export async function start(): Promise<void> {
const versions = await GenericHelpers.getVersions();
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
console.log(`Version: ${versions.cli}`);
await app.externalHooks.run('n8n.ready', [app]);
});
}

View File

@@ -3,11 +3,9 @@ import * as express from 'express';
import {
IResponseCallbackData,
IWorkflowDb,
NodeTypes,
Push,
ResponseHelper,
WebhookHelpers,
WorkflowHelpers,
} from './';
import {
@@ -31,6 +29,7 @@ export class TestWebhooks {
sessionId?: string;
timeout: NodeJS.Timeout,
workflowData: IWorkflowDb;
workflow: Workflow;
};
} = {};
private activeWebhooks: ActiveWebhooks | null = null;
@@ -64,10 +63,13 @@ export class TestWebhooks {
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 = new Workflow({ id: webhookData.workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings});
const workflow = this.testWebhookData[webhookKey].workflow;
// Get the node which has the webhook defined to know where to start from and to
// get additional data
@@ -154,19 +156,26 @@ export class TestWebhooks {
}, 120000);
let key: string;
const activatedKey: string[] = [];
for (const webhookData of webhooks) {
key = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path);
await this.activeWebhooks!.add(workflow, webhookData, mode);
activatedKey.push(key);
this.testWebhookData[key] = {
sessionId,
timeout,
workflow,
workflowData,
};
// Save static data!
this.testWebhookData[key].workflowData.staticData = workflow.staticData;
try {
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;
@@ -181,8 +190,6 @@ export class TestWebhooks {
* @memberof TestWebhooks
*/
cancelTestWebhook(workflowId: string): boolean {
const nodeTypes = NodeTypes();
let foundWebhook = false;
for (const webhookKey of Object.keys(this.testWebhookData)) {
const webhookData = this.testWebhookData[webhookKey];
@@ -191,8 +198,6 @@ export class TestWebhooks {
continue;
}
foundWebhook = true;
clearTimeout(this.testWebhookData[webhookKey].timeout);
// Inform editor-ui that webhook got received
@@ -205,12 +210,17 @@ export class TestWebhooks {
}
}
const workflowData = webhookData.workflowData;
const workflow = new Workflow({ id: workflowData.id.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
const workflow = this.testWebhookData[webhookKey].workflow;
// Remove the webhook
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;
@@ -225,14 +235,10 @@ export class TestWebhooks {
return;
}
const nodeTypes = NodeTypes();
let workflowData: IWorkflowDb;
let workflow: Workflow;
const workflows: Workflow[] = [];
for (const webhookKey of Object.keys(this.testWebhookData)) {
workflowData = this.testWebhookData[webhookKey].workflowData;
workflow = new Workflow({ id: workflowData.id.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
workflow = this.testWebhookData[webhookKey].workflow;
workflows.push(workflow);
}

View File

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

View File

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

View File

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

View File

@@ -24,8 +24,8 @@ import {
IExecutionError,
IRun,
Workflow,
WorkflowHooks,
WorkflowExecuteMode,
WorkflowHooks,
} from 'n8n-workflow';
import * as config from '../config';
@@ -104,11 +104,25 @@ export class WorkflowRunner {
await externalHooks.run('workflow.execute', [data.workflowData, data.executionMode]);
const executionsProcess = config.get('executions.process') as string;
let executionId: string;
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 credentialTypeData: ICredentialsTypeData;
let credentialsOverwrites = this.credentialsOverwrites;
if (loadAllNodeTypes === true) {
// Supply all nodeTypes and credentialTypes
@@ -219,15 +234,22 @@ export class WorkflowRunner {
const credentialTypes = CredentialTypes();
credentialTypeData = credentialTypes.credentialTypes;
} 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);
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).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
const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);

View File

@@ -66,7 +66,7 @@ export class WorkflowRunnerProcess {
// Load the credentials overwrites if any exist
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});
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials);
@@ -135,13 +135,13 @@ export class WorkflowRunnerProcess {
workflowExecuteBefore: [
async (): Promise<void> => {
this.sendHookToParentProcess('workflowExecuteBefore', []);
}
},
],
workflowExecuteAfter: [
async (fullRunData: IRun, newStaticData?: IDataObject): Promise<void> => {
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 });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,7 +52,7 @@ export class ActiveWebhooks {
try {
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
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
@@ -60,7 +60,6 @@ export class ActiveWebhooks {
} catch (error) {
// If there was a problem unregister the webhook again
delete this.webhookUrls[webhookKey];
delete this.workflowWebhooks[webhookData.workflowId];
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> {
const removePromises = [];

View File

@@ -67,8 +67,6 @@ export class ActiveWorkflows {
* @memberof ActiveWorkflows
*/
async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise<void> {
console.log('ADD ID (active): ' + id);
this.workflowData[id] = {};
const triggerNodes = workflow.getTriggerNodes();
@@ -204,8 +202,6 @@ export class ActiveWorkflows {
* @memberof ActiveWorkflows
*/
async remove(id: string): Promise<void> {
console.log('REMOVE ID (active): ' + id);
if (!this.isActive(id)) {
// Workflow is currently not registered
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,
} from 'n8n-workflow';
import { enc, AES } from 'crypto-js';
import { AES, enc } from 'crypto-js';
export class Credentials extends ICredentials {

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import {
BINARY_ENCODING,
IHookFunctions,
ILoadOptionsFunctions,
IResponseError,
IWorkflowSettings,
BINARY_ENCODING,
} from './';
import {
@@ -19,6 +19,7 @@ import {
INodeExecutionData,
INodeParameters,
INodeType,
IOAuth2Options,
IPollFunctions,
IRunExecutionData,
ITaskDataConnections,
@@ -34,7 +35,6 @@ import {
Workflow,
WorkflowDataProxy,
WorkflowExecuteMode,
IOAuth2Options,
} from 'n8n-workflow';
import * as clientOAuth1 from 'oauth-1.0a';
@@ -43,7 +43,7 @@ import * as clientOAuth2 from 'client-oauth2';
import { get } from 'lodash';
import * as express from 'express';
import * as path from 'path';
import { OptionsWithUrl, OptionsWithUri } from 'request';
import { OptionsWithUri, OptionsWithUrl } from 'request';
import * as requestPromise from 'request-promise-native';
import { createHmac } from 'crypto';
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
// 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.
data: binaryData.toString(BINARY_ENCODING)
data: binaryData.toString(BINARY_ENCODING),
};
if (filePath) {
@@ -152,11 +152,16 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin
// on the token-type used.
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)
.catch(async (error: IResponseError) => {
// TODO: Check if also other codes are possible
if (error.statusCode === 401) {
// TODO: Whole refresh process is not tested yet
// Token is probably not valid anymore. So try refresh it.
const tokenRefreshOptions: IDataObject = {};
@@ -388,7 +393,7 @@ export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecu
let returnData;
try {
returnData = workflow.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData);
returnData = workflow.expression.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData);
} catch (e) {
e.message += ` [Error in parameter: "${parameterName}"]`;
throw e;
@@ -434,12 +439,12 @@ export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode,
return undefined;
}
const path = workflow.getSimpleParameterValue(node, webhookDescription['path']);
const path = workflow.expression.getSimpleParameterValue(node, webhookDescription['path']);
if (path === 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);
}
@@ -654,7 +659,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
return continueOnFail(node);
},
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
return additionalData.executeWorkflow(workflowInfo, additionalData, inputData);
@@ -752,7 +757,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
},
evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => {
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 {
return NodeHelpers.getContext(runExecutionData, type, node);

View File

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

View File

@@ -15,8 +15,8 @@ import {
ITaskDataConnections,
IWaitingForExecution,
IWorkflowExecuteAdditionalData,
WorkflowExecuteMode,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
NodeExecuteFunctions,
@@ -84,7 +84,7 @@ export class WorkflowExecute {
],
],
},
}
},
];
this.runExecutionData = {
@@ -137,8 +137,8 @@ export class WorkflowExecute {
// If it has no incoming data add the default empty data
incomingData.push([
{
json: {}
}
json: {},
},
]);
} else {
// Get the data of the incoming connections
@@ -156,7 +156,7 @@ export class WorkflowExecute {
node: workflow.getNode(startNode) as INode,
data: {
main: incomingData,
}
},
};
nodeExecutionStack.push(executeData);
@@ -252,7 +252,7 @@ export class WorkflowExecute {
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
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
main: []
main: [],
};
for (let i = 0; i < workflow.connectionsByDestinationNode[connectionData.node]['main'].length; i++) {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main.push(null);
@@ -282,7 +282,7 @@ export class WorkflowExecute {
// So add it to the execution stack
this.runExecutionData.executionData!.nodeExecutionStack.push({
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
@@ -426,15 +426,15 @@ export class WorkflowExecute {
this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
}
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
main: connectionDataArray
main: connectionDataArray,
};
} else {
// All data is there so add it directly to stack
this.runExecutionData.executionData!.nodeExecutionStack.push({
node: workflow.nodes[connectionData.node],
data: {
main: connectionDataArray
}
main: connectionDataArray,
},
});
}
}
@@ -608,7 +608,7 @@ export class WorkflowExecute {
nodeSuccessData[0] = [
{
json: {},
}
},
];
}
}
@@ -622,6 +622,8 @@ export class WorkflowExecute {
break;
} catch (error) {
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
executionError = {
message: error.message,
stack: error.stack,
@@ -637,7 +639,7 @@ export class WorkflowExecute {
}
taskData = {
startTime,
executionTime: (new Date().getTime()) - startTime
executionTime: (new Date().getTime()) - startTime,
};
if (executionError !== undefined) {
@@ -667,7 +669,7 @@ export class WorkflowExecute {
// Node executed successfully. So add data and go on.
taskData.data = ({
'main': nodeSuccessData
'main': nodeSuccessData,
} as ITaskDataConnections);
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}"`));
}
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('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 password = 'password';
// const nodeType = 'base.noOp';
const newData = 1234;
const key = 'key1';
const password = 'password';
// const nodeType = 'base.noOp';
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 password = 'password';
const key = 'key2';
const password = 'password';
// Saved under "key1"
const initialData = 4321;
const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ=';
// Saved under "key1"
const initialData = 4321;
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
credentials.setDataKey(key, newData, password);
expect(credentials.getDataKey(key, password)).toEqual(newData);
// Set and read new data
credentials.setDataKey(key, newData, password);
expect(credentials.getDataKey(key, password)).toEqual(newData);
// Read the data which got provided encrypted on init
expect(credentials.getDataKey('key1', password)).toEqual(initialData);
});
// Read the data which got provided encrypted on init
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 = [
{
nodeType: 'base.noOp',
user: 'userName',
date: new Date(),
}
];
const nodeAccess = [
{
nodeType: 'base.noOp',
user: 'userName',
date: new Date(),
},
];
const credentials = new Credentials('testName', 'testType', nodeAccess);
const credentials = new Credentials('testName', 'testType', nodeAccess);
const key = 'key1';
const password = 'password';
const nodeType = 'base.noOp';
const newData = 1234;
const key = 'key1';
const password = 'password';
const nodeType = 'base.noOp';
const newData = 1234;
credentials.setDataKey(key, newData, password);
credentials.setDataKey(key, newData, password);
// Should be able to read with nodeType which has access
expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData);
// Should be able to read with nodeType which has access
expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData);
// Should not be able to read with nodeType which does NOT have access
// expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error);
try {
credentials.getDataKey(key, password, 'base.otherNode');
expect(true).toBe(false);
} catch (e) {
expect(e.message).toBe('The node of type "base.otherNode" does not have access to credentials "testName" of type "testType".');
}
// Should not be able to read with nodeType which does NOT have access
// expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error);
try {
credentials.getDataKey(key, password, 'base.otherNode');
expect(true).toBe(false);
} catch (e) {
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
const dbData = credentials.getDataToSave();
expect(dbData.name).toEqual('testName');
expect(dbData.type).toEqual('testType');
expect(dbData.nodesAccess).toEqual(nodeAccess);
// 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));
});
});
// Get the data which will be saved in database
const dbData = credentials.getDataToSave();
expect(dbData.name).toEqual('testName');
expect(dbData.type).toEqual('testType');
expect(dbData.nodesAccess).toEqual(nodeAccess);
// 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));
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.55.0",
"version": "0.60.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@@ -20,12 +20,11 @@
"serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vue-cli-service serve",
"test": "npm run test:unit",
"tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"test:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"uuid": "^8.1.0"
},
"dependencies": {},
"devDependencies": {
"@beyonk/google-fonts-webpack-plugin": "^1.2.3",
"@fortawesome/fontawesome-svg-core": "^1.2.19",
@@ -34,16 +33,16 @@
"@types/dateformat": "^3.0.0",
"@types/express": "^4.17.6",
"@types/file-saver": "^2.0.1",
"@types/jest": "^25.2.1",
"@types/jest": "^26.0.13",
"@types/lodash.get": "^4.4.6",
"@types/lodash.set": "^4.3.6",
"@types/node": "^14.0.27",
"@types/node": "14.0.27",
"@types/quill": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^2.13.0",
"@typescript-eslint/parser": "^2.13.0",
"@vue/cli-plugin-babel": "^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-service": "^3.11.0",
"@vue/eslint-config-standard": "^5.0.1",
@@ -66,16 +65,18 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.39.0",
"n8n-workflow": "~0.42.0",
"node-sass": "^4.12.0",
"normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1",
"quill": "^2.0.0-dev.3",
"quill-autoformat": "^0.1.1",
"sass-loader": "^8.0.0",
"string-template-parser": "^1.2.6",
"ts-jest": "^25.4.0",
"ts-jest": "^26.3.0",
"tslint": "^6.1.2",
"typescript": "~3.7.4",
"typescript": "~3.9.7",
"uuid": "^8.1.0",
"vue": "^2.6.9",
"vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0",
"vue-json-tree": "^0.4.1",
@@ -83,6 +84,7 @@
"vue-router": "^3.0.6",
"vue-template-compiler": "^2.5.17",
"vue-typed-mixins": "^0.2.0",
"vue2-touch-events": "^2.3.2",
"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
getSettings(): Promise<IN8nUISettings>;
getNodeTypes(): Promise<INodeTypeDescription[]>;
getNodesInformation(nodeList: string[]): Promise<INodeTypeDescription[]>;
getNodeParameterOptions(nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
removeTestWebhook(workflowId: string): Promise<boolean>;
runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>;
@@ -399,6 +400,10 @@ export interface IN8nUISettings {
timezone: string;
executionTimeout: number;
maxExecutionTimeout: number;
oauthCallbackUrls: {
oauth1: string;
oauth2: string;
};
urlBaseWebhook: string;
versionCli: string;
}

View File

@@ -4,7 +4,7 @@
<div name="title" class="title-container" slot="title">
<div class="title-left">{{title}}</div>
<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">
<title>Node Documentation</title>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
@@ -20,7 +20,7 @@
</g>
</g>
</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>
@@ -109,27 +109,19 @@ export default mixins(
}
}
},
documentationUrl (): string {
documentationUrl (): string | undefined {
let credentialTypeName = '';
if (this.editCredentials) {
const credentialType = this.$store.getters.credentialType(this.editCredentials.type);
if (credentialType.documentationUrl === undefined) {
return credentialType.name;
} else {
return `${credentialType.documentationUrl}`;
}
credentialTypeName = this.editCredentials.type as string;
} else {
if (this.credentialType) {
const credentialType = this.$store.getters.credentialType(this.credentialType);
if (credentialType.documentationUrl === undefined) {
return credentialType.name;
} else {
return `${credentialType.documentationUrl}`;
}
} else {
return '';
}
credentialTypeName = this.credentialType as string;
}
const credentialType = this.$store.getters.credentialType(credentialTypeName);
if (credentialType.documentationUrl !== undefined) {
return `${credentialType.documentationUrl}`;
}
return undefined;
},
node (): INodeUi {
return this.$store.getters.activeNode;

View File

@@ -235,7 +235,7 @@ export default mixins(
oAuthCallbackUrl (): string {
const types = this.parentTypes(this.credentialTypeData.name);
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 {
for (const property of this.credentialProperties) {
@@ -404,10 +404,11 @@ export default mixins(
message: 'Connected successfully!',
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);

View File

@@ -1,6 +1,6 @@
<template>
<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">
<el-tooltip placement="top" effect="light">
<div slot="content" v-html="nodeIssues"></div>
@@ -13,19 +13,19 @@
<font-awesome-icon icon="sync-alt" spin />
</div>
<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" />
</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" />
</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" />
</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" />
</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" />
</div>
</div>
@@ -110,6 +110,10 @@ export default mixins(nodeBase, workflowHelpers).extend({
classes.push('is-touch-device');
}
if (this.isTouchActive) {
classes.push('touch-active');
}
return classes;
},
nodeIssues (): string {
@@ -134,7 +138,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
}
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) {
@@ -174,7 +178,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
},
data () {
return {
isTouchDevice: 'ontouchstart' in window || navigator.msMaxTouchPoints,
isTouchActive: false,
};
},
methods: {
@@ -199,6 +203,14 @@ export default mixins(nodeBase, workflowHelpers).extend({
setNodeActive () {
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 {
.node-execute {
display: initial;

View File

@@ -1,5 +1,5 @@
<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">
<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'" />
@@ -17,6 +17,7 @@ import Vue from 'vue';
interface NodeIconData {
type: string;
path: string;
fileExtension?: string;
}
export default Vue.extend({
@@ -41,6 +42,12 @@ export default Vue.extend({
'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 {
if (this.nodeType === null) {
return null;
@@ -51,13 +58,14 @@ export default Vue.extend({
if (this.nodeType.icon) {
let type, path;
[type, path] = this.nodeType.icon.split(':');
const returnData = {
const returnData: NodeIconData = {
type,
path,
};
if (type === 'file') {
returnData.path = restUrl + '/node-icon/' + this.nodeType.name;
returnData.fileExtension = path.split('.').slice(-1).join();
}
return returnData;
@@ -83,6 +91,10 @@ export default Vue.extend({
font-weight: bold;
font-size: 20px;
&.full .icon {
margin: 0.24em;
}
.node-icon-placeholder {
text-align: center;
}

View File

@@ -248,7 +248,7 @@ export default mixins(
type: 'boolean',
default: false,
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[],

View File

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

View File

@@ -250,7 +250,7 @@ export default mixins(
* @returns
* @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)) {
// No data found for node
return null;
@@ -291,9 +291,12 @@ export default mixins(
// Get json data
if (outputData.hasOwnProperty('json')) {
const jsonPropertyPrefix = useShort === true ? '$json' : `$node["${nodeName}"].json`;
const jsonDataOptions: IVariableSelectorOption[] = [];
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) {
@@ -308,6 +311,9 @@ export default mixins(
// Get binary data
if (outputData.hasOwnProperty('binary')) {
const binaryPropertyPrefix = useShort === true ? '$binary' : `$node["${nodeName}"].binary`;
const binaryData = [];
let binaryPropertyData = [];
@@ -326,7 +332,7 @@ export default mixins(
binaryPropertyData.push(
{
name: propertyName,
key: `$node["${nodeName}"].binary.${dataPropertyName}.${propertyName}`,
key: `${binaryPropertyPrefix}.${dataPropertyName}.${propertyName}`,
value: outputData.binary![dataPropertyName][propertyName],
},
);
@@ -336,7 +342,7 @@ export default mixins(
binaryData.push(
{
name: dataPropertyName,
key: `$node["${nodeName}"].binary.${dataPropertyName}`,
key: `${binaryPropertyPrefix}.${dataPropertyName}`,
options: this.sortOptions(binaryPropertyData),
allowParentSelect: true,
},
@@ -347,7 +353,7 @@ export default mixins(
returnData.push(
{
name: 'Binary',
key: `$node["${nodeName}"].binary`,
key: binaryPropertyPrefix,
options: this.sortOptions(binaryData),
allowParentSelect: true,
},
@@ -474,7 +480,7 @@ export default mixins(
// (example "IF" node. If node is connected to "true" or to "false" output)
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 (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 { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';
export const mouseSelect = mixins(nodeIndex).extend({
export const mouseSelect = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
data () {
return {
selectActive: false,
selectBox: document.createElement('span'),
};
},
computed: {
isMacOs (): boolean {
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
},
},
mounted () {
this.createSelectBox();
},
@@ -34,6 +33,9 @@ export const mouseSelect = mixins(nodeIndex).extend({
this.$el.appendChild(this.selectBox);
},
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
if (this.isTouchDevice === true) {
return true;
}
if (this.isMacOs) {
return e.metaKey;
}
@@ -125,6 +127,13 @@ export const mouseSelect = mixins(nodeIndex).extend({
},
mouseUpMouseSelect (e: MouseEvent) {
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.
// Else normal node dragging will not work.
return;

View File

@@ -1,41 +1,43 @@
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';
export const moveNodeWorkflow = mixins(nodeIndex).extend({
export const moveNodeWorkflow = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
data () {
return {
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: {
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
if (this.isMacOs) {
return e.metaKey;
}
return e.ctrlKey;
getMousePosition(e: MouseEvent | TouchEvent) {
// @ts-ignore
const x = e.pageX !== undefined ? e.pageX : (e.touches && e.touches[0] && e.touches[0].pageX ? e.touches[0].pageX : 0);
// @ts-ignore
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) {
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
const nodeViewOffsetPositionX = offsetPosition[0] + (e.pageX - this.moveLastPosition[0]);
const nodeViewOffsetPositionY = offsetPosition[1] + (e.pageY - this.moveLastPosition[1]);
const position = this.getMousePosition(e);
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});
// Update the last position
this.moveLastPosition[0] = e.pageX;
this.moveLastPosition[1] = e.pageY;
this.moveLastPosition[0] = position.x;
this.moveLastPosition[1] = position.y;
},
mouseDownMoveWorkflow (e: MouseEvent) {
if (this.isCtrlKeyPressed(e) === false) {
@@ -51,8 +53,10 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({
this.$store.commit('setNodeViewMoveInProgress', true);
this.moveLastPosition[0] = e.pageX;
this.moveLastPosition[1] = e.pageY;
const position = this.getMousePosition(e);
this.moveLastPosition[0] = position.x;
this.moveLastPosition[1] = position.y;
// @ts-ignore
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
},
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) {
// Mouse button is not pressed anymore so stop selection mode
// Happens normally when mouse leave the view pressed and then
@@ -84,9 +97,10 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({
this.moveWorkflow(e);
},
wheelMoveWorkflow (e: WheelEvent) {
const normalized = normalizeWheel(e);
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
const nodeViewOffsetPositionX = offsetPosition[0] - e.deltaX;
const nodeViewOffsetPositionY = offsetPosition[1] - e.deltaY;
const nodeViewOffsetPositionX = offsetPosition[0] - normalized.pixelX;
const nodeViewOffsetPositionY = offsetPosition[1] - normalized.pixelY;
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 { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';
import { NODE_NAME_PREFIX } from '@/constants';
export const nodeBase = mixins(nodeIndex).extend({
export const nodeBase = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
mounted () {
// Initialize the node
if (this.data !== null) {
this.__addNode(this.data);
}
},
data () {
return {
};
},
computed: {
data (): INodeUi {
return this.$store.getters.nodeByName(this.name);
@@ -26,9 +26,6 @@ export const nodeBase = mixins(nodeIndex).extend({
}
return false;
},
isMacOs (): boolean {
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
},
nodeName (): string {
return NODE_NAME_PREFIX + this.nodeIndex;
},
@@ -336,26 +333,27 @@ export const nodeBase = mixins(nodeIndex).extend({
});
},
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
if (this.isMacOs) {
return e.metaKey;
}
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');
touchEnd(e: MouseEvent) {
if (this.isTouchDevice) {
if (this.$store.getters.isActionActive('dragActive')) {
this.$store.commit('removeActiveAction', 'dragActive');
}
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 {
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`);
},
getNodesInformation: (nodeList: string[]): Promise<INodeTypeDescription[]> => {
return self.restApi().makeRestApiRequest('POST', `/node-types`, {nodeNames: nodeList});
},
// Returns all the parameter options from the server
getNodeParameterOptions: (nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => {
const sendData = {

View File

@@ -360,7 +360,7 @@ export const workflowHelpers = mixins(
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.

View File

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

View File

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

View File

@@ -3,11 +3,15 @@
<div
class="node-view-wrapper"
:class="workflowClasses"
@touchstart="mouseDown"
@touchend="mouseUp"
@touchmove="mouseMoveNodeWorkflow"
@mousedown="mouseDown"
v-touch:tap="touchTap"
@mouseup="mouseUp"
@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">
<node
v-for="nodeData in nodes"
@@ -356,14 +360,20 @@ export default mixins(
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
const position = this.getMousePosition(e);
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
this.lastClickPosition[0] = e.pageX - offsetPosition[0];
this.lastClickPosition[1] = e.pageY - offsetPosition[1];
this.lastClickPosition[0] = position.x - offsetPosition[0];
this.lastClickPosition[1] = position.y - offsetPosition[1];
this.mouseDownMouseSelect(e);
this.mouseDownMoveWorkflow(e);
this.mouseDownMouseSelect(e as MouseEvent);
this.mouseDownMoveWorkflow(e as MouseEvent);
// Hide the node-creator
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
// newNodeData.position = [activeNode.position[0], activeNode.position[1] + 60];
newNodeData.position = this.getNewNodePosition(
[lastSelectedNode.position[0] + 150, lastSelectedNode.position[1]],
[lastSelectedNode.position[0] + 200, lastSelectedNode.position[1]],
[100, 0],
);
} else {
@@ -1456,6 +1466,11 @@ export default mixins(
[0, 150],
);
if (newNodeData.webhookId) {
// Make sure that the node gets a new unique webhook-ID
newNodeData.webhookId = uuidv4();
}
await this.addNodes([newNodeData]);
// Automatically deselect all nodes and select the current one and also active
@@ -1593,6 +1608,11 @@ export default mixins(
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
let nodeType: INodeTypeDescription | null;
let foundNodeIssues: INodeIssues | null;
@@ -1703,6 +1723,9 @@ export default mixins(
let oldName: string;
let newName: string;
const createNodes: INode[] = [];
await this.loadNodesProperties(data.nodes.map(node => node.type));
data.nodes.forEach(node => {
if (nodeTypesCount[node.type] !== undefined) {
if (nodeTypesCount[node.type].exist >= nodeTypesCount[node.type].max) {
@@ -1745,6 +1768,10 @@ export default mixins(
for (type of Object.keys(currentConnections[sourceNode])) {
connection[type] = [];
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 = [];
for (connectionIndex = 0; connectionIndex < currentConnections[sourceNode][type][sourceIndex].length; connectionIndex++) {
const nodeConnection: NodeInputConnections = [];
@@ -1908,6 +1935,7 @@ export default mixins(
this.$store.commit('setExecutionTimeout', settings.executionTimeout);
this.$store.commit('setMaxExecutionTimeout', settings.maxExecutionTimeout);
this.$store.commit('setVersionCli', settings.versionCli);
this.$store.commit('setOauthCallbackUrls', settings.oauthCallbackUrls);
},
async loadNodeTypes (): Promise<void> {
const nodeTypes = await this.restApi().getNodeTypes();
@@ -1921,6 +1949,17 @@ export default mixins(
const credentials = await this.restApi().getAllCredentials();
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 () {

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "0.10.0",
"version": "0.11.0",
"description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@@ -24,6 +24,7 @@
"postpack": "rm -f oclif.manifest.json",
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
"tslint": "tslint -p tsconfig.json -c tslint.json",
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"watch": "tsc --watch"
},
"bin": {
@@ -54,15 +55,16 @@
"@oclif/command": "^1.5.18",
"@oclif/errors": "^1.2.2",
"@types/express": "^4.17.6",
"@types/node": "^14.0.27",
"@types/node": "14.0.27",
"change-case": "^4.1.1",
"copyfiles": "^2.1.1",
"inquirer": "^7.0.1",
"n8n-core": "^0.44.0",
"n8n-workflow": "^0.39.0",
"n8n-core": "^0.48.0",
"n8n-workflow": "^0.42.0",
"oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0",
"request": "^2.88.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
const options: ReplaceInFileConfig = {
files: [
destinationFilePath
destinationFilePath,
],
from: [],
to: [],

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import {
NodePropertyTypes,
} from 'n8n-workflow';
export class AsanaApi implements ICredentialType {
name = 'asanaApi';
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 {
name = 'bitlyOAuth2Api';
displayName = 'Bitly OAuth2 API';
displayName = 'Bitly OAuth2 API';
documentationUrl = 'bitly';
extends = [
extends = [
'oAuth2Api',
];
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 {
name = 'contentfulApi';
displayName = 'Contenful API';
documentationUrl = 'contentful';
properties = [
{
displayName: 'Space ID',

View File

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

View File

@@ -13,7 +13,7 @@ export class DisqusApi implements ICredentialType {
name: 'accessToken',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Visit your account details page, and grab the Access Token. See <a href="https://disqus.com/api/docs/auth/">Disqus auth</a>.'
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',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Visit your account details page, and grab the Access Token. See <a href="https://devdocs.drift.com/docs/quick-start">Drift auth</a>.'
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,
default: 'https://api.dropboxapi.com/oauth2/token',
required: true,
},
{
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,

View File

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

View File

@@ -21,7 +21,7 @@ export class FreshdeskApi implements ICredentialType {
type: 'string' as NodePropertyTypes,
placeholder: '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,
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'localhost'
placeholder: 'localhost',
},
{
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',
name: 'server',
type: 'string' as NodePropertyTypes,
default: 'https://gitlab.com'
default: 'https://gitlab.com',
},
{
displayName: 'Access Token',

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ export class GoogleTasksOAuth2Api implements ICredentialType {
displayName: 'Scope',
name: 'scope',
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',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Visit your account details page, and grab the Account ID. See <a href="https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/">Harvest Personal Access Tokens</a>.'
description: 'Visit your account details page, and grab the Account ID. See <a href="https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/">Harvest Personal Access Tokens</a>.',
},
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Visit your account details page, and grab the Access Token. See <a href="https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/">Harvest Personal Access Tokens</a>.'
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 {
name = 'hubspotDeveloperApi';
displayName = 'Hubspot API';
displayName = 'Hubspot Developer API';
documentationUrl = 'hubspot';
properties = [
{

View File

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

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