mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge branch 'master' into expand-zoho-node
This commit is contained in:
commit
68bfd3deee
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x, 14.x]
|
node-version: [14.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
|
@ -13,7 +13,9 @@
|
||||||
"start:windows": "cd packages/cli/bin && n8n",
|
"start:windows": "cd packages/cli/bin && n8n",
|
||||||
"test": "lerna run test",
|
"test": "lerna run test",
|
||||||
"tslint": "lerna exec npm run tslint",
|
"tslint": "lerna exec npm run tslint",
|
||||||
"watch": "lerna run --parallel watch"
|
"watch": "lerna run --parallel watch",
|
||||||
|
"webhook": "./packages/cli/bin/n8n webhook",
|
||||||
|
"worker": "./packages/cli/bin/n8n worker"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"lerna": "^3.13.1",
|
"lerna": "^3.13.1",
|
||||||
|
|
|
@ -2,6 +2,26 @@
|
||||||
|
|
||||||
This list shows all the versions which include breaking changes and how to upgrade.
|
This list shows all the versions which include breaking changes and how to upgrade.
|
||||||
|
|
||||||
|
## 0.118.0
|
||||||
|
|
||||||
|
### What changed?
|
||||||
|
The minimum Node.js version required for n8n is now v14.
|
||||||
|
|
||||||
|
### When is action necessary?
|
||||||
|
If you're using n8n via npm or PM2 or if you're contributing to n8n.
|
||||||
|
|
||||||
|
### How to upgrade:
|
||||||
|
Update the Node.js version to v14 or above.
|
||||||
|
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
### What changed?
|
||||||
|
In the Postgres, CrateDB, QuestDB and TimescaleDB nodes the `Execute Query` operation returns the result from all queries executed instead of just one of the results.
|
||||||
|
|
||||||
|
### When is action necessary?
|
||||||
|
|
||||||
|
If you use any of the above mentioned nodes with the `Execute Query` operation and the result is relevant to you, you are encouraged to revisit your logic. The node output may now contain more information than before. This change was made so that the behavior is more consistent across n8n where input with multiple rows should yield results acccording all input data instead of only one. Please note: n8n was already running multiple queries based on input. Only the output was changed.
|
||||||
|
|
||||||
## 0.117.0
|
## 0.117.0
|
||||||
|
|
||||||
### What changed?
|
### What changed?
|
||||||
|
|
|
@ -24,8 +24,8 @@ if (process.argv.length === 2) {
|
||||||
|
|
||||||
var nodeVersion = process.versions.node.split('.');
|
var nodeVersion = process.versions.node.split('.');
|
||||||
|
|
||||||
if (parseInt(nodeVersion[0], 10) < 12 || parseInt(nodeVersion[0], 10) === 12 && parseInt(nodeVersion[1], 10) < 9) {
|
if (parseInt(nodeVersion[0], 10) < 14) {
|
||||||
console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 12.9 or later!\n`);
|
console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 14 or later!\n`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import {
|
||||||
CredentialTypes,
|
CredentialTypes,
|
||||||
Db,
|
Db,
|
||||||
ExternalHooks,
|
ExternalHooks,
|
||||||
GenericHelpers,
|
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
LoadNodesAndCredentials,
|
LoadNodesAndCredentials,
|
||||||
|
@ -23,6 +22,13 @@ import {
|
||||||
WorkflowRunner,
|
WorkflowRunner,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLogger,
|
||||||
|
} from '../src/Logger';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoggerProxy,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
export class Execute extends Command {
|
export class Execute extends Command {
|
||||||
static description = '\nExecutes a given workflow';
|
static description = '\nExecutes a given workflow';
|
||||||
|
@ -44,6 +50,9 @@ export class Execute extends Command {
|
||||||
|
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
const { flags } = this.parse(Execute);
|
const { flags } = this.parse(Execute);
|
||||||
|
|
||||||
// Start directly with the init of the database to improve startup time
|
// Start directly with the init of the database to improve startup time
|
||||||
|
@ -54,12 +63,12 @@ export class Execute extends Command {
|
||||||
const loadNodesAndCredentialsPromise = loadNodesAndCredentials.init();
|
const loadNodesAndCredentialsPromise = loadNodesAndCredentials.init();
|
||||||
|
|
||||||
if (!flags.id && !flags.file) {
|
if (!flags.id && !flags.file) {
|
||||||
GenericHelpers.logOutput(`Either option "--id" or "--file" have to be set!`);
|
console.info(`Either option "--id" or "--file" have to be set!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.id && flags.file) {
|
if (flags.id && flags.file) {
|
||||||
GenericHelpers.logOutput(`Either "id" or "file" can be set never both!`);
|
console.info(`Either "id" or "file" can be set never both!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +80,7 @@ export class Execute extends Command {
|
||||||
workflowData = JSON.parse(await fs.readFile(flags.file, 'utf8'));
|
workflowData = JSON.parse(await fs.readFile(flags.file, 'utf8'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
GenericHelpers.logOutput(`The file "${flags.file}" could not be found.`);
|
console.info(`The file "${flags.file}" could not be found.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +90,7 @@ export class Execute extends Command {
|
||||||
// Do a basic check if the data in the file looks right
|
// Do a basic check if the data in the file looks right
|
||||||
// TODO: Later check with the help of TypeScript data if it is valid or not
|
// TODO: Later check with the help of TypeScript data if it is valid or not
|
||||||
if (workflowData === undefined || workflowData.nodes === undefined || workflowData.connections === undefined) {
|
if (workflowData === undefined || workflowData.nodes === undefined || workflowData.connections === undefined) {
|
||||||
GenericHelpers.logOutput(`The file "${flags.file}" does not contain valid workflow data.`);
|
console.info(`The file "${flags.file}" does not contain valid workflow data.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
workflowId = workflowData.id!.toString();
|
workflowId = workflowData.id!.toString();
|
||||||
|
@ -95,8 +104,8 @@ export class Execute extends Command {
|
||||||
workflowId = flags.id;
|
workflowId = flags.id;
|
||||||
workflowData = await Db.collections!.Workflow!.findOne(workflowId);
|
workflowData = await Db.collections!.Workflow!.findOne(workflowId);
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
GenericHelpers.logOutput(`The workflow with the id "${workflowId}" does not exist.`);
|
console.info(`The workflow with the id "${workflowId}" does not exist.`);
|
||||||
return;
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +147,7 @@ export class Execute extends Command {
|
||||||
if (startNode === undefined) {
|
if (startNode === undefined) {
|
||||||
// If the workflow does not contain a start-node we can not know what
|
// If the workflow does not contain a start-node we can not know what
|
||||||
// should be executed and with which data to start.
|
// should be executed and with which data to start.
|
||||||
GenericHelpers.logOutput(`The workflow does not contain a "Start" node. So it can not be executed.`);
|
console.info(`The workflow does not contain a "Start" node. So it can not be executed.`);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,9 +172,10 @@ export class Execute extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.data.resultData.error) {
|
if (data.data.resultData.error) {
|
||||||
this.log('Execution was NOT successfull:');
|
console.info('Execution was NOT successful. See log message for details.');
|
||||||
this.log('====================================');
|
logger.info('Execution error:');
|
||||||
this.log(JSON.stringify(data, null, 2));
|
logger.info('====================================');
|
||||||
|
logger.info(JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
const { error } = data.data.resultData;
|
const { error } = data.data.resultData;
|
||||||
throw {
|
throw {
|
||||||
|
@ -174,14 +184,15 @@ export class Execute extends Command {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log('Execution was successfull:');
|
console.info('Execution was successful:');
|
||||||
this.log('====================================');
|
console.info('====================================');
|
||||||
this.log(JSON.stringify(data, null, 2));
|
console.info(JSON.stringify(data, null, 2));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('\nGOT ERROR');
|
console.error('Error executing workflow. See log messages for details.');
|
||||||
console.log('====================================');
|
logger.error('\nExecution error:');
|
||||||
console.error(e.message);
|
logger.info('====================================');
|
||||||
console.error(e.stack);
|
logger.error(e.message);
|
||||||
|
logger.error(e.stack);
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,17 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Db,
|
Db,
|
||||||
GenericHelpers,
|
|
||||||
ICredentialsDecryptedDb,
|
ICredentialsDecryptedDb,
|
||||||
} from '../../src';
|
} from '../../src';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLogger,
|
||||||
|
} from '../../src/Logger';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoggerProxy,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
@ -59,8 +66,11 @@ export class ExportCredentialsCommand extends Command {
|
||||||
};
|
};
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
const { flags } = this.parse(ExportCredentialsCommand);
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
const { flags } = this.parse(ExportCredentialsCommand);
|
||||||
|
|
||||||
if (flags.backup) {
|
if (flags.backup) {
|
||||||
flags.all = true;
|
flags.all = true;
|
||||||
flags.pretty = true;
|
flags.pretty = true;
|
||||||
|
@ -68,41 +78,42 @@ export class ExportCredentialsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!flags.all && !flags.id) {
|
if (!flags.all && !flags.id) {
|
||||||
GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`);
|
console.info(`Either option "--all" or "--id" have to be set!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.all && flags.id) {
|
if (flags.all && flags.id) {
|
||||||
GenericHelpers.logOutput(`You should either use "--all" or "--id" but never both!`);
|
console.info(`You should either use "--all" or "--id" but never both!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
try {
|
try {
|
||||||
if (!flags.output) {
|
if (!flags.output) {
|
||||||
GenericHelpers.logOutput(`You must inform an output directory via --output when using --separate`);
|
console.info(`You must inform an output directory via --output when using --separate`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(flags.output)) {
|
if (fs.existsSync(flags.output)) {
|
||||||
if (!fs.lstatSync(flags.output).isDirectory()) {
|
if (!fs.lstatSync(flags.output).isDirectory()) {
|
||||||
GenericHelpers.logOutput(`The paramenter --output must be a directory`);
|
console.info(`The paramenter --output must be a directory`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fs.mkdirSync(flags.output, { recursive: true });
|
fs.mkdirSync(flags.output, { recursive: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('\nFILESYSTEM ERROR');
|
console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.');
|
||||||
console.log('====================================');
|
logger.error('\nFILESYSTEM ERROR');
|
||||||
console.error(e.message);
|
logger.info('====================================');
|
||||||
console.error(e.stack);
|
logger.error(e.message);
|
||||||
|
logger.error(e.stack);
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
} else if (flags.output) {
|
} else if (flags.output) {
|
||||||
if (fs.existsSync(flags.output)) {
|
if (fs.existsSync(flags.output)) {
|
||||||
if (fs.lstatSync(flags.output).isDirectory()) {
|
if (fs.lstatSync(flags.output).isDirectory()) {
|
||||||
GenericHelpers.logOutput(`The paramenter --output must be a writeble file`);
|
console.info(`The paramenter --output must be a writeble file`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,18 +154,21 @@ export class ExportCredentialsCommand extends Command {
|
||||||
const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + credentials[i].id + '.json';
|
const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + credentials[i].id + '.json';
|
||||||
fs.writeFileSync(filename, fileContents);
|
fs.writeFileSync(filename, fileContents);
|
||||||
}
|
}
|
||||||
console.log('Successfully exported', i, 'credentials.');
|
console.info(`Successfully exported ${i} credentials.`);
|
||||||
} else {
|
} else {
|
||||||
const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined);
|
const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined);
|
||||||
if (flags.output) {
|
if (flags.output) {
|
||||||
fs.writeFileSync(flags.output!, fileContents);
|
fs.writeFileSync(flags.output!, fileContents);
|
||||||
console.log('Successfully exported', credentials.length, 'credentials.');
|
console.info(`Successfully exported ${credentials.length} credentials.`);
|
||||||
} else {
|
} else {
|
||||||
console.log(fileContents);
|
console.info(fileContents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Force exit as process won't exit using MySQL or Postgres.
|
||||||
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error(error.message);
|
console.error('Error exporting credentials. See log messages for details.');
|
||||||
|
logger.error(error.message);
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,16 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Db,
|
Db,
|
||||||
GenericHelpers,
|
|
||||||
} from '../../src';
|
} from '../../src';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLogger,
|
||||||
|
} from '../../src/Logger';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoggerProxy,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
@ -49,6 +56,9 @@ export class ExportWorkflowsCommand extends Command {
|
||||||
};
|
};
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
const { flags } = this.parse(ExportWorkflowsCommand);
|
const { flags } = this.parse(ExportWorkflowsCommand);
|
||||||
|
|
||||||
if (flags.backup) {
|
if (flags.backup) {
|
||||||
|
@ -58,41 +68,42 @@ export class ExportWorkflowsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!flags.all && !flags.id) {
|
if (!flags.all && !flags.id) {
|
||||||
GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`);
|
console.info(`Either option "--all" or "--id" have to be set!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.all && flags.id) {
|
if (flags.all && flags.id) {
|
||||||
GenericHelpers.logOutput(`You should either use "--all" or "--id" but never both!`);
|
console.info(`You should either use "--all" or "--id" but never both!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
try {
|
try {
|
||||||
if (!flags.output) {
|
if (!flags.output) {
|
||||||
GenericHelpers.logOutput(`You must inform an output directory via --output when using --separate`);
|
console.info(`You must inform an output directory via --output when using --separate`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(flags.output)) {
|
if (fs.existsSync(flags.output)) {
|
||||||
if (!fs.lstatSync(flags.output).isDirectory()) {
|
if (!fs.lstatSync(flags.output).isDirectory()) {
|
||||||
GenericHelpers.logOutput(`The paramenter --output must be a directory`);
|
console.info(`The paramenter --output must be a directory`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fs.mkdirSync(flags.output, { recursive: true });
|
fs.mkdirSync(flags.output, { recursive: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('\nFILESYSTEM ERROR');
|
console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.');
|
||||||
console.log('====================================');
|
logger.error('\nFILESYSTEM ERROR');
|
||||||
console.error(e.message);
|
logger.info('====================================');
|
||||||
console.error(e.stack);
|
logger.error(e.message);
|
||||||
|
logger.error(e.stack);
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
} else if (flags.output) {
|
} else if (flags.output) {
|
||||||
if (fs.existsSync(flags.output)) {
|
if (fs.existsSync(flags.output)) {
|
||||||
if (fs.lstatSync(flags.output).isDirectory()) {
|
if (fs.lstatSync(flags.output).isDirectory()) {
|
||||||
GenericHelpers.logOutput(`The paramenter --output must be a writeble file`);
|
console.info(`The paramenter --output must be a writeble file`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,18 +130,21 @@ export class ExportWorkflowsCommand extends Command {
|
||||||
const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + workflows[i].id + '.json';
|
const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + workflows[i].id + '.json';
|
||||||
fs.writeFileSync(filename, fileContents);
|
fs.writeFileSync(filename, fileContents);
|
||||||
}
|
}
|
||||||
console.log('Successfully exported', i, 'workflows.');
|
console.info(`Successfully exported ${i} workflows.`);
|
||||||
} else {
|
} else {
|
||||||
const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined);
|
const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined);
|
||||||
if (flags.output) {
|
if (flags.output) {
|
||||||
fs.writeFileSync(flags.output!, fileContents);
|
fs.writeFileSync(flags.output!, fileContents);
|
||||||
console.log('Successfully exported', workflows.length, workflows.length === 1 ? 'workflow.' : 'workflows.');
|
console.info(`Successfully exported ${workflows.length} ${workflows.length === 1 ? 'workflow.' : 'workflows.'}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(fileContents);
|
console.info(fileContents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Force exit as process won't exit using MySQL or Postgres.
|
||||||
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error(error.message);
|
console.error('Error exporting workflows. See log messages for details.');
|
||||||
|
logger.error(error.message);
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,16 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Db,
|
Db,
|
||||||
GenericHelpers,
|
|
||||||
} from '../../src';
|
} from '../../src';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLogger,
|
||||||
|
} from '../../src/Logger';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoggerProxy,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as glob from 'glob-promise';
|
import * as glob from 'glob-promise';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
@ -37,17 +44,20 @@ export class ImportCredentialsCommand extends Command {
|
||||||
};
|
};
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
const { flags } = this.parse(ImportCredentialsCommand);
|
const { flags } = this.parse(ImportCredentialsCommand);
|
||||||
|
|
||||||
if (!flags.input) {
|
if (!flags.input) {
|
||||||
GenericHelpers.logOutput(`An input file or directory with --input must be provided`);
|
console.info(`An input file or directory with --input must be provided`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
if (fs.existsSync(flags.input)) {
|
if (fs.existsSync(flags.input)) {
|
||||||
if (!fs.lstatSync(flags.input).isDirectory()) {
|
if (!fs.lstatSync(flags.input).isDirectory()) {
|
||||||
GenericHelpers.logOutput(`The paramenter --input must be a directory`);
|
console.info(`The paramenter --input must be a directory`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,9 +99,11 @@ export class ImportCredentialsCommand extends Command {
|
||||||
await Db.collections.Credentials!.save(fileContents[i]);
|
await Db.collections.Credentials!.save(fileContents[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Successfully imported', i, 'credentials.');
|
console.info(`Successfully imported ${i} ${i === 1 ? 'credential.' : 'credentials.'}`);
|
||||||
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error(error.message);
|
console.error('An error occurred while exporting credentials. See log messages for details.');
|
||||||
|
logger.error(error.message);
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,16 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Db,
|
Db,
|
||||||
GenericHelpers,
|
|
||||||
} from '../../src';
|
} from '../../src';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLogger,
|
||||||
|
} from '../../src/Logger';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoggerProxy,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as glob from 'glob-promise';
|
import * as glob from 'glob-promise';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
@ -32,17 +39,20 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
};
|
};
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
const { flags } = this.parse(ImportWorkflowsCommand);
|
const { flags } = this.parse(ImportWorkflowsCommand);
|
||||||
|
|
||||||
if (!flags.input) {
|
if (!flags.input) {
|
||||||
GenericHelpers.logOutput(`An input file or directory with --input must be provided`);
|
console.info(`An input file or directory with --input must be provided`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
if (fs.existsSync(flags.input)) {
|
if (fs.existsSync(flags.input)) {
|
||||||
if (!fs.lstatSync(flags.input).isDirectory()) {
|
if (!fs.lstatSync(flags.input).isDirectory()) {
|
||||||
GenericHelpers.logOutput(`The paramenter --input must be a directory`);
|
console.info(`The paramenter --input must be a directory`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,9 +79,11 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Successfully imported', i, i === 1 ? 'workflow.' : 'workflows.');
|
console.info(`Successfully imported ${i} ${i === 1 ? 'workflow.' : 'workflows.'}`);
|
||||||
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error(error.message);
|
console.error('An error occurred while exporting workflows. See log messages for details.');
|
||||||
|
logger.error(error.message);
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,11 +25,17 @@ import {
|
||||||
} from '../src';
|
} from '../src';
|
||||||
import { IDataObject } from 'n8n-workflow';
|
import { IDataObject } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLogger,
|
||||||
|
} from '../src/Logger';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoggerProxy,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
||||||
let processExistCode = 0;
|
let processExistCode = 0;
|
||||||
|
|
||||||
|
|
||||||
export class Start extends Command {
|
export class Start extends Command {
|
||||||
static description = 'Starts n8n. Makes Web-UI available and starts active workflows';
|
static description = 'Starts n8n. Makes Web-UI available and starts active workflows';
|
||||||
|
|
||||||
|
@ -71,7 +77,7 @@ export class Start extends Command {
|
||||||
* get removed.
|
* get removed.
|
||||||
*/
|
*/
|
||||||
static async stopProcess() {
|
static async stopProcess() {
|
||||||
console.log(`\nStopping n8n...`);
|
getLogger().info('\nStopping n8n...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const externalHooks = ExternalHooks();
|
const externalHooks = ExternalHooks();
|
||||||
|
@ -132,13 +138,18 @@ export class Start extends Command {
|
||||||
// Wrap that the process does not close but we can still use async
|
// Wrap that the process does not close but we can still use async
|
||||||
await (async () => {
|
await (async () => {
|
||||||
try {
|
try {
|
||||||
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
logger.info('Initializing n8n process');
|
||||||
|
|
||||||
// Start directly with the init of the database to improve startup time
|
// Start directly with the init of the database to improve startup time
|
||||||
const startDbInitPromise = Db.init().catch((error: Error) => {
|
const startDbInitPromise = Db.init().catch((error: Error) => {
|
||||||
console.error(`There was an error initializing DB: ${error.message}`);
|
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||||
|
|
||||||
processExistCode = 1;
|
processExistCode = 1;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
process.emit('SIGINT');
|
process.emit('SIGINT');
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make sure the settings exist
|
// Make sure the settings exist
|
||||||
|
@ -184,7 +195,7 @@ export class Start extends Command {
|
||||||
cumulativeTimeout += now - lastTimer;
|
cumulativeTimeout += now - lastTimer;
|
||||||
lastTimer = now;
|
lastTimer = now;
|
||||||
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
||||||
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.');
|
logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,9 +224,9 @@ export class Start extends Command {
|
||||||
|
|
||||||
redis.on('error', (error) => {
|
redis.on('error', (error) => {
|
||||||
if (error.toString().includes('ECONNREFUSED') === true) {
|
if (error.toString().includes('ECONNREFUSED') === true) {
|
||||||
console.warn('Redis unavailable - trying to reconnect...');
|
logger.warn('Redis unavailable - trying to reconnect...');
|
||||||
} else {
|
} else {
|
||||||
console.warn('Error with Redis: ', error);
|
logger.warn('Error with Redis: ', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,13 @@ import {
|
||||||
GenericHelpers,
|
GenericHelpers,
|
||||||
} from '../../src';
|
} from '../../src';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLogger,
|
||||||
|
} from '../../src/Logger';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoggerProxy,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
export class UpdateWorkflowCommand extends Command {
|
export class UpdateWorkflowCommand extends Command {
|
||||||
static description = '\Update workflows';
|
static description = '\Update workflows';
|
||||||
|
@ -34,25 +41,28 @@ export class UpdateWorkflowCommand extends Command {
|
||||||
};
|
};
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
const { flags } = this.parse(UpdateWorkflowCommand);
|
const { flags } = this.parse(UpdateWorkflowCommand);
|
||||||
|
|
||||||
if (!flags.all && !flags.id) {
|
if (!flags.all && !flags.id) {
|
||||||
GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`);
|
console.info(`Either option "--all" or "--id" have to be set!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.all && flags.id) {
|
if (flags.all && flags.id) {
|
||||||
GenericHelpers.logOutput(`Either something else on top should be "--all" or "--id" can be set never both!`);
|
console.info(`Either something else on top should be "--all" or "--id" can be set never both!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateQuery: IDataObject = {};
|
const updateQuery: IDataObject = {};
|
||||||
if (flags.active === undefined) {
|
if (flags.active === undefined) {
|
||||||
GenericHelpers.logOutput(`No update flag like "--active=true" has been set!`);
|
console.info(`No update flag like "--active=true" has been set!`);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
if (!['false', 'true'].includes(flags.active)) {
|
if (!['false', 'true'].includes(flags.active)) {
|
||||||
GenericHelpers.logOutput(`Valid values for flag "--active" are only "false" or "true"!`);
|
console.info(`Valid values for flag "--active" are only "false" or "true"!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateQuery.active = flags.active === 'true';
|
updateQuery.active = flags.active === 'true';
|
||||||
|
@ -63,20 +73,21 @@ export class UpdateWorkflowCommand extends Command {
|
||||||
|
|
||||||
const findQuery: IDataObject = {};
|
const findQuery: IDataObject = {};
|
||||||
if (flags.id) {
|
if (flags.id) {
|
||||||
console.log(`Deactivating workflow with ID: ${flags.id}`);
|
console.info(`Deactivating workflow with ID: ${flags.id}`);
|
||||||
findQuery.id = flags.id;
|
findQuery.id = flags.id;
|
||||||
} else {
|
} else {
|
||||||
console.log('Deactivating all workflows');
|
console.info('Deactivating all workflows');
|
||||||
findQuery.active = true;
|
findQuery.active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Db.collections.Workflow!.update(findQuery, updateQuery);
|
await Db.collections.Workflow!.update(findQuery, updateQuery);
|
||||||
console.log('Done');
|
console.info('Done');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('\nGOT ERROR');
|
console.error('Error updating database. See log messages for details.');
|
||||||
console.log('====================================');
|
logger.error('\nGOT ERROR');
|
||||||
console.error(e.message);
|
logger.info('====================================');
|
||||||
console.error(e.stack);
|
logger.error(e.message);
|
||||||
|
logger.error(e.stack);
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,13 @@ import {
|
||||||
} from '../src';
|
} from '../src';
|
||||||
import { IDataObject } from 'n8n-workflow';
|
import { IDataObject } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLogger,
|
||||||
|
} from '../src/Logger';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoggerProxy,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
||||||
let processExistCode = 0;
|
let processExistCode = 0;
|
||||||
|
@ -42,7 +49,7 @@ export class Webhook extends Command {
|
||||||
* get removed.
|
* get removed.
|
||||||
*/
|
*/
|
||||||
static async stopProcess() {
|
static async stopProcess() {
|
||||||
console.log(`\nStopping n8n...`);
|
LoggerProxy.info(`\nStopping n8n...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const externalHooks = ExternalHooks();
|
const externalHooks = ExternalHooks();
|
||||||
|
@ -54,17 +61,6 @@ export class Webhook extends Command {
|
||||||
process.exit(processExistCode);
|
process.exit(processExistCode);
|
||||||
}, 30000);
|
}, 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
|
// Wait for active workflow executions to finish
|
||||||
const activeExecutionsInstance = ActiveExecutions.getInstance();
|
const activeExecutionsInstance = ActiveExecutions.getInstance();
|
||||||
let executingWorkflows = activeExecutionsInstance.getActiveExecutions();
|
let executingWorkflows = activeExecutionsInstance.getActiveExecutions();
|
||||||
|
@ -72,7 +68,7 @@ export class Webhook extends Command {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
while (executingWorkflows.length !== 0) {
|
while (executingWorkflows.length !== 0) {
|
||||||
if (count++ % 4 === 0) {
|
if (count++ % 4 === 0) {
|
||||||
console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`);
|
LoggerProxy.info(`Waiting for ${executingWorkflows.length} active executions to finish...`);
|
||||||
}
|
}
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
setTimeout(resolve, 500);
|
setTimeout(resolve, 500);
|
||||||
|
@ -81,7 +77,7 @@ export class Webhook extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('There was an error shutting down n8n.', error);
|
LoggerProxy.error('There was an error shutting down n8n.', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(processExistCode);
|
process.exit(processExistCode);
|
||||||
|
@ -89,6 +85,9 @@ export class Webhook extends Command {
|
||||||
|
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
// Make sure that n8n shuts down gracefully if possible
|
// Make sure that n8n shuts down gracefully if possible
|
||||||
process.on('SIGTERM', Webhook.stopProcess);
|
process.on('SIGTERM', Webhook.stopProcess);
|
||||||
process.on('SIGINT', Webhook.stopProcess);
|
process.on('SIGINT', Webhook.stopProcess);
|
||||||
|
@ -116,11 +115,12 @@ export class Webhook extends Command {
|
||||||
try {
|
try {
|
||||||
// Start directly with the init of the database to improve startup time
|
// Start directly with the init of the database to improve startup time
|
||||||
const startDbInitPromise = Db.init().catch(error => {
|
const startDbInitPromise = Db.init().catch(error => {
|
||||||
console.error(`There was an error initializing DB: ${error.message}`);
|
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||||
|
|
||||||
processExistCode = 1;
|
processExistCode = 1;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
process.emit('SIGINT');
|
process.emit('SIGINT');
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make sure the settings exist
|
// Make sure the settings exist
|
||||||
|
@ -166,7 +166,7 @@ export class Webhook extends Command {
|
||||||
cumulativeTimeout += now - lastTimer;
|
cumulativeTimeout += now - lastTimer;
|
||||||
lastTimer = now;
|
lastTimer = now;
|
||||||
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
||||||
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.');
|
logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,9 +195,9 @@ export class Webhook extends Command {
|
||||||
|
|
||||||
redis.on('error', (error) => {
|
redis.on('error', (error) => {
|
||||||
if (error.toString().includes('ECONNREFUSED') === true) {
|
if (error.toString().includes('ECONNREFUSED') === true) {
|
||||||
console.warn('Redis unavailable - trying to reconnect...');
|
logger.warn('Redis unavailable - trying to reconnect...');
|
||||||
} else {
|
} else {
|
||||||
console.warn('Error with Redis: ', error);
|
logger.warn('Error with Redis: ', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -209,14 +209,16 @@ export class Webhook extends Command {
|
||||||
await activeWorkflowRunner.initWebhooks();
|
await activeWorkflowRunner.initWebhooks();
|
||||||
|
|
||||||
const editorUrl = GenericHelpers.getBaseUrl();
|
const editorUrl = GenericHelpers.getBaseUrl();
|
||||||
this.log('Webhook listener waiting for requests.');
|
console.info('Webhook listener waiting for requests.');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error(`There was an error: ${error.message}`);
|
console.error('Exiting due to error. See log message for details.');
|
||||||
|
logger.error(`Webhook process cannot continue. "${error.message}"`);
|
||||||
|
|
||||||
processExistCode = 1;
|
processExistCode = 1;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
process.emit('SIGINT');
|
process.emit('SIGINT');
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,14 @@ import {
|
||||||
WorkflowExecuteAdditionalData,
|
WorkflowExecuteAdditionalData,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLogger,
|
||||||
|
} from '../src/Logger';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoggerProxy,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import * as Bull from 'bull';
|
import * as Bull from 'bull';
|
||||||
import * as Queue from '../src/Queue';
|
import * as Queue from '../src/Queue';
|
||||||
|
@ -71,7 +79,7 @@ export class Worker extends Command {
|
||||||
* get removed.
|
* get removed.
|
||||||
*/
|
*/
|
||||||
static async stopProcess() {
|
static async stopProcess() {
|
||||||
console.log(`\nStopping n8n...`);
|
LoggerProxy.info(`Stopping n8n...`);
|
||||||
|
|
||||||
// Stop accepting new jobs
|
// Stop accepting new jobs
|
||||||
Worker.jobQueue.pause(true);
|
Worker.jobQueue.pause(true);
|
||||||
|
@ -95,7 +103,7 @@ export class Worker extends Command {
|
||||||
while (Object.keys(Worker.runningJobs).length !== 0) {
|
while (Object.keys(Worker.runningJobs).length !== 0) {
|
||||||
if (count++ % 4 === 0) {
|
if (count++ % 4 === 0) {
|
||||||
const waitLeft = Math.ceil((stopTime - new Date().getTime()) / 1000);
|
const waitLeft = Math.ceil((stopTime - new Date().getTime()) / 1000);
|
||||||
console.log(`Waiting for ${Object.keys(Worker.runningJobs).length} active executions to finish... (wait ${waitLeft} more seconds)`);
|
LoggerProxy.info(`Waiting for ${Object.keys(Worker.runningJobs).length} active executions to finish... (wait ${waitLeft} more seconds)`);
|
||||||
}
|
}
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
setTimeout(resolve, 500);
|
setTimeout(resolve, 500);
|
||||||
|
@ -103,7 +111,7 @@ export class Worker extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('There was an error shutting down n8n.', error);
|
LoggerProxy.error('There was an error shutting down n8n.', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(Worker.processExistCode);
|
process.exit(Worker.processExistCode);
|
||||||
|
@ -113,7 +121,7 @@ export class Worker extends Command {
|
||||||
const jobData = job.data as IBullJobData;
|
const jobData = job.data as IBullJobData;
|
||||||
const executionDb = await Db.collections.Execution!.findOne(jobData.executionId) as IExecutionFlattedDb;
|
const executionDb = await Db.collections.Execution!.findOne(jobData.executionId) as IExecutionFlattedDb;
|
||||||
const currentExecutionDb = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse;
|
const currentExecutionDb = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse;
|
||||||
console.log(`Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${jobData.executionId})`);
|
LoggerProxy.info(`Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${jobData.executionId})`);
|
||||||
|
|
||||||
let staticData = currentExecutionDb.workflowData!.staticData;
|
let staticData = currentExecutionDb.workflowData!.staticData;
|
||||||
if (jobData.loadStaticData === true) {
|
if (jobData.loadStaticData === true) {
|
||||||
|
@ -170,7 +178,10 @@ export class Worker extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
console.log('Starting n8n worker...');
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
console.info('Starting n8n worker...');
|
||||||
|
|
||||||
// Make sure that n8n shuts down gracefully if possible
|
// Make sure that n8n shuts down gracefully if possible
|
||||||
process.on('SIGTERM', Worker.stopProcess);
|
process.on('SIGTERM', Worker.stopProcess);
|
||||||
|
@ -183,11 +194,12 @@ export class Worker extends Command {
|
||||||
|
|
||||||
// Start directly with the init of the database to improve startup time
|
// Start directly with the init of the database to improve startup time
|
||||||
const startDbInitPromise = Db.init().catch(error => {
|
const startDbInitPromise = Db.init().catch(error => {
|
||||||
console.error(`There was an error initializing DB: ${error.message}`);
|
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||||
|
|
||||||
Worker.processExistCode = 1;
|
Worker.processExistCode = 1;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
process.emit('SIGINT');
|
process.emit('SIGINT');
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make sure the settings exist
|
// Make sure the settings exist
|
||||||
|
@ -221,10 +233,10 @@ export class Worker extends Command {
|
||||||
|
|
||||||
const versions = await GenericHelpers.getVersions();
|
const versions = await GenericHelpers.getVersions();
|
||||||
|
|
||||||
console.log('\nn8n worker is now ready');
|
console.info('\nn8n worker is now ready');
|
||||||
console.log(` * Version: ${versions.cli}`);
|
console.info(` * Version: ${versions.cli}`);
|
||||||
console.log(` * Concurrency: ${flags.concurrency}`);
|
console.info(` * Concurrency: ${flags.concurrency}`);
|
||||||
console.log('');
|
console.info('');
|
||||||
|
|
||||||
Worker.jobQueue.on('global:progress', (jobId, progress) => {
|
Worker.jobQueue.on('global:progress', (jobId, progress) => {
|
||||||
// Progress of a job got updated which does get used
|
// Progress of a job got updated which does get used
|
||||||
|
@ -252,27 +264,28 @@ export class Worker extends Command {
|
||||||
cumulativeTimeout += now - lastTimer;
|
cumulativeTimeout += now - lastTimer;
|
||||||
lastTimer = now;
|
lastTimer = now;
|
||||||
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
||||||
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.');
|
logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.warn('Redis unavailable - trying to reconnect...');
|
logger.warn('Redis unavailable - trying to reconnect...');
|
||||||
} else if (error.toString().includes('Error initializing Lua scripts') === true) {
|
} else if (error.toString().includes('Error initializing Lua scripts') === true) {
|
||||||
// This is a non-recoverable error
|
// This is a non-recoverable error
|
||||||
// Happens when worker starts and Redis is unavailable
|
// Happens when worker starts and Redis is unavailable
|
||||||
// Even if Redis comes back online, worker will be zombie
|
// Even if Redis comes back online, worker will be zombie
|
||||||
console.error('Error initializing worker.');
|
logger.error('Error initializing worker.');
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
} else {
|
} else {
|
||||||
console.error('Error from queue: ', error);
|
logger.error('Error from queue: ', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error(`There was an error: ${error.message}`);
|
logger.error(`Worker process cannot continue. "${error.message}"`);
|
||||||
|
|
||||||
Worker.processExistCode = 1;
|
Worker.processExistCode = 1;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
process.emit('SIGINT');
|
process.emit('SIGINT');
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import * as convict from 'convict';
|
import * as convict from 'convict';
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as core from 'n8n-core';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
@ -572,6 +574,41 @@ const config = convict({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
logs: {
|
||||||
|
level: {
|
||||||
|
doc: 'Log output level. Options are error, warn, info, verbose and debug.',
|
||||||
|
format: String,
|
||||||
|
default: 'info',
|
||||||
|
env: 'N8N_LOG_LEVEL',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
doc: 'Where to output logs. Options are: console, file. Multiple can be separated by comma (",")',
|
||||||
|
format: String,
|
||||||
|
default: 'console',
|
||||||
|
env: 'N8N_LOG_OUTPUT',
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
fileCountMax: {
|
||||||
|
doc: 'Maximum number of files to keep.',
|
||||||
|
format: Number,
|
||||||
|
default: 100,
|
||||||
|
env: 'N8N_LOG_FILE_COUNT_MAX',
|
||||||
|
},
|
||||||
|
fileSizeMax: {
|
||||||
|
doc: 'Maximum size for each log file in MB.',
|
||||||
|
format: Number,
|
||||||
|
default: 16,
|
||||||
|
env: 'N8N_LOG_FILE_SIZE_MAX',
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
doc: 'Log file location; only used if log output is set to file.',
|
||||||
|
format: String,
|
||||||
|
default: path.join(core.UserSettings.getUserN8nFolderPath(), 'logs/n8n.log'),
|
||||||
|
env: 'N8N_LOG_FILE_LOCATION',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Overwrite default configuration with settings which got defined in
|
// Overwrite default configuration with settings which got defined in
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.117.0",
|
"version": "0.118.0",
|
||||||
"description": "n8n Workflow Automation Tool",
|
"description": "n8n Workflow Automation Tool",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
"workflow"
|
"workflow"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=14.0.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"bin",
|
"bin",
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
"@types/jest": "^26.0.13",
|
"@types/jest": "^26.0.13",
|
||||||
"@types/localtunnel": "^1.9.0",
|
"@types/localtunnel": "^1.9.0",
|
||||||
"@types/lodash.get": "^4.4.6",
|
"@types/lodash.get": "^4.4.6",
|
||||||
"@types/node": "14.0.27",
|
"@types/node": "^14.14.40",
|
||||||
"@types/open": "^6.1.0",
|
"@types/open": "^6.1.0",
|
||||||
"@types/parseurl": "^1.3.1",
|
"@types/parseurl": "^1.3.1",
|
||||||
"@types/request-promise-native": "~1.0.15",
|
"@types/request-promise-native": "~1.0.15",
|
||||||
|
@ -87,6 +87,7 @@
|
||||||
"body-parser": "^1.18.3",
|
"body-parser": "^1.18.3",
|
||||||
"body-parser-xml": "^1.1.0",
|
"body-parser-xml": "^1.1.0",
|
||||||
"bull": "^3.19.0",
|
"bull": "^3.19.0",
|
||||||
|
"callsites": "^3.1.0",
|
||||||
"client-oauth2": "^4.2.5",
|
"client-oauth2": "^4.2.5",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"connect-history-api-fallback": "^1.6.0",
|
"connect-history-api-fallback": "^1.6.0",
|
||||||
|
@ -103,10 +104,10 @@
|
||||||
"localtunnel": "^2.0.0",
|
"localtunnel": "^2.0.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"mysql2": "~2.2.0",
|
"mysql2": "~2.2.0",
|
||||||
"n8n-core": "~0.68.0",
|
"n8n-core": "~0.69.0",
|
||||||
"n8n-editor-ui": "~0.87.0",
|
"n8n-editor-ui": "~0.88.0",
|
||||||
"n8n-nodes-base": "~0.114.0",
|
"n8n-nodes-base": "~0.115.0",
|
||||||
"n8n-workflow": "~0.56.0",
|
"n8n-workflow": "~0.57.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"open": "^7.0.0",
|
"open": "^7.0.0",
|
||||||
"pg": "^8.3.0",
|
"pg": "^8.3.0",
|
||||||
|
@ -114,7 +115,7 @@
|
||||||
"request-promise-native": "^1.0.7",
|
"request-promise-native": "^1.0.7",
|
||||||
"sqlite3": "^5.0.1",
|
"sqlite3": "^5.0.1",
|
||||||
"sse-channel": "^3.1.1",
|
"sse-channel": "^3.1.1",
|
||||||
"tslib": "1.13.0",
|
"tslib": "1.14.1",
|
||||||
"typeorm": "^0.2.30"
|
"typeorm": "^0.2.30"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|
|
@ -35,6 +35,9 @@ import {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
import {
|
||||||
|
LoggerProxy as Logger,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
export class ActiveWorkflowRunner {
|
export class ActiveWorkflowRunner {
|
||||||
private activeWorkflows: ActiveWorkflows | null = null;
|
private activeWorkflows: ActiveWorkflows | null = null;
|
||||||
|
@ -43,7 +46,6 @@ export class ActiveWorkflowRunner {
|
||||||
[key: string]: IActivationError;
|
[key: string]: IActivationError;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
|
||||||
// Get the active workflows from database
|
// Get the active workflows from database
|
||||||
|
@ -59,20 +61,24 @@ export class ActiveWorkflowRunner {
|
||||||
this.activeWorkflows = new ActiveWorkflows();
|
this.activeWorkflows = new ActiveWorkflows();
|
||||||
|
|
||||||
if (workflowsData.length !== 0) {
|
if (workflowsData.length !== 0) {
|
||||||
console.log('\n ================================');
|
console.info(' ================================');
|
||||||
console.log(' Start Active Workflows:');
|
console.info(' Start Active Workflows:');
|
||||||
console.log(' ================================');
|
console.info(' ================================');
|
||||||
|
|
||||||
for (const workflowData of workflowsData) {
|
for (const workflowData of workflowsData) {
|
||||||
console.log(` - ${workflowData.name}`);
|
console.log(` - ${workflowData.name}`);
|
||||||
|
Logger.debug(`Initializing active workflow "${workflowData.name}" (startup)`, {workflowName: workflowData.name, workflowId: workflowData.id});
|
||||||
try {
|
try {
|
||||||
await this.add(workflowData.id.toString(), 'init', workflowData);
|
await this.add(workflowData.id.toString(), 'init', workflowData);
|
||||||
|
Logger.verbose(`Successfully started workflow "${workflowData.name}"`, {workflowName: workflowData.name, workflowId: workflowData.id});
|
||||||
console.log(` => Started`);
|
console.log(` => Started`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(` => ERROR: Workflow could not be activated:`);
|
console.log(` => ERROR: Workflow could not be activated:`);
|
||||||
console.log(` ${error.message}`);
|
console.log(` ${error.message}`);
|
||||||
|
Logger.error(`Unable to initialize workflow "${workflowData.name}" (startup)`, {workflowName: workflowData.name, workflowId: workflowData.id});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Logger.verbose('Finished initializing active workflows (startup)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +94,7 @@ export class ActiveWorkflowRunner {
|
||||||
*/
|
*/
|
||||||
async removeAll(): Promise<void> {
|
async removeAll(): Promise<void> {
|
||||||
const activeWorkflowId: string[] = [];
|
const activeWorkflowId: string[] = [];
|
||||||
|
Logger.verbose('Call to remove all active workflows received (removeAll)');
|
||||||
|
|
||||||
if (this.activeWorkflows !== null) {
|
if (this.activeWorkflows !== null) {
|
||||||
// TODO: This should be renamed!
|
// TODO: This should be renamed!
|
||||||
|
@ -117,6 +124,7 @@ export class ActiveWorkflowRunner {
|
||||||
* @memberof ActiveWorkflowRunner
|
* @memberof ActiveWorkflowRunner
|
||||||
*/
|
*/
|
||||||
async executeWebhook(httpMethod: WebhookHttpMethod, path: string, req: express.Request, res: express.Response): Promise<IResponseCallbackData> {
|
async executeWebhook(httpMethod: WebhookHttpMethod, path: string, req: express.Request, res: express.Response): Promise<IResponseCallbackData> {
|
||||||
|
Logger.debug(`Received webhoook "${httpMethod}" for path "${path}"`);
|
||||||
if (this.activeWorkflows === null) {
|
if (this.activeWorkflows === null) {
|
||||||
throw new ResponseHelper.ResponseError('The "activeWorkflows" instance did not get initialized yet.', 404, 404);
|
throw new ResponseHelper.ResponseError('The "activeWorkflows" instance did not get initialized yet.', 404, 404);
|
||||||
}
|
}
|
||||||
|
@ -437,6 +445,7 @@ export class ActiveWorkflowRunner {
|
||||||
return ((workflow: Workflow, node: INode) => {
|
return ((workflow: Workflow, node: INode) => {
|
||||||
const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode, activation);
|
const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode, activation);
|
||||||
returnFunctions.__emit = (data: INodeExecutionData[][]): void => {
|
returnFunctions.__emit = (data: INodeExecutionData[][]): void => {
|
||||||
|
Logger.debug(`Received event to trigger execution for workflow "${workflow.name}"`);
|
||||||
this.runWorkflow(workflowData, node, data, additionalData, mode);
|
this.runWorkflow(workflowData, node, data, additionalData, mode);
|
||||||
};
|
};
|
||||||
return returnFunctions;
|
return returnFunctions;
|
||||||
|
@ -458,6 +467,7 @@ export class ActiveWorkflowRunner {
|
||||||
return ((workflow: Workflow, node: INode) => {
|
return ((workflow: Workflow, node: INode) => {
|
||||||
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode, activation);
|
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode, activation);
|
||||||
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
|
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
|
||||||
|
Logger.debug(`Received trigger for workflow "${workflow.name}"`);
|
||||||
WorkflowHelpers.saveStaticData(workflow);
|
WorkflowHelpers.saveStaticData(workflow);
|
||||||
this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) => console.error(err));
|
this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) => console.error(err));
|
||||||
};
|
};
|
||||||
|
@ -492,6 +502,7 @@ export class ActiveWorkflowRunner {
|
||||||
|
|
||||||
const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated(['n8n-nodes-base.start']);
|
const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated(['n8n-nodes-base.start']);
|
||||||
if (canBeActivated === false) {
|
if (canBeActivated === false) {
|
||||||
|
Logger.error(`Unable to activate workflow "${workflowData.name}"`);
|
||||||
throw new Error(`The workflow can not be activated because it does not contain any nodes which could start the workflow. Only workflows which have trigger or webhook nodes can be activated.`);
|
throw new Error(`The workflow can not be activated because it does not contain any nodes which could start the workflow. Only workflows which have trigger or webhook nodes can be activated.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -507,6 +518,7 @@ export class ActiveWorkflowRunner {
|
||||||
if (workflowInstance.getTriggerNodes().length !== 0
|
if (workflowInstance.getTriggerNodes().length !== 0
|
||||||
|| workflowInstance.getPollNodes().length !== 0) {
|
|| workflowInstance.getPollNodes().length !== 0) {
|
||||||
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions);
|
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions);
|
||||||
|
Logger.info(`Successfully activated workflow "${workflowData.name}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.activationErrors[workflowId] !== undefined) {
|
if (this.activationErrors[workflowId] !== undefined) {
|
||||||
|
|
|
@ -3,14 +3,11 @@ import * as express from 'express';
|
||||||
import { join as pathJoin } from 'path';
|
import { join as pathJoin } from 'path';
|
||||||
import {
|
import {
|
||||||
readFile as fsReadFile,
|
readFile as fsReadFile,
|
||||||
} from 'fs';
|
} from 'fs/promises';
|
||||||
import { promisify } from 'util';
|
|
||||||
import { IDataObject } from 'n8n-workflow';
|
import { IDataObject } from 'n8n-workflow';
|
||||||
|
|
||||||
import { IPackageVersions } from './';
|
import { IPackageVersions } from './';
|
||||||
|
|
||||||
const fsReadFileAsync = promisify(fsReadFile);
|
|
||||||
|
|
||||||
let versionCache: IPackageVersions | undefined;
|
let versionCache: IPackageVersions | undefined;
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,7 +69,7 @@ export async function getVersions(): Promise<IPackageVersions> {
|
||||||
return versionCache;
|
return versionCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageFile = await fsReadFileAsync(pathJoin(__dirname, '../../package.json'), 'utf8') as string;
|
const packageFile = await fsReadFile(pathJoin(__dirname, '../../package.json'), 'utf8') as string;
|
||||||
const packageData = JSON.parse(packageFile);
|
const packageData = JSON.parse(packageFile);
|
||||||
|
|
||||||
versionCache = {
|
versionCache = {
|
||||||
|
@ -122,7 +119,7 @@ export async function getConfigValue(configKey: string): Promise<string | boolea
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await fsReadFileAsync(fileEnvironmentVariable, 'utf8') as string;
|
data = await fsReadFile(fileEnvironmentVariable, 'utf8') as string;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);
|
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);
|
||||||
|
|
|
@ -14,15 +14,9 @@ import {
|
||||||
readdir as fsReaddir,
|
readdir as fsReaddir,
|
||||||
readFile as fsReadFile,
|
readFile as fsReadFile,
|
||||||
stat as fsStat,
|
stat as fsStat,
|
||||||
} from 'fs';
|
} from 'fs/promises';
|
||||||
import * as glob from 'glob-promise';
|
import * as glob from 'glob-promise';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { promisify } from 'util';
|
|
||||||
|
|
||||||
const fsAccessAsync = promisify(fsAccess);
|
|
||||||
const fsReaddirAsync = promisify(fsReaddir);
|
|
||||||
const fsReadFileAsync = promisify(fsReadFile);
|
|
||||||
const fsStatAsync = promisify(fsStat);
|
|
||||||
|
|
||||||
|
|
||||||
class LoadNodesAndCredentialsClass {
|
class LoadNodesAndCredentialsClass {
|
||||||
|
@ -49,7 +43,7 @@ class LoadNodesAndCredentialsClass {
|
||||||
];
|
];
|
||||||
for (const checkPath of checkPaths) {
|
for (const checkPath of checkPaths) {
|
||||||
try {
|
try {
|
||||||
await fsAccessAsync(checkPath);
|
await fsAccess(checkPath);
|
||||||
// Folder exists, so use it.
|
// Folder exists, so use it.
|
||||||
this.nodeModulesPath = path.dirname(checkPath);
|
this.nodeModulesPath = path.dirname(checkPath);
|
||||||
break;
|
break;
|
||||||
|
@ -102,13 +96,13 @@ class LoadNodesAndCredentialsClass {
|
||||||
const getN8nNodePackagesRecursive = async (relativePath: string): Promise<string[]> => {
|
const getN8nNodePackagesRecursive = async (relativePath: string): Promise<string[]> => {
|
||||||
const results: string[] = [];
|
const results: string[] = [];
|
||||||
const nodeModulesPath = `${this.nodeModulesPath}/${relativePath}`;
|
const nodeModulesPath = `${this.nodeModulesPath}/${relativePath}`;
|
||||||
for (const file of await fsReaddirAsync(nodeModulesPath)) {
|
for (const file of await fsReaddir(nodeModulesPath)) {
|
||||||
const isN8nNodesPackage = file.indexOf('n8n-nodes-') === 0;
|
const isN8nNodesPackage = file.indexOf('n8n-nodes-') === 0;
|
||||||
const isNpmScopedPackage = file.indexOf('@') === 0;
|
const isNpmScopedPackage = file.indexOf('@') === 0;
|
||||||
if (!isN8nNodesPackage && !isNpmScopedPackage) {
|
if (!isN8nNodesPackage && !isNpmScopedPackage) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!(await fsStatAsync(nodeModulesPath)).isDirectory()) {
|
if (!(await fsStat(nodeModulesPath)).isDirectory()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isN8nNodesPackage) { results.push(`${relativePath}${file}`); }
|
if (isN8nNodesPackage) { results.push(`${relativePath}${file}`); }
|
||||||
|
@ -234,7 +228,7 @@ class LoadNodesAndCredentialsClass {
|
||||||
const packagePath = path.join(this.nodeModulesPath, packageName);
|
const packagePath = path.join(this.nodeModulesPath, packageName);
|
||||||
|
|
||||||
// Read the data from the package.json file to see if any n8n data is defiend
|
// Read the data from the package.json file to see if any n8n data is defiend
|
||||||
const packageFileString = await fsReadFileAsync(path.join(packagePath, 'package.json'), 'utf8');
|
const packageFileString = await fsReadFile(path.join(packagePath, 'package.json'), 'utf8');
|
||||||
const packageFile = JSON.parse(packageFileString);
|
const packageFile = JSON.parse(packageFileString);
|
||||||
if (!packageFile.hasOwnProperty('n8n')) {
|
if (!packageFile.hasOwnProperty('n8n')) {
|
||||||
return;
|
return;
|
||||||
|
|
114
packages/cli/src/Logger.ts
Normal file
114
packages/cli/src/Logger.ts
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import config = require('../config');
|
||||||
|
import * as winston from 'winston';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
ILogger,
|
||||||
|
LogTypes,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import * as callsites from 'callsites';
|
||||||
|
import { basename } from 'path';
|
||||||
|
|
||||||
|
class Logger implements ILogger {
|
||||||
|
private logger: winston.Logger;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const level = config.get('logs.level');
|
||||||
|
const output = (config.get('logs.output') as string).split(',').map(output => output.trim());
|
||||||
|
|
||||||
|
this.logger = winston.createLogger({
|
||||||
|
level,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (output.includes('console')) {
|
||||||
|
let format: winston.Logform.Format;
|
||||||
|
if (['debug', 'verbose'].includes(level)) {
|
||||||
|
format = winston.format.combine(
|
||||||
|
winston.format.metadata(),
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.colorize({ all: true }),
|
||||||
|
winston.format.printf(({ level, message, timestamp, metadata }) => {
|
||||||
|
return `${timestamp} | ${level.padEnd(18)} | ${message}` + (Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : '');
|
||||||
|
}) as winston.Logform.Format
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
format = winston.format.printf(({ message }) => message) as winston.Logform.Format;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.add(
|
||||||
|
new winston.transports.Console({
|
||||||
|
format,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.includes('file')) {
|
||||||
|
const fileLogFormat = winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.metadata(),
|
||||||
|
winston.format.json()
|
||||||
|
);
|
||||||
|
this.logger.add(
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: config.get('logs.file.location'),
|
||||||
|
format: fileLogFormat,
|
||||||
|
maxsize: config.get('logs.file.fileSizeMax') as number * 1048576, // config * 1mb
|
||||||
|
maxFiles: config.get('logs.file.fileCountMax'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(type: LogTypes, message: string, meta: object = {}) {
|
||||||
|
const callsite = callsites();
|
||||||
|
// We are using the third array element as the structure is as follows:
|
||||||
|
// [0]: this file
|
||||||
|
// [1]: Should be LoggerProxy
|
||||||
|
// [2]: Should point to the caller.
|
||||||
|
// Note: getting line number is useless because at this point
|
||||||
|
// We are in runtime, so it means we are looking at compiled js files
|
||||||
|
const logDetails = {} as IDataObject;
|
||||||
|
if (callsite[2] !== undefined) {
|
||||||
|
logDetails.file = basename(callsite[2].getFileName() || '');
|
||||||
|
const functionName = callsite[2].getFunctionName();
|
||||||
|
if (functionName) {
|
||||||
|
logDetails.function = functionName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.log(type, message, {...meta, ...logDetails});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods below
|
||||||
|
|
||||||
|
debug(message: string, meta: object = {}) {
|
||||||
|
this.log('debug', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: string, meta: object = {}) {
|
||||||
|
this.log('info', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, meta: object = {}) {
|
||||||
|
this.log('error', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose(message: string, meta: object = {}) {
|
||||||
|
this.log('verbose', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string, meta: object = {}) {
|
||||||
|
this.log('warn', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeLoggerInstance: Logger | undefined;
|
||||||
|
|
||||||
|
export function getLogger() {
|
||||||
|
if (activeLoggerInstance === undefined) {
|
||||||
|
activeLoggerInstance = new Logger();
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeLoggerInstance;
|
||||||
|
}
|
|
@ -7,6 +7,10 @@ import {
|
||||||
IPushDataType,
|
IPushDataType,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoggerProxy as Logger,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
export class Push {
|
export class Push {
|
||||||
private channel: sseChannel;
|
private channel: sseChannel;
|
||||||
private connections: {
|
private connections: {
|
||||||
|
@ -24,6 +28,7 @@ export class Push {
|
||||||
|
|
||||||
this.channel.on('disconnect', (channel: string, res: express.Response) => {
|
this.channel.on('disconnect', (channel: string, res: express.Response) => {
|
||||||
if (res.req !== undefined) {
|
if (res.req !== undefined) {
|
||||||
|
Logger.debug(`Remove editor-UI session`, { sessionId: res.req.query.sessionId });
|
||||||
delete this.connections[res.req.query.sessionId as string];
|
delete this.connections[res.req.query.sessionId as string];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -39,6 +44,8 @@ export class Push {
|
||||||
* @memberof Push
|
* @memberof Push
|
||||||
*/
|
*/
|
||||||
add(sessionId: string, req: express.Request, res: express.Response) {
|
add(sessionId: string, req: express.Request, res: express.Response) {
|
||||||
|
Logger.debug(`Add editor-UI session`, { sessionId });
|
||||||
|
|
||||||
if (this.connections[sessionId] !== undefined) {
|
if (this.connections[sessionId] !== undefined) {
|
||||||
// Make sure to remove existing connection with the same session
|
// Make sure to remove existing connection with the same session
|
||||||
// id if one exists already
|
// id if one exists already
|
||||||
|
@ -64,11 +71,12 @@ export class Push {
|
||||||
|
|
||||||
send(type: IPushDataType, data: any, sessionId?: string) { // tslint:disable-line:no-any
|
send(type: IPushDataType, data: any, sessionId?: string) { // tslint:disable-line:no-any
|
||||||
if (sessionId !== undefined && this.connections[sessionId] === undefined) {
|
if (sessionId !== undefined && this.connections[sessionId] === undefined) {
|
||||||
// TODO: Log that properly!
|
Logger.error(`The session "${sessionId}" is not registred.`, { sessionId });
|
||||||
console.error(`The session "${sessionId}" is not registred.`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.debug(`Send data of type "${type}" to editor-UI`, { dataType: type, sessionId });
|
||||||
|
|
||||||
const sendData: IPushData = {
|
const sendData: IPushData = {
|
||||||
type,
|
type,
|
||||||
data,
|
data,
|
||||||
|
|
|
@ -30,13 +30,14 @@ import {
|
||||||
IWebhookData,
|
IWebhookData,
|
||||||
IWebhookResponseData,
|
IWebhookResponseData,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
|
LoggerProxy as Logger,
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
const activeExecutions = ActiveExecutions.getInstance();
|
|
||||||
|
|
||||||
|
const activeExecutions = ActiveExecutions.getInstance();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the webhooks which should be created for the give workflow
|
* Returns all the webhooks which should be created for the give workflow
|
||||||
|
@ -286,6 +287,8 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||||
const workflowRunner = new WorkflowRunner();
|
const workflowRunner = new WorkflowRunner();
|
||||||
const executionId = await workflowRunner.run(runData, true, !didSendResponse);
|
const executionId = await workflowRunner.run(runData, true, !didSendResponse);
|
||||||
|
|
||||||
|
Logger.verbose(`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`, { executionId });
|
||||||
|
|
||||||
// Get a promise which resolves when the workflow did execute and send then response
|
// Get a promise which resolves when the workflow did execute and send then response
|
||||||
const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<IExecutionDb | undefined>;
|
const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise<IExecutionDb | undefined>;
|
||||||
executePromise.then((data) => {
|
executePromise.then((data) => {
|
||||||
|
|
|
@ -37,6 +37,7 @@ import {
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
IWorkflowExecuteHooks,
|
IWorkflowExecuteHooks,
|
||||||
IWorkflowHooksOptionalParameters,
|
IWorkflowHooksOptionalParameters,
|
||||||
|
LoggerProxy as Logger,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
|
@ -44,11 +45,10 @@ import {
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
|
||||||
import { LessThanOrEqual } from "typeorm";
|
import { LessThanOrEqual } from 'typeorm';
|
||||||
|
|
||||||
const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
|
const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if there was an error and if errorWorkflow or a trigger is defined. If so it collects
|
* Checks if there was an error and if errorWorkflow or a trigger is defined. If so it collects
|
||||||
* all the data and executes it
|
* all the data and executes it
|
||||||
|
@ -85,9 +85,11 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo
|
||||||
// Run the error workflow
|
// Run the error workflow
|
||||||
// To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow.
|
// To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow.
|
||||||
if (workflowData.settings !== undefined && workflowData.settings.errorWorkflow && !(mode === 'error' && workflowData.id && workflowData.settings.errorWorkflow.toString() === workflowData.id.toString())) {
|
if (workflowData.settings !== undefined && workflowData.settings.errorWorkflow && !(mode === 'error' && workflowData.id && workflowData.settings.errorWorkflow.toString() === workflowData.id.toString())) {
|
||||||
|
Logger.verbose(`Start external error workflow`, { executionId, errorWorkflowId: workflowData.settings.errorWorkflow.toString(), workflowId: workflowData.id });
|
||||||
// If a specific error workflow is set run only that one
|
// If a specific error workflow is set run only that one
|
||||||
WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData);
|
WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData);
|
||||||
} else if (mode !== 'error' && workflowData.id !== undefined && workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)) {
|
} else if (mode !== 'error' && workflowData.id !== undefined && workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)) {
|
||||||
|
Logger.verbose(`Start internal error workflow`, { executionId, workflowId: workflowData.id });
|
||||||
// If the workflow contains
|
// If the workflow contains
|
||||||
WorkflowHelpers.executeErrorWorkflow(workflowData.id.toString(), workflowErrorData);
|
WorkflowHelpers.executeErrorWorkflow(workflowData.id.toString(), workflowErrorData);
|
||||||
}
|
}
|
||||||
|
@ -102,6 +104,8 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo
|
||||||
let throttling = false;
|
let throttling = false;
|
||||||
function pruneExecutionData(): void {
|
function pruneExecutionData(): void {
|
||||||
if (!throttling) {
|
if (!throttling) {
|
||||||
|
Logger.verbose('Pruning execution data from database');
|
||||||
|
|
||||||
throttling = true;
|
throttling = true;
|
||||||
const timeout = config.get('executions.pruneDataTimeout') as number; // in seconds
|
const timeout = config.get('executions.pruneDataTimeout') as number; // in seconds
|
||||||
const maxAge = config.get('executions.pruneDataMaxAge') as number; // in h
|
const maxAge = config.get('executions.pruneDataMaxAge') as number; // in h
|
||||||
|
@ -133,6 +137,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
|
||||||
if (this.sessionId === undefined) {
|
if (this.sessionId === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id });
|
||||||
|
|
||||||
const pushInstance = Push.getInstance();
|
const pushInstance = Push.getInstance();
|
||||||
pushInstance.send('nodeExecuteBefore', {
|
pushInstance.send('nodeExecuteBefore', {
|
||||||
|
@ -147,6 +152,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
|
||||||
if (this.sessionId === undefined) {
|
if (this.sessionId === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id });
|
||||||
|
|
||||||
const pushInstance = Push.getInstance();
|
const pushInstance = Push.getInstance();
|
||||||
pushInstance.send('nodeExecuteAfter', {
|
pushInstance.send('nodeExecuteAfter', {
|
||||||
|
@ -158,6 +164,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
|
||||||
],
|
],
|
||||||
workflowExecuteBefore: [
|
workflowExecuteBefore: [
|
||||||
async function (this: WorkflowHooks): Promise<void> {
|
async function (this: WorkflowHooks): Promise<void> {
|
||||||
|
Logger.debug(`Executing hook (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id });
|
||||||
// Push data to session which started the workflow
|
// Push data to session which started the workflow
|
||||||
if (this.sessionId === undefined) {
|
if (this.sessionId === undefined) {
|
||||||
return;
|
return;
|
||||||
|
@ -168,13 +175,14 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
startedAt: new Date(),
|
startedAt: new Date(),
|
||||||
retryOf: this.retryOf,
|
retryOf: this.retryOf,
|
||||||
workflowId: this.workflowData.id as string,
|
workflowId: this.workflowData.id, sessionId: this.sessionId as string,
|
||||||
workflowName: this.workflowData.name,
|
workflowName: this.workflowData.name,
|
||||||
}, this.sessionId);
|
}, this.sessionId);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
workflowExecuteAfter: [
|
workflowExecuteAfter: [
|
||||||
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> {
|
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> {
|
||||||
|
Logger.debug(`Executing hook (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id });
|
||||||
// Push data to session which started the workflow
|
// Push data to session which started the workflow
|
||||||
if (this.sessionId === undefined) {
|
if (this.sessionId === undefined) {
|
||||||
return;
|
return;
|
||||||
|
@ -195,6 +203,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Push data to editor-ui once workflow finished
|
// Push data to editor-ui once workflow finished
|
||||||
|
Logger.debug(`Save execution progress to database for execution ID ${this.executionId} `, { executionId: this.executionId, workflowId: this.workflowData.id });
|
||||||
// TODO: Look at this again
|
// TODO: Look at this again
|
||||||
const sendData: IPushDataExecutionFinished = {
|
const sendData: IPushDataExecutionFinished = {
|
||||||
executionId: this.executionId,
|
executionId: this.executionId,
|
||||||
|
@ -232,6 +241,8 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Logger.debug(`Save execution progress to database for execution ID ${this.executionId} `, { executionId: this.executionId, nodeName });
|
||||||
|
|
||||||
const execution = await Db.collections.Execution!.findOne(this.executionId);
|
const execution = await Db.collections.Execution!.findOne(this.executionId);
|
||||||
|
|
||||||
if (execution === undefined) {
|
if (execution === undefined) {
|
||||||
|
@ -286,7 +297,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
|
||||||
// For busy machines, we may get "Database is locked" errors.
|
// For busy machines, we may get "Database is locked" errors.
|
||||||
|
|
||||||
// We do this to prevent crashes and executions ending in `unknown` state.
|
// We do this to prevent crashes and executions ending in `unknown` state.
|
||||||
console.log(`Failed saving execution progress to database for execution ID ${this.executionId}`, err);
|
Logger.error(`Failed saving execution progress to database for execution ID ${this.executionId} (hookFunctionsPreExecute, nodeExecuteAfter)`, { ...err, executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
@ -307,6 +318,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
workflowExecuteBefore: [],
|
workflowExecuteBefore: [],
|
||||||
workflowExecuteAfter: [
|
workflowExecuteAfter: [
|
||||||
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> {
|
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> {
|
||||||
|
Logger.debug(`Executing hook (hookFunctionsSave)`, { executionId: this.executionId, workflowId: this.workflowData.id });
|
||||||
|
|
||||||
// Prune old execution data
|
// Prune old execution data
|
||||||
if (config.get('executions.pruneData')) {
|
if (config.get('executions.pruneData')) {
|
||||||
|
@ -321,8 +333,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
try {
|
try {
|
||||||
await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData);
|
await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: Add proper logging!
|
Logger.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (hookFunctionsSave)`, { executionId: this.executionId, workflowId: this.workflowData.id });
|
||||||
console.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,6 +386,9 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
fullExecutionData.workflowId = this.workflowData.id.toString();
|
fullExecutionData.workflowId = this.workflowData.id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Leave log message before flatten as that operation increased memory usage a lot and the chance of a crash is highest here
|
||||||
|
Logger.debug(`Save execution data to database for execution ID ${this.executionId}`, { executionId: this.executionId, workflowId: this.workflowData.id });
|
||||||
|
|
||||||
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||||
|
|
||||||
// Save the Execution in DB
|
// Save the Execution in DB
|
||||||
|
@ -420,8 +434,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
try {
|
try {
|
||||||
await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData);
|
await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: Add proper logging!
|
Logger.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (workflowExecuteAfter)`, { sessionId: this.sessionId, workflowId: this.workflowData.id });
|
||||||
console.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -606,7 +619,16 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
|
||||||
// This one already contains changes to talk to parent process
|
// This one already contains changes to talk to parent process
|
||||||
// and get executionID from `activeExecutions` running on main process
|
// and get executionID from `activeExecutions` running on main process
|
||||||
additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow;
|
additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow;
|
||||||
additionalDataIntegrated.executionTimeoutTimestamp = additionalData.executionTimeoutTimestamp;
|
|
||||||
|
let subworkflowTimeout = additionalData.executionTimeoutTimestamp;
|
||||||
|
if (workflowData.settings?.executionTimeout !== undefined && workflowData.settings.executionTimeout > 0) {
|
||||||
|
// We might have received a max timeout timestamp from the parent workflow
|
||||||
|
// If we did, then we get the minimum time between the two timeouts
|
||||||
|
// If no timeout was given from the parent, then we use our timeout.
|
||||||
|
subworkflowTimeout = Math.min(additionalData.executionTimeoutTimestamp || Number.MAX_SAFE_INTEGER, Date.now() + (workflowData.settings.executionTimeout as number * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
additionalDataIntegrated.executionTimeoutTimestamp = subworkflowTimeout;
|
||||||
|
|
||||||
|
|
||||||
// Execute the workflow
|
// Execute the workflow
|
||||||
|
|
|
@ -18,8 +18,8 @@ import {
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
IWorkflowCredentials,
|
IWorkflowCredentials,
|
||||||
Workflow,
|
LoggerProxy as Logger,
|
||||||
} from 'n8n-workflow';
|
Workflow,} from 'n8n-workflow';
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
|
||||||
|
|
||||||
if (workflowData === undefined) {
|
if (workflowData === undefined) {
|
||||||
// The error workflow could not be found
|
// The error workflow could not be found
|
||||||
console.error(`ERROR: Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find error workflow "${workflowId}"`);
|
Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find error workflow "${workflowId}"`, { workflowId });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflowStartNode === undefined) {
|
if (workflowStartNode === undefined) {
|
||||||
console.error(`ERROR: Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`);
|
Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData
|
||||||
const workflowRunner = new WorkflowRunner();
|
const workflowRunner = new WorkflowRunner();
|
||||||
await workflowRunner.run(runData);
|
await workflowRunner.run(runData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`ERROR: Calling Error Workflow for "${workflowErrorData.workflow.id}": ${error.message}`);
|
Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`, { workflowId: workflowErrorData.workflow.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,8 +315,7 @@ export async function saveStaticData(workflow: Workflow): Promise <void> {
|
||||||
await saveStaticDataById(workflow.id!, workflow.staticData);
|
await saveStaticDataById(workflow.id!, workflow.staticData);
|
||||||
workflow.staticData.__dataChanged = false;
|
workflow.staticData.__dataChanged = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: Add proper logging!
|
Logger.error(`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${e.message}"`, { workflowId: workflow.id });
|
||||||
console.error(`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
import {
|
import {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
IRun,
|
IRun,
|
||||||
|
LoggerProxy as Logger,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
|
@ -177,20 +178,24 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
// Register the active execution
|
// Register the active execution
|
||||||
const executionId = await this.activeExecutions.add(data, undefined);
|
const executionId = await this.activeExecutions.add(data, undefined);
|
||||||
|
Logger.verbose(`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, {executionId});
|
||||||
|
|
||||||
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true);
|
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true);
|
||||||
|
|
||||||
let workflowExecution: PCancelable<IRun>;
|
let workflowExecution: PCancelable<IRun>;
|
||||||
if (data.executionData !== undefined) {
|
if (data.executionData !== undefined) {
|
||||||
|
Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {executionId});
|
||||||
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode, data.executionData);
|
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode, data.executionData);
|
||||||
workflowExecution = workflowExecute.processRunExecutionData(workflow);
|
workflowExecution = workflowExecute.processRunExecutionData(workflow);
|
||||||
} else if (data.runData === undefined || data.startNodes === undefined || data.startNodes.length === 0 || data.destinationNode === undefined) {
|
} else if (data.runData === undefined || data.startNodes === undefined || data.startNodes.length === 0 || data.destinationNode === undefined) {
|
||||||
|
Logger.debug(`Execution ID ${executionId} will run executing all nodes.`, {executionId});
|
||||||
// Execute all nodes
|
// Execute all nodes
|
||||||
|
|
||||||
// Can execute without webhook so go on
|
// Can execute without webhook so go on
|
||||||
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
|
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
|
||||||
workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode);
|
workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode);
|
||||||
} else {
|
} else {
|
||||||
|
Logger.debug(`Execution ID ${executionId} is a partial execution.`, {executionId});
|
||||||
// Execute only the nodes between start and destination nodes
|
// Execute only the nodes between start and destination nodes
|
||||||
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
|
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
|
||||||
workflowExecution = workflowExecute.runPartialWorkflow(workflow, data.runData, data.startNodes, data.destinationNode);
|
workflowExecution = workflowExecute.runPartialWorkflow(workflow, data.runData, data.startNodes, data.destinationNode);
|
||||||
|
@ -450,6 +455,7 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
// Listen to data from the subprocess
|
// Listen to data from the subprocess
|
||||||
subprocess.on('message', async (message: IProcessMessage) => {
|
subprocess.on('message', async (message: IProcessMessage) => {
|
||||||
|
Logger.debug(`Received child process message of type ${message.type} for execution ID ${executionId}.`, {executionId});
|
||||||
if (message.type === 'start') {
|
if (message.type === 'start') {
|
||||||
// Now that the execution actually started set the timeout again so that does not time out to early.
|
// Now that the execution actually started set the timeout again so that does not time out to early.
|
||||||
startedAt = new Date();
|
startedAt = new Date();
|
||||||
|
@ -491,11 +497,13 @@ export class WorkflowRunner {
|
||||||
// Also get informed when the processes does exit especially when it did crash or timed out
|
// Also get informed when the processes does exit especially when it did crash or timed out
|
||||||
subprocess.on('exit', async (code, signal) => {
|
subprocess.on('exit', async (code, signal) => {
|
||||||
if (signal === 'SIGTERM'){
|
if (signal === 'SIGTERM'){
|
||||||
|
Logger.debug(`Subprocess for execution ID ${executionId} timed out.`, {executionId});
|
||||||
// Execution timed out and its process has been terminated
|
// Execution timed out and its process has been terminated
|
||||||
const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
|
const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
|
||||||
|
|
||||||
this.processError(timeoutError, startedAt, data.executionMode, executionId);
|
this.processError(timeoutError, startedAt, data.executionMode, executionId);
|
||||||
} else if (code !== 0) {
|
} else if (code !== 0) {
|
||||||
|
Logger.debug(`Subprocess for execution ID ${executionId} finished with error code ${code}.`, {executionId});
|
||||||
// Process did exit with error code, so something went wrong.
|
// Process did exit with error code, so something went wrong.
|
||||||
const executionError = new WorkflowOperationError('Workflow execution process did crash for an unknown reason!');
|
const executionError = new WorkflowOperationError('Workflow execution process did crash for an unknown reason!');
|
||||||
|
|
||||||
|
|
|
@ -20,23 +20,29 @@ import {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IExecuteWorkflowInfo,
|
IExecuteWorkflowInfo,
|
||||||
|
ILogger,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
INodeType,
|
INodeType,
|
||||||
INodeTypeData,
|
INodeTypeData,
|
||||||
IRun,
|
IRun,
|
||||||
IRunExecutionData,
|
|
||||||
ITaskData,
|
ITaskData,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
IWorkflowExecuteHooks,
|
IWorkflowExecuteHooks,
|
||||||
|
LoggerProxy,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
WorkflowOperationError,
|
WorkflowOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLogger,
|
||||||
|
} from '../src/Logger';
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
|
||||||
export class WorkflowRunnerProcess {
|
export class WorkflowRunnerProcess {
|
||||||
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
||||||
|
logger: ILogger;
|
||||||
startedAt = new Date();
|
startedAt = new Date();
|
||||||
workflow: Workflow | undefined;
|
workflow: Workflow | undefined;
|
||||||
workflowExecute: WorkflowExecute | undefined;
|
workflowExecute: WorkflowExecute | undefined;
|
||||||
|
@ -57,7 +63,13 @@ export class WorkflowRunnerProcess {
|
||||||
process.on('SIGTERM', WorkflowRunnerProcess.stopProcess);
|
process.on('SIGTERM', WorkflowRunnerProcess.stopProcess);
|
||||||
process.on('SIGINT', WorkflowRunnerProcess.stopProcess);
|
process.on('SIGINT', WorkflowRunnerProcess.stopProcess);
|
||||||
|
|
||||||
|
const logger = this.logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
this.data = inputData;
|
this.data = inputData;
|
||||||
|
|
||||||
|
logger.verbose('Initializing n8n sub-process', { pid: process.pid, workflowId: this.data.workflowData.id });
|
||||||
|
|
||||||
let className: string;
|
let className: string;
|
||||||
let tempNode: INodeType;
|
let tempNode: INodeType;
|
||||||
let filePath: string;
|
let filePath: string;
|
||||||
|
@ -152,6 +164,8 @@ export class WorkflowRunnerProcess {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await sendToParentProcess('finishExecution', { executionId, result });
|
||||||
|
|
||||||
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(result);
|
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(result);
|
||||||
return returnData!.data!.main;
|
return returnData!.data!.main;
|
||||||
};
|
};
|
||||||
|
@ -187,12 +201,7 @@ export class WorkflowRunnerProcess {
|
||||||
parameters,
|
parameters,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// TODO: Add proper logging
|
this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error});
|
||||||
console.error(`There was a problem sending hook: "${hook}"`);
|
|
||||||
console.error('Parameters:');
|
|
||||||
console.error(parameters);
|
|
||||||
console.error('Error:');
|
|
||||||
console.error(error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-core",
|
"name": "n8n-core",
|
||||||
"version": "0.68.0",
|
"version": "0.69.0",
|
||||||
"description": "Core functionality of n8n",
|
"description": "Core functionality of n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
"file-type": "^14.6.2",
|
"file-type": "^14.6.2",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
"n8n-workflow": "~0.56.0",
|
"n8n-workflow": "~0.57.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"p-cancelable": "^2.0.0",
|
"p-cancelable": "^2.0.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
IPollResponse,
|
IPollResponse,
|
||||||
ITriggerResponse,
|
ITriggerResponse,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
|
LoggerProxy as Logger,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowActivateMode,
|
WorkflowActivateMode,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
|
@ -17,6 +18,7 @@ import {
|
||||||
IWorkflowData,
|
IWorkflowData,
|
||||||
} from './';
|
} from './';
|
||||||
|
|
||||||
|
|
||||||
export class ActiveWorkflows {
|
export class ActiveWorkflows {
|
||||||
private workflowData: {
|
private workflowData: {
|
||||||
[key: string]: IWorkflowData;
|
[key: string]: IWorkflowData;
|
||||||
|
@ -163,6 +165,7 @@ export class ActiveWorkflows {
|
||||||
|
|
||||||
// The trigger function to execute when the cron-time got reached
|
// The trigger function to execute when the cron-time got reached
|
||||||
const executeTrigger = async () => {
|
const executeTrigger = async () => {
|
||||||
|
Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, {workflowName: workflow.name, workflowId: workflow.id});
|
||||||
const pollResponse = await workflow.runPoll(node, pollFunctions);
|
const pollResponse = await workflow.runPoll(node, pollFunctions);
|
||||||
|
|
||||||
if (pollResponse !== null) {
|
if (pollResponse !== null) {
|
||||||
|
|
|
@ -51,6 +51,9 @@ import * as requestPromise from 'request-promise-native';
|
||||||
import { createHmac } from 'crypto';
|
import { createHmac } from 'crypto';
|
||||||
import { fromBuffer } from 'file-type';
|
import { fromBuffer } from 'file-type';
|
||||||
import { lookup } from 'mime-types';
|
import { lookup } from 'mime-types';
|
||||||
|
import {
|
||||||
|
LoggerProxy as Logger,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
const requestPromiseWithDefaults = requestPromise.defaults({
|
const requestPromiseWithDefaults = requestPromise.defaults({
|
||||||
timeout: 300000, // 5 minutes
|
timeout: 300000, // 5 minutes
|
||||||
|
@ -188,8 +191,12 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" expired. Should revalidate.`);
|
||||||
|
|
||||||
const newToken = await token.refresh(tokenRefreshOptions);
|
const newToken = await token.refresh(tokenRefreshOptions);
|
||||||
|
|
||||||
|
Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" has been renewed.`);
|
||||||
|
|
||||||
credentials.oauthTokenData = newToken.data;
|
credentials.oauthTokenData = newToken.data;
|
||||||
|
|
||||||
// Find the name of the credentials
|
// Find the name of the credentials
|
||||||
|
@ -201,6 +208,8 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin
|
||||||
// Save the refreshed token
|
// Save the refreshed token
|
||||||
await additionalData.credentialsHelper.updateCredentials(name, credentialsType, credentials);
|
await additionalData.credentialsHelper.updateCredentials(name, credentialsType, credentials);
|
||||||
|
|
||||||
|
Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" has been saved to database successfully.`);
|
||||||
|
|
||||||
// Make the request again with the new token
|
// Make the request again with the new token
|
||||||
const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject);
|
const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject);
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
ITaskDataConnections,
|
ITaskDataConnections,
|
||||||
IWaitingForExecution,
|
IWaitingForExecution,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
|
LoggerProxy as Logger,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
WorkflowOperationError,
|
WorkflowOperationError,
|
||||||
|
@ -400,6 +401,13 @@ export class WorkflowExecute {
|
||||||
|
|
||||||
nodeToAdd = parentNode;
|
nodeToAdd = parentNode;
|
||||||
}
|
}
|
||||||
|
const parentNodesNodeToAdd = workflow.getParentNodes(nodeToAdd as string);
|
||||||
|
if (parentNodesNodeToAdd.includes(parentNodeName) && nodeSuccessData[outputIndex].length === 0) {
|
||||||
|
// We do not add the node if there is no input data and the node that should be connected
|
||||||
|
// is a child of the parent node. Because else it would run a node even though it should be
|
||||||
|
// specifically not run, as it did not receive any data.
|
||||||
|
nodeToAdd = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (nodeToAdd === undefined) {
|
if (nodeToAdd === undefined) {
|
||||||
// No node has to get added so process
|
// No node has to get added so process
|
||||||
|
@ -482,6 +490,8 @@ export class WorkflowExecute {
|
||||||
* @memberof WorkflowExecute
|
* @memberof WorkflowExecute
|
||||||
*/
|
*/
|
||||||
processRunExecutionData(workflow: Workflow): PCancelable<IRun> {
|
processRunExecutionData(workflow: Workflow): PCancelable<IRun> {
|
||||||
|
Logger.verbose('Workflow execution started', { workflowId: workflow.id });
|
||||||
|
|
||||||
const startedAt = new Date();
|
const startedAt = new Date();
|
||||||
|
|
||||||
const workflowIssues = workflow.checkReadyForExecution();
|
const workflowIssues = workflow.checkReadyForExecution();
|
||||||
|
@ -502,7 +512,6 @@ export class WorkflowExecute {
|
||||||
this.runExecutionData.startData = {};
|
this.runExecutionData.startData = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let currentExecutionTry = '';
|
let currentExecutionTry = '';
|
||||||
let lastExecutionTry = '';
|
let lastExecutionTry = '';
|
||||||
|
|
||||||
|
@ -564,6 +573,7 @@ export class WorkflowExecute {
|
||||||
executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
|
executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
|
||||||
executionNode = executionData.node;
|
executionNode = executionData.node;
|
||||||
|
|
||||||
|
Logger.debug(`Start processing node "${executionNode.name}"`, { node: executionNode.name, workflowId: workflow.id });
|
||||||
await this.executeHook('nodeExecuteBefore', [executionNode.name]);
|
await this.executeHook('nodeExecuteBefore', [executionNode.name]);
|
||||||
|
|
||||||
// Get the index of the current run
|
// Get the index of the current run
|
||||||
|
@ -661,7 +671,9 @@ export class WorkflowExecute {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.debug(`Running node "${executionNode.name}" started`, { node: executionNode.name, workflowId: workflow.id });
|
||||||
nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
|
nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
|
||||||
|
Logger.debug(`Running node "${executionNode.name}" finished successfully`, { node: executionNode.name, workflowId: workflow.id });
|
||||||
|
|
||||||
if (nodeSuccessData === undefined) {
|
if (nodeSuccessData === undefined) {
|
||||||
// Node did not get executed
|
// Node did not get executed
|
||||||
|
@ -698,6 +710,8 @@ export class WorkflowExecute {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Logger.debug(`Running node "${executionNode.name}" finished with error`, { node: executionNode.name, workflowId: workflow.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -829,8 +843,10 @@ export class WorkflowExecute {
|
||||||
const fullRunData = this.getFullRunData(startedAt);
|
const fullRunData = this.getFullRunData(startedAt);
|
||||||
|
|
||||||
if (executionError !== undefined) {
|
if (executionError !== undefined) {
|
||||||
|
Logger.verbose(`Workflow execution finished with error`, { error: executionError, workflowId: workflow.id });
|
||||||
fullRunData.data.resultData.error = executionError;
|
fullRunData.data.resultData.error = executionError;
|
||||||
} else {
|
} else {
|
||||||
|
Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id });
|
||||||
fullRunData.finished = true;
|
fullRunData.finished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -465,6 +465,30 @@ class NodeTypesClass implements INodeTypes {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'n8n-nodes-base.noOp': {
|
||||||
|
sourcePath: '',
|
||||||
|
type: {
|
||||||
|
description: {
|
||||||
|
displayName: 'No Operation, do nothing',
|
||||||
|
name: 'noOp',
|
||||||
|
icon: 'fa:arrow-right',
|
||||||
|
group: ['organization'],
|
||||||
|
version: 1,
|
||||||
|
description: 'No Operation',
|
||||||
|
defaults: {
|
||||||
|
name: 'NoOp',
|
||||||
|
color: '#b0b0b0',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
return this.prepareOutputData(items);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
'n8n-nodes-base.set': {
|
'n8n-nodes-base.set': {
|
||||||
sourcePath: '',
|
sourcePath: '',
|
||||||
type: {
|
type: {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IConnections,
|
IConnections,
|
||||||
|
ILogger,
|
||||||
INode,
|
INode,
|
||||||
IRun,
|
IRun,
|
||||||
|
LoggerProxy,
|
||||||
Workflow,
|
Workflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -1152,11 +1154,188 @@ describe('WorkflowExecute', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: 'should not use empty data in sibling if parent did not send any data',
|
||||||
|
input: {
|
||||||
|
// Leave the workflowData in regular JSON to be able to easily
|
||||||
|
// copy it from/in the UI
|
||||||
|
workflowData: {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"name": "Start",
|
||||||
|
"type": "n8n-nodes-base.start",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
250,
|
||||||
|
300,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"values": {
|
||||||
|
"number": [
|
||||||
|
{
|
||||||
|
"name": "value1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"options": {},
|
||||||
|
},
|
||||||
|
"name": "Set",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
450,
|
||||||
|
300,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"name": "Merge",
|
||||||
|
"type": "n8n-nodes-base.merge",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1050,
|
||||||
|
250,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"number": [
|
||||||
|
{
|
||||||
|
"value1": "={{$json[\"value1\"]}}",
|
||||||
|
"operation": "equal",
|
||||||
|
"value2": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"name": "IF",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
650,
|
||||||
|
300,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"name": "NoOpTrue",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
850,
|
||||||
|
150,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"name": "NoOpFalse",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
850,
|
||||||
|
400,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Start": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Set",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"Set": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "IF",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"IF": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "NoOpTrue",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Merge",
|
||||||
|
"type": "main",
|
||||||
|
"index": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "NoOpFalse",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"NoOpTrue": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Merge",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
nodeExecutionOrder: [
|
||||||
|
'Start',
|
||||||
|
'Set',
|
||||||
|
'IF',
|
||||||
|
'NoOpFalse',
|
||||||
|
],
|
||||||
|
nodeData: {
|
||||||
|
IF: [
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
NoOpFalse: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
value1: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const fakeLogger = {
|
||||||
|
log: () => {},
|
||||||
|
debug: () => {},
|
||||||
|
verbose: () => {},
|
||||||
|
info: () => {},
|
||||||
|
warn: () => {},
|
||||||
|
error: () => {},
|
||||||
|
} as ILogger;
|
||||||
|
|
||||||
const executionMode = 'manual';
|
const executionMode = 'manual';
|
||||||
const nodeTypes = Helpers.NodeTypes();
|
const nodeTypes = Helpers.NodeTypes();
|
||||||
|
LoggerProxy.init(fakeLogger);
|
||||||
|
|
||||||
for (const testData of tests) {
|
for (const testData of tests) {
|
||||||
test(testData.description, async () => {
|
test(testData.description, async () => {
|
||||||
|
@ -1201,7 +1380,6 @@ describe('WorkflowExecute', () => {
|
||||||
expect(result.finished).toEqual(true);
|
expect(result.finished).toEqual(true);
|
||||||
expect(result.data.executionData!.contextData).toEqual({});
|
expect(result.data.executionData!.contextData).toEqual({});
|
||||||
expect(result.data.executionData!.nodeExecutionStack).toEqual([]);
|
expect(result.data.executionData!.nodeExecutionStack).toEqual([]);
|
||||||
expect(result.data.executionData!.waitingExecution).toEqual({});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-editor-ui",
|
"name": "n8n-editor-ui",
|
||||||
"version": "0.87.0",
|
"version": "0.88.0",
|
||||||
"description": "Workflow Editor UI for n8n",
|
"description": "Workflow Editor UI for n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.set": "^4.3.2",
|
"lodash.set": "^4.3.2",
|
||||||
"n8n-workflow": "~0.56.0",
|
"n8n-workflow": "~0.57.0",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.12.0",
|
||||||
"normalize-wheel": "^1.0.1",
|
"normalize-wheel": "^1.0.1",
|
||||||
"prismjs": "^1.17.1",
|
"prismjs": "^1.17.1",
|
||||||
|
|
|
@ -435,7 +435,7 @@ export type MenuItemPosition = 'top' | 'bottom';
|
||||||
export interface IMenuItem {
|
export interface IMenuItem {
|
||||||
id: string;
|
id: string;
|
||||||
type: MenuItemType;
|
type: MenuItemType;
|
||||||
position: MenuItemPosition;
|
position?: MenuItemPosition;
|
||||||
properties: ILinkMenuItemProperties;
|
properties: ILinkMenuItemProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
import { restApi } from '@/components/mixins/restApi';
|
import { restApi } from '@/components/mixins/restApi';
|
||||||
import { showMessage } from '@/components/mixins/showMessage';
|
import { showMessage } from '@/components/mixins/showMessage';
|
||||||
import CredentialsInput from '@/components/CredentialsInput.vue';
|
import CredentialsInput from '@/components/CredentialsInput.vue';
|
||||||
|
@ -71,6 +72,7 @@ import { INodeUi } from '../Interface';
|
||||||
export default mixins(
|
export default mixins(
|
||||||
restApi,
|
restApi,
|
||||||
showMessage,
|
showMessage,
|
||||||
|
externalHooks,
|
||||||
).extend({
|
).extend({
|
||||||
name: 'CredentialsEdit',
|
name: 'CredentialsEdit',
|
||||||
props: [
|
props: [
|
||||||
|
@ -195,7 +197,6 @@ export default mixins(
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.credentialData = currentCredentials;
|
this.credentialData = currentCredentials;
|
||||||
} else {
|
} else {
|
||||||
Vue.nextTick(() => {
|
Vue.nextTick(() => {
|
||||||
|
@ -227,6 +228,9 @@ export default mixins(
|
||||||
this.credentialType = null;
|
this.credentialType = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async credentialType (newValue, oldValue) {
|
||||||
|
this.$externalHooks().run('credentialsEdit.credentialTypeChanged', { newValue, oldValue, editCredentials: !!this.editCredentials, credentialType: this.credentialType, setCredentialType: this.setCredentialType });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getCredentialProperties (name: string): INodeProperties[] {
|
getCredentialProperties (name: string): INodeProperties[] {
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
import { restApi } from '@/components/mixins/restApi';
|
import { restApi } from '@/components/mixins/restApi';
|
||||||
import { ICredentialsResponse } from '@/Interface';
|
import { ICredentialsResponse } from '@/Interface';
|
||||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||||
|
@ -47,6 +48,7 @@ import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
|
externalHooks,
|
||||||
genericHelpers,
|
genericHelpers,
|
||||||
nodeHelpers,
|
nodeHelpers,
|
||||||
restApi,
|
restApi,
|
||||||
|
@ -75,6 +77,7 @@ export default mixins(
|
||||||
this.loadCredentials();
|
this.loadCredentials();
|
||||||
this.loadCredentialTypes();
|
this.loadCredentialTypes();
|
||||||
}
|
}
|
||||||
|
this.$externalHooks().run('credentialsList.dialogVisibleChanged', { dialogVisible: newValue });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<div v-if="showDocumentHelp && nodeType" class="text">
|
<div v-if="showDocumentHelp && nodeType" class="text">
|
||||||
Need help? <a id="doc-hyperlink" v-if="showDocumentHelp && nodeType" :href="documentationUrl" target="_blank">Open {{nodeType.displayName}} documentation</a>
|
Need help? <a id="doc-hyperlink" v-if="showDocumentHelp && nodeType" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open {{nodeType.displayName}} documentation</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
@ -49,10 +49,16 @@ import {
|
||||||
IUpdateInformation,
|
IUpdateInformation,
|
||||||
} from '../Interface';
|
} from '../Interface';
|
||||||
|
|
||||||
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||||
|
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
|
|
||||||
import NodeSettings from '@/components/NodeSettings.vue';
|
import NodeSettings from '@/components/NodeSettings.vue';
|
||||||
import RunData from '@/components/RunData.vue';
|
import RunData from '@/components/RunData.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
|
export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
name: 'DataDisplay',
|
name: 'DataDisplay',
|
||||||
components: {
|
components: {
|
||||||
NodeSettings,
|
NodeSettings,
|
||||||
|
@ -88,6 +94,13 @@ export default Vue.extend({
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
node (node, oldNode) {
|
||||||
|
if(node && !oldNode) {
|
||||||
|
this.$externalHooks().run('dataDisplay.nodeTypeChanged', { nodeSubtitle: this.getNodeSubtitle(node, this.nodeType, this.getWorkflow()) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
valueChanged (parameterData: IUpdateInformation) {
|
valueChanged (parameterData: IUpdateInformation) {
|
||||||
this.$emit('valueChanged', parameterData);
|
this.$emit('valueChanged', parameterData);
|
||||||
|
@ -102,6 +115,9 @@ export default Vue.extend({
|
||||||
this.$store.commit('setActiveNode', null);
|
this.$store.commit('setActiveNode', null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onDocumentationUrlClick () {
|
||||||
|
this.$externalHooks().run('dataDisplay.onDocumentationUrlClick', { nodeType: this.nodeType, documentationUrl: this.documentationUrl });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -158,6 +158,7 @@ import Vue from 'vue';
|
||||||
import ExecutionTime from '@/components/ExecutionTime.vue';
|
import ExecutionTime from '@/components/ExecutionTime.vue';
|
||||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||||
|
|
||||||
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
import { restApi } from '@/components/mixins/restApi';
|
import { restApi } from '@/components/mixins/restApi';
|
||||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||||
import { showMessage } from '@/components/mixins/showMessage';
|
import { showMessage } from '@/components/mixins/showMessage';
|
||||||
|
@ -182,6 +183,7 @@ import {
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
|
externalHooks,
|
||||||
genericHelpers,
|
genericHelpers,
|
||||||
restApi,
|
restApi,
|
||||||
showMessage,
|
showMessage,
|
||||||
|
@ -558,6 +560,8 @@ export default mixins(
|
||||||
await this.loadWorkflows();
|
await this.loadWorkflows();
|
||||||
await this.refreshData();
|
await this.refreshData();
|
||||||
this.handleAutoRefreshToggle();
|
this.handleAutoRefreshToggle();
|
||||||
|
|
||||||
|
this.$externalHooks().run('executionsList.openDialog');
|
||||||
},
|
},
|
||||||
async retryExecution (execution: IExecutionShortResponse, loadWorkflow?: boolean) {
|
async retryExecution (execution: IExecutionShortResponse, loadWorkflow?: boolean) {
|
||||||
this.isDataLoading = true;
|
this.isDataLoading = true;
|
||||||
|
|
|
@ -21,21 +21,7 @@
|
||||||
</a>
|
</a>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<el-menu-item
|
<MenuItemsIterator :items="sidebarMenuTopItems" :root="true"/>
|
||||||
v-for="item in sidebarMenuTopItems"
|
|
||||||
:key="item.id"
|
|
||||||
:index="item.id"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
v-if="item.type === 'link'"
|
|
||||||
:href="item.properties.href"
|
|
||||||
:target="item.properties.newWindow ? '_blank' : '_self'"
|
|
||||||
class="primary-item"
|
|
||||||
>
|
|
||||||
<font-awesome-icon :icon="item.properties.icon" />
|
|
||||||
<span slot="title" class="item-title-root">{{ item.properties.title }}</span>
|
|
||||||
</a>
|
|
||||||
</el-menu-item>
|
|
||||||
|
|
||||||
<el-submenu index="workflow" title="Workflow">
|
<el-submenu index="workflow" title="Workflow">
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
|
@ -136,30 +122,8 @@
|
||||||
<span slot="title" class="item-title-root">Help</span>
|
<span slot="title" class="item-title-root">Help</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-menu-item index="help-documentation">
|
<MenuItemsIterator :items="helpMenuItems" />
|
||||||
<template slot="title">
|
|
||||||
<a href="https://docs.n8n.io" target="_blank">
|
|
||||||
<font-awesome-icon icon="book"/>
|
|
||||||
<span slot="title" class="item-title">Documentation</span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="help-forum">
|
|
||||||
<template slot="title">
|
|
||||||
<a href="https://community.n8n.io" target="_blank">
|
|
||||||
<font-awesome-icon icon="users"/>
|
|
||||||
<span slot="title" class="item-title">Forum</span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="help-examples">
|
|
||||||
<template slot="title">
|
|
||||||
<a href="https://n8n.io/workflows" target="_blank">
|
|
||||||
<font-awesome-icon icon="network-wired"/>
|
|
||||||
<span slot="title" class="item-title">Workflows</span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="help-about">
|
<el-menu-item index="help-about">
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<font-awesome-icon class="about-icon" icon="info"/>
|
<font-awesome-icon class="about-icon" icon="info"/>
|
||||||
|
@ -168,21 +132,7 @@
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-submenu>
|
</el-submenu>
|
||||||
|
|
||||||
<el-menu-item
|
<MenuItemsIterator :items="sidebarMenuBottomItems" :root="true"/>
|
||||||
v-for="item in sidebarMenuBottomItems"
|
|
||||||
:key="item.id"
|
|
||||||
:index="item.id"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
v-if="item.type === 'link'"
|
|
||||||
:href="item.properties.href"
|
|
||||||
:target="item.properties.newWindow ? '_blank' : '_self'"
|
|
||||||
class="primary-item"
|
|
||||||
>
|
|
||||||
<font-awesome-icon :icon="item.properties.icon" />
|
|
||||||
<span slot="title" class="item-title-root">{{ item.properties.title }}</span>
|
|
||||||
</a>
|
|
||||||
</el-menu-item>
|
|
||||||
|
|
||||||
</el-menu>
|
</el-menu>
|
||||||
|
|
||||||
|
@ -192,6 +142,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||||
|
|
||||||
|
@ -219,6 +170,40 @@ import { workflowRun } from '@/components/mixins/workflowRun';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
import MenuItemsIterator from './MainSidebarMenuItemsIterator.vue';
|
||||||
|
|
||||||
|
const helpMenuItems: IMenuItem[] = [
|
||||||
|
{
|
||||||
|
id: 'docs',
|
||||||
|
type: 'link',
|
||||||
|
properties: {
|
||||||
|
href: 'https://docs.n8n.io',
|
||||||
|
title: 'Documentation',
|
||||||
|
icon: 'book',
|
||||||
|
newWindow: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'forum',
|
||||||
|
type: 'link',
|
||||||
|
properties: {
|
||||||
|
href: 'https://community.n8n.io',
|
||||||
|
title: 'Forum',
|
||||||
|
icon: 'users',
|
||||||
|
newWindow: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'examples',
|
||||||
|
type: 'link',
|
||||||
|
properties: {
|
||||||
|
href: 'https://n8n.io/workflows',
|
||||||
|
title: 'Workflows',
|
||||||
|
icon: 'network-wired',
|
||||||
|
newWindow: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
genericHelpers,
|
genericHelpers,
|
||||||
|
@ -237,6 +222,7 @@ export default mixins(
|
||||||
ExecutionsList,
|
ExecutionsList,
|
||||||
WorkflowOpen,
|
WorkflowOpen,
|
||||||
WorkflowSettings,
|
WorkflowSettings,
|
||||||
|
MenuItemsIterator,
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -250,6 +236,7 @@ export default mixins(
|
||||||
stopExecutionInProgress: false,
|
stopExecutionInProgress: false,
|
||||||
workflowOpenDialogVisible: false,
|
workflowOpenDialogVisible: false,
|
||||||
workflowSettingsDialogVisible: false,
|
workflowSettingsDialogVisible: false,
|
||||||
|
helpMenuItems,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-menu-item
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
:index="item.id"
|
||||||
|
@click="onClick(item)"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="item.properties.icon" />
|
||||||
|
<span slot="title" :class="{'item-title-root': root, 'item-title': !root}">{{ item.properties.title }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import { IMenuItem } from '../Interface';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'MenuItemsIterator',
|
||||||
|
props: [
|
||||||
|
'items',
|
||||||
|
'root',
|
||||||
|
],
|
||||||
|
methods: {
|
||||||
|
onClick(item: IMenuItem) {
|
||||||
|
if (item && item.type === 'link' && item.properties) {
|
||||||
|
const href = item.properties.href;
|
||||||
|
if (!href) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.properties.newWindow) {
|
||||||
|
window.open(href);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.location.assign(item.properties.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -47,14 +47,11 @@
|
||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { nodeBase } from '@/components/mixins/nodeBase';
|
import { nodeBase } from '@/components/mixins/nodeBase';
|
||||||
|
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
INode,
|
|
||||||
INodeIssueObjectProperty,
|
|
||||||
INodePropertyOptions,
|
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
ITaskData,
|
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -62,7 +59,7 @@ import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
export default mixins(nodeBase, workflowHelpers).extend({
|
export default mixins(nodeBase, nodeHelpers, workflowHelpers).extend({
|
||||||
name: 'Node',
|
name: 'Node',
|
||||||
components: {
|
components: {
|
||||||
NodeIcon,
|
NodeIcon,
|
||||||
|
@ -133,41 +130,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
nodeSubtitle (): string | undefined {
|
nodeSubtitle (): string | undefined {
|
||||||
if (this.data.notesInFlow) {
|
return this.getNodeSubtitle(this.data, this.nodeType, this.workflow);
|
||||||
return this.data.notes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.nodeType !== null && this.nodeType.subtitle !== undefined) {
|
|
||||||
return this.workflow.expression.getSimpleParameterValue(this.data as INode, this.nodeType.subtitle, 'internal') as string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.data.parameters.operation !== undefined) {
|
|
||||||
const operation = this.data.parameters.operation as string;
|
|
||||||
if (this.nodeType === null) {
|
|
||||||
return operation;
|
|
||||||
}
|
|
||||||
|
|
||||||
const operationData = this.nodeType.properties.find((property) => {
|
|
||||||
return property.name === 'operation';
|
|
||||||
});
|
|
||||||
if (operationData === undefined) {
|
|
||||||
return operation;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (operationData.options === undefined) {
|
|
||||||
return operation;
|
|
||||||
}
|
|
||||||
|
|
||||||
const optionData = operationData.options.find((option) => {
|
|
||||||
return (option as INodePropertyOptions).value === this.data.parameters.operation;
|
|
||||||
});
|
|
||||||
if (optionData === undefined) {
|
|
||||||
return operation;
|
|
||||||
}
|
|
||||||
|
|
||||||
return optionData.name;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
},
|
||||||
workflowRunning (): boolean {
|
workflowRunning (): boolean {
|
||||||
return this.$store.getters.isActionActive('workflowRunning');
|
return this.$store.getters.isActionActive('workflowRunning');
|
||||||
|
@ -186,7 +149,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
|
||||||
this.disableNodes([this.data]);
|
this.disableNodes([this.data]);
|
||||||
},
|
},
|
||||||
executeNode () {
|
executeNode () {
|
||||||
this.$emit('runWorkflow', this.data.name);
|
this.$emit('runWorkflow', this.data.name, 'Node.executeNode');
|
||||||
},
|
},
|
||||||
deleteNode () {
|
deleteNode () {
|
||||||
Vue.nextTick(() => {
|
Vue.nextTick(() => {
|
||||||
|
|
|
@ -24,10 +24,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import { externalHooks } from "@/components/mixins/externalHooks";
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
import NodeCreateItem from '@/components/NodeCreateItem.vue';
|
import NodeCreateItem from '@/components/NodeCreateItem.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
import mixins from "vue-typed-mixins";
|
||||||
|
|
||||||
|
export default mixins(externalHooks).extend({
|
||||||
name: 'NodeCreateList',
|
name: 'NodeCreateList',
|
||||||
components: {
|
components: {
|
||||||
NodeCreateItem,
|
NodeCreateItem,
|
||||||
|
@ -70,13 +73,18 @@ export default Vue.extend({
|
||||||
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
|
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$externalHooks().run('nodeCreateList.filteredNodeTypesComputed', { nodeFilter: this.nodeFilter, result: returnData, selectedType: this.selectedType });
|
||||||
return returnData;
|
return returnData;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
nodeFilter (newVal, oldVal) {
|
nodeFilter (newValue, oldValue) {
|
||||||
// Reset the index whenver the filter-value changes
|
// Reset the index whenver the filter-value changes
|
||||||
this.activeNodeTypeIndex = 0;
|
this.activeNodeTypeIndex = 0;
|
||||||
|
this.$externalHooks().run('nodeCreateList.nodeFilterChanged', { oldValue, newValue, selectedType: this.selectedType, filteredNodes: this.filteredNodeTypes });
|
||||||
|
},
|
||||||
|
selectedType (newValue, oldValue) {
|
||||||
|
this.$externalHooks().run('nodeCreateList.selectedTypeChanged', { oldValue, newValue });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -105,6 +113,12 @@ export default Vue.extend({
|
||||||
this.$emit('nodeTypeSelected', nodeTypeName);
|
this.$emit('nodeTypeSelected', nodeTypeName);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.$externalHooks().run('nodeCreateList.mounted');
|
||||||
|
},
|
||||||
|
async destroyed() {
|
||||||
|
this.$externalHooks().run('nodeCreateList.destroyed');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<el-button
|
<el-button
|
||||||
v-if="node && !isReadOnly"
|
v-if="node && !isReadOnly"
|
||||||
:disabled="workflowRunning"
|
:disabled="workflowRunning"
|
||||||
@click.stop="runWorkflow(node.name)"
|
@click.stop="runWorkflow(node.name, 'RunData.ExecuteNodeButton')"
|
||||||
class="execute-node-button"
|
class="execute-node-button"
|
||||||
:title="`Executes this ${node.name} node after executing any previous nodes that have not yet returned data`"
|
:title="`Executes this ${node.name} node after executing any previous nodes that have not yet returned data`"
|
||||||
>
|
>
|
||||||
|
@ -228,6 +228,7 @@ import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
|
||||||
import NodeErrorView from '@/components/Error/NodeViewError.vue';
|
import NodeErrorView from '@/components/Error/NodeViewError.vue';
|
||||||
|
|
||||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||||
|
import { externalHooks } from "@/components/mixins/externalHooks";
|
||||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||||
import { workflowRun } from '@/components/mixins/workflowRun';
|
import { workflowRun } from '@/components/mixins/workflowRun';
|
||||||
|
@ -239,6 +240,7 @@ const deselectedPlaceholder = '_!^&*';
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
copyPaste,
|
copyPaste,
|
||||||
|
externalHooks,
|
||||||
genericHelpers,
|
genericHelpers,
|
||||||
nodeHelpers,
|
nodeHelpers,
|
||||||
workflowRun,
|
workflowRun,
|
||||||
|
@ -617,8 +619,9 @@ export default mixins(
|
||||||
jsonData () {
|
jsonData () {
|
||||||
this.refreshDataSize();
|
this.refreshDataSize();
|
||||||
},
|
},
|
||||||
displayMode () {
|
displayMode (newValue, oldValue) {
|
||||||
this.closeBinaryDataDisplay();
|
this.closeBinaryDataDisplay();
|
||||||
|
this.$externalHooks().run('runData.displayModeChanged', { newValue, oldValue });
|
||||||
},
|
},
|
||||||
maxRunIndex () {
|
maxRunIndex () {
|
||||||
this.runIndex = Math.min(this.runIndex, this.maxRunIndex);
|
this.runIndex = Math.min(this.runIndex, this.maxRunIndex);
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
import { restApi } from '@/components/mixins/restApi';
|
import { restApi } from '@/components/mixins/restApi';
|
||||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||||
import { showMessage } from '@/components/mixins/showMessage';
|
import { showMessage } from '@/components/mixins/showMessage';
|
||||||
|
@ -180,6 +181,7 @@ import {
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
|
externalHooks,
|
||||||
genericHelpers,
|
genericHelpers,
|
||||||
restApi,
|
restApi,
|
||||||
showMessage,
|
showMessage,
|
||||||
|
@ -225,6 +227,7 @@ export default mixins(
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
this.openDialog();
|
this.openDialog();
|
||||||
}
|
}
|
||||||
|
this.$externalHooks().run('workflowSettings.dialogVisibleChanged', { dialogVisible: newValue });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -456,6 +459,8 @@ export default mixins(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldSettings = JSON.parse(JSON.stringify(this.$store.getters.workflowSettings));
|
||||||
|
|
||||||
this.$store.commit('setWorkflowSettings', localWorkflowSettings);
|
this.$store.commit('setWorkflowSettings', localWorkflowSettings);
|
||||||
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
@ -467,6 +472,8 @@ export default mixins(
|
||||||
});
|
});
|
||||||
|
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
|
|
||||||
|
this.$externalHooks().run('workflowSettings.saveSettings', { oldSettings });
|
||||||
},
|
},
|
||||||
toggleTimeout() {
|
toggleTimeout() {
|
||||||
this.workflowSettings.executionTimeout = this.workflowSettings.executionTimeout === -1 ? 0 : -1;
|
this.workflowSettings.executionTimeout = this.workflowSettings.executionTimeout === -1 ? 0 : -1;
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const genericHelpers = mixins(showMessage).extend({
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
startLoading () {
|
startLoading (text?: string) {
|
||||||
if (this.loadingService !== null) {
|
if (this.loadingService !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ export const genericHelpers = mixins(showMessage).extend({
|
||||||
this.loadingService = this.$loading(
|
this.loadingService = this.$loading(
|
||||||
{
|
{
|
||||||
lock: true,
|
lock: true,
|
||||||
text: 'Loading',
|
text: text || 'Loading',
|
||||||
spinner: 'el-icon-loading',
|
spinner: 'el-icon-loading',
|
||||||
background: 'rgba(255, 255, 255, 0.8)',
|
background: 'rgba(255, 255, 255, 0.8)',
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
IRunData,
|
IRunData,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
ITaskDataConnections,
|
ITaskDataConnections,
|
||||||
|
INode,
|
||||||
|
INodePropertyOptions,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -321,5 +323,43 @@ export const nodeHelpers = mixins(
|
||||||
this.updateNodeCredentialIssues(node);
|
this.updateNodeCredentialIssues(node);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
getNodeSubtitle (data, nodeType, workflow): string | undefined {
|
||||||
|
if (data.notesInFlow) {
|
||||||
|
return data.notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeType !== null && nodeType.subtitle !== undefined) {
|
||||||
|
return workflow.expression.getSimpleParameterValue(data as INode, nodeType.subtitle, 'internal') as string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.parameters.operation !== undefined) {
|
||||||
|
const operation = data.parameters.operation as string;
|
||||||
|
if (nodeType === null) {
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const operationData:INodeProperties = nodeType.properties.find((property: INodeProperties) => {
|
||||||
|
return property.name === 'operation';
|
||||||
|
});
|
||||||
|
if (operationData === undefined) {
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operationData.options === undefined) {
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionData = operationData.options.find((option) => {
|
||||||
|
return (option as INodePropertyOptions).value === data.parameters.operation;
|
||||||
|
});
|
||||||
|
if (optionData === undefined) {
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionData.name;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
IPushDataTestWebhook,
|
IPushDataTestWebhook,
|
||||||
} from '../../Interface';
|
} from '../../Interface';
|
||||||
|
|
||||||
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||||
import { showMessage } from '@/components/mixins/showMessage';
|
import { showMessage } from '@/components/mixins/showMessage';
|
||||||
import { titleChange } from '@/components/mixins/titleChange';
|
import { titleChange } from '@/components/mixins/titleChange';
|
||||||
|
@ -15,6 +16,7 @@ import { titleChange } from '@/components/mixins/titleChange';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
export const pushConnection = mixins(
|
export const pushConnection = mixins(
|
||||||
|
externalHooks,
|
||||||
nodeHelpers,
|
nodeHelpers,
|
||||||
showMessage,
|
showMessage,
|
||||||
titleChange,
|
titleChange,
|
||||||
|
@ -202,6 +204,7 @@ export const pushConnection = mixins(
|
||||||
|
|
||||||
const runDataExecuted = pushData.data;
|
const runDataExecuted = pushData.data;
|
||||||
|
|
||||||
|
let runDataExecutedErrorMessage;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const workflow = this.getWorkflow();
|
const workflow = this.getWorkflow();
|
||||||
if (runDataExecuted.finished !== true) {
|
if (runDataExecuted.finished !== true) {
|
||||||
|
@ -221,6 +224,9 @@ export const pushConnection = mixins(
|
||||||
: runDataExecuted.data.resultData.error.message;
|
: runDataExecuted.data.resultData.error.message;
|
||||||
errorMessage = `There was a problem executing the workflow:<br /><strong>"${receivedError}"</strong>`;
|
errorMessage = `There was a problem executing the workflow:<br /><strong>"${receivedError}"</strong>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runDataExecutedErrorMessage = errorMessage;
|
||||||
|
|
||||||
this.$titleSet(workflow.name, 'ERROR');
|
this.$titleSet(workflow.name, 'ERROR');
|
||||||
this.$showMessage({
|
this.$showMessage({
|
||||||
title: 'Problem executing workflow',
|
title: 'Problem executing workflow',
|
||||||
|
@ -249,6 +255,20 @@ export const pushConnection = mixins(
|
||||||
// Set the node execution issues on all the nodes which produced an error so that
|
// Set the node execution issues on all the nodes which produced an error so that
|
||||||
// it can be displayed in the node-view
|
// it can be displayed in the node-view
|
||||||
this.updateNodesExecutionIssues();
|
this.updateNodesExecutionIssues();
|
||||||
|
|
||||||
|
let itemsCount = 0;
|
||||||
|
if(runDataExecuted.data.resultData.lastNodeExecuted && !runDataExecutedErrorMessage) {
|
||||||
|
itemsCount = runDataExecuted.data.resultData.runData[runDataExecuted.data.resultData.lastNodeExecuted][0].data!.main[0]!.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$externalHooks().run('pushConnection.executionFinished', {
|
||||||
|
itemsCount,
|
||||||
|
nodeName: runDataExecuted.data.resultData.lastNodeExecuted,
|
||||||
|
errorMessage: runDataExecutedErrorMessage,
|
||||||
|
runDataExecutedStartData: runDataExecuted.data.startData,
|
||||||
|
resultDataError: runDataExecuted.data.resultData.error,
|
||||||
|
});
|
||||||
|
|
||||||
} else if (receivedData.type === 'executionStarted') {
|
} else if (receivedData.type === 'executionStarted') {
|
||||||
const pushData = receivedData.data as IPushDataExecutionStarted;
|
const pushData = receivedData.data as IPushDataExecutionStarted;
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,13 @@ import Vue from 'vue';
|
||||||
|
|
||||||
import { Notification } from 'element-ui';
|
import { Notification } from 'element-ui';
|
||||||
import { ElNotificationOptions } from 'element-ui/types/notification';
|
import { ElNotificationOptions } from 'element-ui/types/notification';
|
||||||
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
|
||||||
|
|
||||||
// export const showMessage = {
|
// export const showMessage = {
|
||||||
export const showMessage = Vue.extend({
|
export const showMessage = mixins(externalHooks).extend({
|
||||||
methods: {
|
methods: {
|
||||||
$showMessage (messageData: ElNotificationOptions) {
|
$showMessage (messageData: ElNotificationOptions) {
|
||||||
messageData.dangerouslyUseHTMLString = true;
|
messageData.dangerouslyUseHTMLString = true;
|
||||||
|
@ -21,6 +25,7 @@ export const showMessage = Vue.extend({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
});
|
});
|
||||||
|
this.$externalHooks().run('showMessage.showError', { title, message, errorMessage: error.message });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -53,7 +53,7 @@ export const workflowRun = mixins(
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
async runWorkflow (nodeName: string): Promise<IExecutionPushResponse | undefined> {
|
async runWorkflow (nodeName: string, source?: string): Promise<IExecutionPushResponse | undefined> {
|
||||||
if (this.$store.getters.isActionActive('workflowRunning') === true) {
|
if (this.$store.getters.isActionActive('workflowRunning') === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ export const workflowRun = mixins(
|
||||||
duration: 0,
|
duration: 0,
|
||||||
});
|
});
|
||||||
this.$titleSet(workflow.name as string, 'ERROR');
|
this.$titleSet(workflow.name as string, 'ERROR');
|
||||||
this.$externalHooks().run('workflow.runError', { errorMessages });
|
this.$externalHooks().run('workflowRun.runError', { errorMessages, nodeName });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,7 +172,11 @@ export const workflowRun = mixins(
|
||||||
};
|
};
|
||||||
this.$store.commit('setWorkflowExecutionData', executionData);
|
this.$store.commit('setWorkflowExecutionData', executionData);
|
||||||
|
|
||||||
return await this.runWorkflowApi(startRunData);
|
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);
|
||||||
|
|
||||||
|
this.$externalHooks().run('workflowRun.runWorkflow', { nodeName, source });
|
||||||
|
|
||||||
|
return runWorkflowApiResponse;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$titleSet(workflow.name as string, 'ERROR');
|
this.$titleSet(workflow.name as string, 'ERROR');
|
||||||
this.$showError(error, 'Problem running workflow', 'There was a problem running the workflow:');
|
this.$showError(error, 'Problem running workflow', 'There was a problem running the workflow:');
|
||||||
|
|
|
@ -198,7 +198,7 @@ export default mixins(
|
||||||
this.createNodeActive = false;
|
this.createNodeActive = false;
|
||||||
},
|
},
|
||||||
nodes: {
|
nodes: {
|
||||||
async handler (val, oldVal) {
|
async handler (value, oldValue) {
|
||||||
// Load a workflow
|
// Load a workflow
|
||||||
let workflowId = null as string | null;
|
let workflowId = null as string | null;
|
||||||
if (this.$route && this.$route.params.name) {
|
if (this.$route && this.$route.params.name) {
|
||||||
|
@ -208,7 +208,7 @@ export default mixins(
|
||||||
deep: true,
|
deep: true,
|
||||||
},
|
},
|
||||||
connections: {
|
connections: {
|
||||||
async handler (val, oldVal) {
|
async handler (value, oldValue) {
|
||||||
// Load a workflow
|
// Load a workflow
|
||||||
let workflowId = null as string | null;
|
let workflowId = null as string | null;
|
||||||
if (this.$route && this.$route.params.name) {
|
if (this.$route && this.$route.params.name) {
|
||||||
|
@ -332,6 +332,7 @@ export default mixins(
|
||||||
},
|
},
|
||||||
openNodeCreator () {
|
openNodeCreator () {
|
||||||
this.createNodeActive = true;
|
this.createNodeActive = true;
|
||||||
|
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'add_node_button' });
|
||||||
},
|
},
|
||||||
async openExecution (executionId: string) {
|
async openExecution (executionId: string) {
|
||||||
this.resetWorkspace();
|
this.resetWorkspace();
|
||||||
|
@ -354,6 +355,8 @@ export default mixins(
|
||||||
this.$store.commit('setWorkflowExecutionData', data);
|
this.$store.commit('setWorkflowExecutionData', data);
|
||||||
|
|
||||||
await this.addNodes(JSON.parse(JSON.stringify(data.workflowData.nodes)), JSON.parse(JSON.stringify(data.workflowData.connections)));
|
await this.addNodes(JSON.parse(JSON.stringify(data.workflowData.nodes)), JSON.parse(JSON.stringify(data.workflowData.connections)));
|
||||||
|
|
||||||
|
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
|
||||||
},
|
},
|
||||||
async openWorkflow (workflowId: string) {
|
async openWorkflow (workflowId: string) {
|
||||||
this.resetWorkspace();
|
this.resetWorkspace();
|
||||||
|
@ -1052,6 +1055,8 @@ export default mixins(
|
||||||
|
|
||||||
this.$store.commit('setStateDirty', true);
|
this.$store.commit('setStateDirty', true);
|
||||||
|
|
||||||
|
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
|
||||||
|
|
||||||
// Automatically deselect all nodes and select the current one and also active
|
// Automatically deselect all nodes and select the current one and also active
|
||||||
// current node
|
// current node
|
||||||
this.deselectAllNodes();
|
this.deselectAllNodes();
|
||||||
|
@ -1174,6 +1179,7 @@ export default mixins(
|
||||||
|
|
||||||
// Display the node-creator
|
// Display the node-creator
|
||||||
this.createNodeActive = true;
|
this.createNodeActive = true;
|
||||||
|
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'node_connection_drop' });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.instance.bind('connection', (info: OnConnectionBindInfo) => {
|
this.instance.bind('connection', (info: OnConnectionBindInfo) => {
|
||||||
|
@ -1453,6 +1459,8 @@ export default mixins(
|
||||||
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
||||||
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
|
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
|
||||||
} else {
|
} else {
|
||||||
|
this.startLoading('Redirecting');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
"@oclif/dev-cli": "^1.22.2",
|
"@oclif/dev-cli": "^1.22.2",
|
||||||
"@types/copyfiles": "^2.1.1",
|
"@types/copyfiles": "^2.1.1",
|
||||||
"@types/inquirer": "^6.5.0",
|
"@types/inquirer": "^6.5.0",
|
||||||
"@types/tmp": "^0.1.0",
|
"@types/tmp": "^0.2.0",
|
||||||
"@types/vorpal": "^1.11.0",
|
"@types/vorpal": "^1.11.0",
|
||||||
"tslint": "^6.1.2"
|
"tslint": "^6.1.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import { ChildProcess, spawn } from 'child_process';
|
import { ChildProcess, spawn } from 'child_process';
|
||||||
const copyfiles = require('copyfiles');
|
const copyfiles = require('copyfiles');
|
||||||
|
|
||||||
import {
|
import {
|
||||||
readFile as fsReadFile,
|
readFile as fsReadFile,
|
||||||
|
} from 'fs/promises';
|
||||||
|
import {
|
||||||
write as fsWrite,
|
write as fsWrite,
|
||||||
} from 'fs';
|
} from 'fs';
|
||||||
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { file } from 'tmp-promise';
|
import { file } from 'tmp-promise';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
@ -32,7 +36,7 @@ export async function createCustomTsconfig () {
|
||||||
const tsconfigPath = join(__dirname, '../../src/tsconfig-build.json');
|
const tsconfigPath = join(__dirname, '../../src/tsconfig-build.json');
|
||||||
|
|
||||||
// Read the tsconfi file
|
// Read the tsconfi file
|
||||||
const tsConfigString = await fsReadFileAsync(tsconfigPath, { encoding: 'utf8'}) as string;
|
const tsConfigString = await fsReadFile(tsconfigPath, { encoding: 'utf8'}) as string;
|
||||||
const tsConfig = JSON.parse(tsConfigString);
|
const tsConfig = JSON.parse(tsConfigString);
|
||||||
|
|
||||||
// Set absolute include paths
|
// Set absolute include paths
|
||||||
|
|
|
@ -20,13 +20,51 @@ export class ERPNextApi implements ICredentialType {
|
||||||
type: 'string' as NodePropertyTypes,
|
type: 'string' as NodePropertyTypes,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Environment',
|
||||||
|
name: 'environment',
|
||||||
|
type: 'options' as NodePropertyTypes,
|
||||||
|
default: 'cloudHosted',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Cloud-hosted',
|
||||||
|
value: 'cloudHosted',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Self-hosted',
|
||||||
|
value: 'selfHosted',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Subdomain',
|
displayName: 'Subdomain',
|
||||||
name: 'subdomain',
|
name: 'subdomain',
|
||||||
type: 'string' as NodePropertyTypes,
|
type: 'string' as NodePropertyTypes,
|
||||||
default: '',
|
default: '',
|
||||||
placeholder: 'n8n',
|
placeholder: 'n8n',
|
||||||
description: 'ERPNext subdomain. For instance, entering n8n will make the url look like: https://n8n.erpnext.com/.',
|
description: 'Subdomain of cloud-hosted ERPNext instance. For example, "n8n" is the subdomain in: <code>https://n8n.erpnext.com</code>',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
environment: [
|
||||||
|
'cloudHosted',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Domain',
|
||||||
|
name: 'domain',
|
||||||
|
type: 'string' as NodePropertyTypes,
|
||||||
|
default: '',
|
||||||
|
placeholder: 'https://www.mydomain.com',
|
||||||
|
description: 'Fully qualified domain name of self-hosted ERPNext instance.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
environment: [
|
||||||
|
'selfHosted',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
18
packages/nodes-base/credentials/KitemakerApi.credentials.ts
Normal file
18
packages/nodes-base/credentials/KitemakerApi.credentials.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {
|
||||||
|
ICredentialType,
|
||||||
|
NodePropertyTypes,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class KitemakerApi implements ICredentialType {
|
||||||
|
name = 'kitemakerApi';
|
||||||
|
displayName = 'Kitemaker API';
|
||||||
|
documentationUrl = 'kitemaker';
|
||||||
|
properties = [
|
||||||
|
{
|
||||||
|
displayName: 'Personal Access Token',
|
||||||
|
name: 'personalAccessToken',
|
||||||
|
type: 'string' as NodePropertyTypes,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
|
@ -3,15 +3,11 @@ import {
|
||||||
NodePropertyTypes,
|
NodePropertyTypes,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
|
||||||
export class Mqtt implements ICredentialType {
|
export class Mqtt implements ICredentialType {
|
||||||
name = 'mqtt';
|
name = 'mqtt';
|
||||||
displayName = 'MQTT';
|
displayName = 'MQTT';
|
||||||
documentationUrl = 'mqtt';
|
documentationUrl = 'mqtt';
|
||||||
properties = [
|
properties = [
|
||||||
// The credentials to get from user and save encrypted.
|
|
||||||
// Properties can be defined exactly in the same way
|
|
||||||
// as node properties.
|
|
||||||
{
|
{
|
||||||
displayName: 'Protocol',
|
displayName: 'Protocol',
|
||||||
name: 'protocol',
|
name: 'protocol',
|
||||||
|
@ -55,5 +51,19 @@ export class Mqtt implements ICredentialType {
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Clean Session',
|
||||||
|
name: 'clean',
|
||||||
|
type: 'boolean' as NodePropertyTypes,
|
||||||
|
default: true,
|
||||||
|
description: `Set to false to receive QoS 1 and 2 messages while offline.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Client ID',
|
||||||
|
name: 'clientId',
|
||||||
|
type: 'string' as NodePropertyTypes,
|
||||||
|
default: '',
|
||||||
|
description: 'Client ID. If left empty, one is autogenrated for you',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,9 @@ import {
|
||||||
const scopes = [
|
const scopes = [
|
||||||
'attachments:write',
|
'attachments:write',
|
||||||
'channels:remove',
|
'channels:remove',
|
||||||
|
'comments:remove',
|
||||||
'messages:remove',
|
'messages:remove',
|
||||||
|
'threads:remove',
|
||||||
'workspaces:read',
|
'workspaces:read',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<svg width="60" height="60" xmlns="http://www.w3.org/2000/svg"><defs><radialGradient cx="50%" cy="55%" fx="50%" fy="55%" r="72.507%" gradientTransform="matrix(.92404 0 0 1 .038 0)" id="a"><stop stop-color="#FFB900" offset="0%"/><stop stop-color="#F95D8F" offset="60%"/><stop stop-color="#F95353" offset="99.91%"/></radialGradient></defs><path d="M45.594 28.5c-6.994.003-12.664 5.673-12.667 12.667.003 6.995 5.673 12.664 12.667 12.668 6.995-.004 12.664-5.673 12.667-12.668-.003-6.994-5.672-12.664-12.667-12.667zm-32.927.001C5.673 28.505.003 34.174 0 41.17c.003 6.994 5.673 12.664 12.667 12.667 6.995-.003 12.664-5.673 12.668-12.667-.004-6.995-5.673-12.664-12.668-12.668zM41.79 12.667c-.002 6.995-5.671 12.665-12.666 12.67-6.995-.004-12.664-5.674-12.667-12.67C16.46 5.673 22.13.003 29.123 0c6.994.004 12.663 5.673 12.666 12.667z" transform="translate(.732 2.732)" fill="url(#a)" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg width="60" height="60" xmlns="http://www.w3.org/2000/svg"><defs><radialGradient cx="50%" cy="55%" fx="50%" fy="55%" r="72.507%" gradientTransform="matrix(.92404 0 0 1 .038 0)" id="a"><stop stop-color="#FFB900" offset="0%"/><stop stop-color="#F95D8F" offset="60%"/><stop stop-color="#F95353" offset="99.91%"/></radialGradient></defs><path d="M45.594 28.5c-6.994.003-12.664 5.673-12.667 12.667.003 6.995 5.673 12.664 12.667 12.668 6.995-.004 12.664-5.673 12.667-12.668-.003-6.994-5.672-12.664-12.667-12.667zm-32.927.001C5.673 28.505.003 34.174 0 41.17c.003 6.994 5.673 12.664 12.667 12.667 6.995-.003 12.664-5.673 12.668-12.667-.004-6.995-5.673-12.664-12.668-12.668zM41.79 12.667c-.002 6.995-5.671 12.665-12.666 12.67-6.995-.004-12.664-5.674-12.667-12.67C16.46 5.673 22.13.003 29.123 0c6.994.004 12.663 5.673 12.666 12.667z" transform="translate(.732 2.732)" fill="url(#a)" fill-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 951 B After Width: | Height: | Size: 904 B |
|
@ -140,6 +140,27 @@ export class AwsLambda implements INodeType {
|
||||||
value: func.FunctionArn as string,
|
value: func.FunctionArn as string,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.NextMarker) {
|
||||||
|
let marker: string = data.NextMarker;
|
||||||
|
while (true) {
|
||||||
|
const dataLoop = await awsApiRequestREST.call(this, 'lambda', 'GET', `/2015-03-31/functions/?MaxItems=50&Marker=${encodeURIComponent(marker)}`);
|
||||||
|
|
||||||
|
for (const func of dataLoop.Functions!) {
|
||||||
|
returnData.push({
|
||||||
|
name: func.FunctionName as string,
|
||||||
|
value: func.FunctionArn as string,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataLoop.NextMarker) {
|
||||||
|
marker = dataLoop.NextMarker;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return returnData;
|
return returnData;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -58,6 +58,11 @@ export class AwsComprehend implements INodeType {
|
||||||
value: 'detectDominantLanguage',
|
value: 'detectDominantLanguage',
|
||||||
description: 'Identify the dominant language',
|
description: 'Identify the dominant language',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Detect Entities',
|
||||||
|
value: 'detectEntities',
|
||||||
|
description: 'Inspects text for named entities, and returns information about them',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Detect Sentiment',
|
name: 'Detect Sentiment',
|
||||||
value: 'detectSentiment',
|
value: 'detectSentiment',
|
||||||
|
@ -129,6 +134,7 @@ export class AwsComprehend implements INodeType {
|
||||||
],
|
],
|
||||||
operation: [
|
operation: [
|
||||||
'detectSentiment',
|
'detectSentiment',
|
||||||
|
'detectEntities',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -168,6 +174,35 @@ export class AwsComprehend implements INodeType {
|
||||||
default: true,
|
default: true,
|
||||||
description: 'When set to true a simplify version of the response will be used else the raw data.',
|
description: 'When set to true a simplify version of the response will be used else the raw data.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'text',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'detectEntities',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Endpoint Arn',
|
||||||
|
name: 'endpointArn',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: {
|
||||||
|
alwaysOpenEditWindow: true,
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The Amazon Resource Name of an endpoint that is associated with a custom entity recognition model.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -209,6 +244,26 @@ export class AwsComprehend implements INodeType {
|
||||||
};
|
};
|
||||||
responseData = await awsApiRequestREST.call(this, 'comprehend', 'POST', '', JSON.stringify(body), { 'x-amz-target': action, 'Content-Type': 'application/x-amz-json-1.1' });
|
responseData = await awsApiRequestREST.call(this, 'comprehend', 'POST', '', JSON.stringify(body), { 'x-amz-target': action, 'Content-Type': 'application/x-amz-json-1.1' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//https://docs.aws.amazon.com/comprehend/latest/dg/API_DetectEntities.html
|
||||||
|
if (operation === 'detectEntities') {
|
||||||
|
const action = 'Comprehend_20171127.DetectEntities';
|
||||||
|
const text = this.getNodeParameter('text', i) as string;
|
||||||
|
const languageCode = this.getNodeParameter('languageCode', i) as string;
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||||
|
|
||||||
|
const body: IDataObject = {
|
||||||
|
Text: text,
|
||||||
|
LanguageCode: languageCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (additionalFields.endpointArn) {
|
||||||
|
body.EndpointArn = additionalFields.endpointArn;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await awsApiRequestREST.call(this, 'comprehend', 'POST', '', JSON.stringify(body), { 'x-amz-target': action, 'Content-Type': 'application/x-amz-json-1.1' });
|
||||||
|
responseData = responseData.Entities;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(responseData)) {
|
if (Array.isArray(responseData)) {
|
||||||
|
|
|
@ -80,9 +80,9 @@ export class CrateDb implements INodeType {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
placeholder: 'SELECT id, name FROM product WHERE id < 40',
|
placeholder: 'SELECT id, name FROM product WHERE quantity > $1 AND price <= $2',
|
||||||
required: true,
|
required: true,
|
||||||
description: 'The SQL query to execute.',
|
description: 'The SQL query to execute. You can use n8n expressions or $1 and $2 in conjunction with query parameters.',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -235,6 +235,21 @@ export class CrateDb implements INodeType {
|
||||||
'See the docs for more examples',
|
'See the docs for more examples',
|
||||||
].join('<br>'),
|
].join('<br>'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Query Parameters',
|
||||||
|
name: 'queryParams',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'/operation': [
|
||||||
|
'executeQuery',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
placeholder: 'quantity,price',
|
||||||
|
description: 'Comma separated list of properties which should be used as query parameters.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -15,6 +15,13 @@
|
||||||
{
|
{
|
||||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.dropbox/"
|
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.dropbox/"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"generic": [
|
||||||
|
{
|
||||||
|
"label": "Hey founders! Your business doesn't need you to operate",
|
||||||
|
"icon": " 🖥️",
|
||||||
|
"url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -24,8 +24,8 @@ export async function erpNextApiRequest(
|
||||||
uri?: string,
|
uri?: string,
|
||||||
option: IDataObject = {},
|
option: IDataObject = {},
|
||||||
) {
|
) {
|
||||||
|
const credentials = this.getCredentials('erpNextApi') as ERPNextApiCredentials;
|
||||||
const credentials = this.getCredentials('erpNextApi');
|
const baseUrl = getBaseUrl(credentials);
|
||||||
|
|
||||||
if (credentials === undefined) {
|
if (credentials === undefined) {
|
||||||
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
|
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
|
||||||
|
@ -40,7 +40,7 @@ export async function erpNextApiRequest(
|
||||||
method,
|
method,
|
||||||
body,
|
body,
|
||||||
qs: query,
|
qs: query,
|
||||||
uri: uri || `https://${credentials.subdomain}.erpnext.com${resource}`,
|
uri: uri || `${baseUrl}${resource}`,
|
||||||
json: true,
|
json: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -56,13 +56,12 @@ export async function erpNextApiRequest(
|
||||||
try {
|
try {
|
||||||
return await this.helpers.request!(options);
|
return await this.helpers.request!(options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
if (error.statusCode === 403) {
|
if (error.statusCode === 403) {
|
||||||
throw new NodeApiError(this.getNode(), { message: `DocType unavailable.` });
|
throw new NodeApiError(this.getNode(), { message: 'DocType unavailable.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.statusCode === 307) {
|
if (error.statusCode === 307) {
|
||||||
throw new NodeApiError(this.getNode(), { message:`Please ensure the subdomain is correct.` });
|
throw new NodeApiError(this.getNode(), { message: 'Please ensure the subdomain is correct.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NodeApiError(this.getNode(), error);
|
throw new NodeApiError(this.getNode(), error);
|
||||||
|
@ -95,3 +94,19 @@ export async function erpNextApiRequestAllItems(
|
||||||
|
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the base API URL based on the user's environment.
|
||||||
|
*/
|
||||||
|
const getBaseUrl = ({ environment, domain, subdomain }: ERPNextApiCredentials) =>
|
||||||
|
environment === 'cloudHosted'
|
||||||
|
? `https://${subdomain}.erpnext.com`
|
||||||
|
: domain;
|
||||||
|
|
||||||
|
type ERPNextApiCredentials = {
|
||||||
|
apiKey: string;
|
||||||
|
apiSecret: string;
|
||||||
|
environment: 'cloudHosted' | 'selfHosted';
|
||||||
|
subdomain?: string;
|
||||||
|
domain?: string;
|
||||||
|
};
|
||||||
|
|
|
@ -15,6 +15,13 @@
|
||||||
{
|
{
|
||||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.eventbriteTrigger/"
|
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.eventbriteTrigger/"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"generic": [
|
||||||
|
{
|
||||||
|
"label": "Hey founders! Your business doesn't need you to operate",
|
||||||
|
"icon": " 🖥️",
|
||||||
|
"url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
import {
|
import {
|
||||||
readFile as fsReadFile,
|
readFile as fsReadFile,
|
||||||
} from 'fs';
|
} from 'fs/promises';
|
||||||
import { promisify } from 'util';
|
|
||||||
|
|
||||||
const fsReadFileAsync = promisify(fsReadFile);
|
|
||||||
|
|
||||||
import { IExecuteFunctions } from 'n8n-core';
|
import { IExecuteFunctions } from 'n8n-core';
|
||||||
import {
|
import {
|
||||||
|
@ -162,7 +159,7 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
|
|
||||||
let workflowJson;
|
let workflowJson;
|
||||||
try {
|
try {
|
||||||
workflowJson = await fsReadFileAsync(workflowPath, { encoding: 'utf8' }) as string;
|
workflowJson = await fsReadFile(workflowPath, { encoding: 'utf8' }) as string;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
throw new NodeOperationError(this.getNode(), `The file "${workflowPath}" could not be found.`);
|
throw new NodeOperationError(this.getNode(), `The file "${workflowPath}" could not be found.`);
|
||||||
|
|
|
@ -20,7 +20,7 @@ export class FacebookGraphApi implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
displayName: 'Facebook Graph API',
|
displayName: 'Facebook Graph API',
|
||||||
name: 'facebookGraphApi',
|
name: 'facebookGraphApi',
|
||||||
icon: 'file:facebook.png',
|
icon: 'file:facebook.svg',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Interacts with Facebook using the Graph API',
|
description: 'Interacts with Facebook using the Graph API',
|
||||||
|
|
|
@ -29,7 +29,7 @@ export class FacebookTrigger implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
displayName: 'Facebook Trigger',
|
displayName: 'Facebook Trigger',
|
||||||
name: 'facebookTrigger',
|
name: 'facebookTrigger',
|
||||||
icon: 'file:facebook.png',
|
icon: 'file:facebook.svg',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["appId"] +"/"+ $parameter["object"]}}',
|
subtitle: '={{$parameter["appId"] +"/"+ $parameter["object"]}}',
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.4 KiB |
1
packages/nodes-base/nodes/Facebook/facebook.svg
Normal file
1
packages/nodes-base/nodes/Facebook/facebook.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60"><path d="M59.5 30C59.5 13.71 46.29.5 30 .5S.5 13.71.5 30c0 14.72 10.79 26.93 24.89 29.14V38.53H17.9V30h7.49v-6.5c0-7.39 4.4-11.48 11.14-11.48 3.23 0 6.6.58 6.6.58v7.26h-3.72c-3.66 0-4.81 2.27-4.81 4.61V30h8.18l-1.31 8.53H34.6v20.61C48.71 56.93 59.5 44.72 59.5 30z" fill="#1877f2"/><path d="M41.48 38.53L42.79 30h-8.18v-5.53c0-2.33 1.14-4.61 4.81-4.61h3.72V12.6s-3.38-.58-6.6-.58c-6.74 0-11.14 4.08-11.14 11.48V30h-7.5v8.53h7.49v20.61c1.5.24 3.04.36 4.61.36s3.11-.12 4.61-.36V38.53h6.87z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 567 B |
|
@ -102,7 +102,7 @@ export class Ftp implements INodeType {
|
||||||
{
|
{
|
||||||
name: 'Delete',
|
name: 'Delete',
|
||||||
value: 'delete',
|
value: 'delete',
|
||||||
description: 'Delete a file.',
|
description: 'Delete a file/folder.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Download',
|
name: 'Download',
|
||||||
|
@ -148,6 +148,46 @@ export class Ftp implements INodeType {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'options',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Option',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'delete',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Folder',
|
||||||
|
name: 'folder',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'When set to true, folders can be deleted.',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Recursive',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
folder: [
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'recursive',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'If true, remove all files and directories in target directory.',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// download
|
// download
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -401,8 +441,13 @@ export class Ftp implements INodeType {
|
||||||
|
|
||||||
if (operation === 'delete') {
|
if (operation === 'delete') {
|
||||||
const path = this.getNodeParameter('path', i) as string;
|
const path = this.getNodeParameter('path', i) as string;
|
||||||
|
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
|
||||||
responseData = await sftp!.delete(path);
|
if (options.folder === true) {
|
||||||
|
responseData = await sftp!.rmdir(path, !!options.recursive);
|
||||||
|
} else {
|
||||||
|
responseData = await sftp!.delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
returnItems.push({ json: { success: true } });
|
returnItems.push({ json: { success: true } });
|
||||||
}
|
}
|
||||||
|
@ -488,8 +533,13 @@ export class Ftp implements INodeType {
|
||||||
|
|
||||||
if (operation === 'delete') {
|
if (operation === 'delete') {
|
||||||
const path = this.getNodeParameter('path', i) as string;
|
const path = this.getNodeParameter('path', i) as string;
|
||||||
|
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
|
||||||
responseData = await ftp!.delete(path);
|
if (options.folder === true) {
|
||||||
|
responseData = await ftp!.rmdir(path, !!options.recursive);
|
||||||
|
} else {
|
||||||
|
responseData = await ftp!.delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
returnItems.push({ json: { success: true } });
|
returnItems.push({ json: { success: true } });
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,7 +180,7 @@ export class GetResponseTrigger implements INodeType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('[404]')) {
|
if (error.httpCode === '404') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -353,7 +353,7 @@ export class GithubTrigger implements INodeType {
|
||||||
try {
|
try {
|
||||||
await githubApiRequest.call(this, 'GET', endpoint, {});
|
await githubApiRequest.call(this, 'GET', endpoint, {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('[404]:')) {
|
if (error.httpCode === '404') {
|
||||||
// Webhook does not exist
|
// Webhook does not exist
|
||||||
delete webhookData.webhookId;
|
delete webhookData.webhookId;
|
||||||
delete webhookData.webhookEvents;
|
delete webhookData.webhookEvents;
|
||||||
|
@ -399,7 +399,7 @@ export class GithubTrigger implements INodeType {
|
||||||
try {
|
try {
|
||||||
responseData = await githubApiRequest.call(this, 'POST', endpoint, body);
|
responseData = await githubApiRequest.call(this, 'POST', endpoint, body);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('[422]:')) {
|
if (error.httpCode === '422') {
|
||||||
// Webhook exists already
|
// Webhook exists already
|
||||||
|
|
||||||
// Get the data of the already registered webhook
|
// Get the data of the already registered webhook
|
||||||
|
|
|
@ -179,7 +179,7 @@ export class GitlabTrigger implements INodeType {
|
||||||
try {
|
try {
|
||||||
await gitlabApiRequest.call(this, 'GET', endpoint, {});
|
await gitlabApiRequest.call(this, 'GET', endpoint, {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('[404]:')) {
|
if (error.httpCode === '404') {
|
||||||
// Webhook does not exist
|
// Webhook does not exist
|
||||||
delete webhookData.webhookId;
|
delete webhookData.webhookId;
|
||||||
delete webhookData.webhookEvents;
|
delete webhookData.webhookEvents;
|
||||||
|
|
|
@ -27,11 +27,13 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF
|
||||||
};
|
};
|
||||||
|
|
||||||
options = Object.assign({}, options, option);
|
options = Object.assign({}, options, option);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Object.keys(body).length === 0) {
|
if (Object.keys(body).length === 0) {
|
||||||
delete options.body;
|
delete options.body;
|
||||||
}
|
}
|
||||||
|
if (Object.keys(qs).length === 0) {
|
||||||
|
delete options.qs;
|
||||||
|
}
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
return await this.helpers.requestOAuth2.call(this, 'googleAnalyticsOAuth2', options);
|
return await this.helpers.requestOAuth2.call(this, 'googleAnalyticsOAuth2', options);
|
||||||
|
|
||||||
|
@ -45,12 +47,11 @@ export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOp
|
||||||
const returnData: IDataObject[] = [];
|
const returnData: IDataObject[] = [];
|
||||||
|
|
||||||
let responseData;
|
let responseData;
|
||||||
body.pageSize = 100;
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
responseData = await googleApiRequest.call(this, method, endpoint, body, query, uri);
|
responseData = await googleApiRequest.call(this, method, endpoint, body, query, uri);
|
||||||
if (body.reportRequests && Array.isArray(body.reportRequests)) {
|
if (body.reportRequests && Array.isArray(body.reportRequests)) {
|
||||||
body.reportRequests[0].pageToken = responseData['nextPageToken'];
|
body.reportRequests[0]['pageToken'] = responseData[propertyName][0].nextPageToken;
|
||||||
} else {
|
} else {
|
||||||
body.pageToken = responseData['nextPageToken'];
|
body.pageToken = responseData['nextPageToken'];
|
||||||
}
|
}
|
||||||
|
@ -58,28 +59,42 @@ export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOp
|
||||||
} while (
|
} while (
|
||||||
(responseData['nextPageToken'] !== undefined &&
|
(responseData['nextPageToken'] !== undefined &&
|
||||||
responseData['nextPageToken'] !== '') ||
|
responseData['nextPageToken'] !== '') ||
|
||||||
(responseData['reports'] &&
|
(responseData[propertyName] &&
|
||||||
responseData['reports'][0].nextPageToken &&
|
responseData[propertyName][0].nextPageToken &&
|
||||||
responseData['reports'][0].nextPageToken !== undefined)
|
responseData[propertyName][0].nextPageToken !== undefined)
|
||||||
);
|
);
|
||||||
|
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function simplify(responseData: any) { // tslint:disable-line:no-any
|
export function simplify(responseData: any | [any]) { // tslint:disable-line:no-any
|
||||||
const { columnHeader: { dimensions }, data: { rows } } = responseData[0];
|
const response = [];
|
||||||
responseData = [];
|
for (const { columnHeader: { dimensions }, data: { rows } } of responseData) {
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const data: IDataObject = {};
|
const data: IDataObject = {};
|
||||||
if (dimensions) {
|
if (dimensions) {
|
||||||
for (let i = 0; i < dimensions.length; i++) {
|
for (let i = 0; i < dimensions.length; i++) {
|
||||||
data[dimensions[i]] = row.dimensions[i];
|
data[dimensions[i]] = row.dimensions[i];
|
||||||
|
data['total'] = row.metrics[0].values.join(',');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
data['total'] = row.metrics[0].values.join(',');
|
data['total'] = row.metrics[0].values.join(',');
|
||||||
}
|
}
|
||||||
} else {
|
response.push(data);
|
||||||
data['total'] = row.metrics[0].values.join(',');
|
|
||||||
}
|
}
|
||||||
responseData.push(data);
|
|
||||||
}
|
}
|
||||||
return responseData;
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function merge(responseData: [any]) { // tslint:disable-line:no-any
|
||||||
|
const response: { columnHeader: IDataObject, data: { rows: [] } } = {
|
||||||
|
columnHeader: responseData[0].columnHeader,
|
||||||
|
data: responseData[0].data,
|
||||||
|
};
|
||||||
|
const allRows = [];
|
||||||
|
for (const { data: { rows } } of responseData) {
|
||||||
|
allRows.push(...rows);
|
||||||
|
}
|
||||||
|
response.data.rows = allRows as [];
|
||||||
|
return [response];
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,13 +24,14 @@ import {
|
||||||
import {
|
import {
|
||||||
googleApiRequest,
|
googleApiRequest,
|
||||||
googleApiRequestAllItems,
|
googleApiRequestAllItems,
|
||||||
|
merge,
|
||||||
simplify,
|
simplify,
|
||||||
} from './GenericFunctions';
|
} from './GenericFunctions';
|
||||||
|
|
||||||
import * as moment from 'moment-timezone';
|
import * as moment from 'moment-timezone';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IData
|
IData,
|
||||||
} from './Interfaces';
|
} from './Interfaces';
|
||||||
|
|
||||||
export class GoogleAnalytics implements INodeType {
|
export class GoogleAnalytics implements INodeType {
|
||||||
|
@ -69,7 +70,7 @@ export class GoogleAnalytics implements INodeType {
|
||||||
value: 'userActivity',
|
value: 'userActivity',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
default:'report',
|
default: 'report',
|
||||||
},
|
},
|
||||||
//-------------------------------
|
//-------------------------------
|
||||||
// Reports Operations
|
// Reports Operations
|
||||||
|
@ -152,12 +153,13 @@ export class GoogleAnalytics implements INodeType {
|
||||||
let endpoint = '';
|
let endpoint = '';
|
||||||
let responseData;
|
let responseData;
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
if(resource === 'report') {
|
if (resource === 'report') {
|
||||||
if(operation === 'get') {
|
if (operation === 'get') {
|
||||||
//https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet
|
//https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet
|
||||||
method = 'POST';
|
method = 'POST';
|
||||||
endpoint = '/v4/reports:batchGet';
|
endpoint = '/v4/reports:batchGet';
|
||||||
const viewId = this.getNodeParameter('viewId', i) as string;
|
const viewId = this.getNodeParameter('viewId', i) as string;
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
|
||||||
const additionalFields = this.getNodeParameter(
|
const additionalFields = this.getNodeParameter(
|
||||||
'additionalFields',
|
'additionalFields',
|
||||||
i,
|
i,
|
||||||
|
@ -165,63 +167,69 @@ export class GoogleAnalytics implements INodeType {
|
||||||
const simple = this.getNodeParameter('simple', i) as boolean;
|
const simple = this.getNodeParameter('simple', i) as boolean;
|
||||||
|
|
||||||
const body: IData = {
|
const body: IData = {
|
||||||
viewId,
|
viewId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if(additionalFields.useResourceQuotas){
|
if (additionalFields.useResourceQuotas) {
|
||||||
qs.useResourceQuotas = additionalFields.useResourceQuotas;
|
qs.useResourceQuotas = additionalFields.useResourceQuotas;
|
||||||
}
|
}
|
||||||
if(additionalFields.dateRangesUi){
|
if (additionalFields.dateRangesUi) {
|
||||||
const dateValues = (additionalFields.dateRangesUi as IDataObject).dateRanges as IDataObject;
|
const dateValues = (additionalFields.dateRangesUi as IDataObject).dateRanges as IDataObject;
|
||||||
if(dateValues){
|
if (dateValues) {
|
||||||
const start = dateValues.startDate as string;
|
const start = dateValues.startDate as string;
|
||||||
const end = dateValues.endDate as string;
|
const end = dateValues.endDate as string;
|
||||||
Object.assign(
|
Object.assign(
|
||||||
body,
|
body,
|
||||||
{
|
{
|
||||||
dateRanges:
|
dateRanges:
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
startDate: moment(start).utc().format('YYYY-MM-DD'),
|
startDate: moment(start).utc().format('YYYY-MM-DD'),
|
||||||
endDate: moment(end).utc().format('YYYY-MM-DD'),
|
endDate: moment(end).utc().format('YYYY-MM-DD'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(additionalFields.metricsUi) {
|
if (additionalFields.metricsUi) {
|
||||||
const metrics = (additionalFields.metricsUi as IDataObject).metricValues as IDataObject[];
|
const metrics = (additionalFields.metricsUi as IDataObject).metricValues as IDataObject[];
|
||||||
body.metrics = metrics;
|
body.metrics = metrics;
|
||||||
}
|
}
|
||||||
if(additionalFields.dimensionUi){
|
if (additionalFields.dimensionUi) {
|
||||||
const dimensions = (additionalFields.dimensionUi as IDataObject).dimensionValues as IDataObject[];
|
const dimensions = (additionalFields.dimensionUi as IDataObject).dimensionValues as IDataObject[];
|
||||||
if (dimensions) {
|
if (dimensions) {
|
||||||
body.dimensions = dimensions;
|
body.dimensions = dimensions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(additionalFields.includeEmptyRows){
|
if (additionalFields.includeEmptyRows) {
|
||||||
Object.assign(body, { includeEmptyRows: additionalFields.includeEmptyRows });
|
Object.assign(body, { includeEmptyRows: additionalFields.includeEmptyRows });
|
||||||
}
|
}
|
||||||
if(additionalFields.hideTotals){
|
if (additionalFields.hideTotals) {
|
||||||
Object.assign(body, { hideTotals: additionalFields.hideTotals });
|
Object.assign(body, { hideTotals: additionalFields.hideTotals });
|
||||||
}
|
}
|
||||||
if(additionalFields.hideValueRanges){
|
if (additionalFields.hideValueRanges) {
|
||||||
Object.assign(body, { hideTotals: additionalFields.hideTotals });
|
Object.assign(body, { hideTotals: additionalFields.hideTotals });
|
||||||
}
|
}
|
||||||
|
|
||||||
responseData = await googleApiRequest.call(this, method, endpoint, { reportRequests: [body] }, qs);
|
if (returnAll === true) {
|
||||||
responseData = responseData.reports;
|
responseData = await googleApiRequestAllItems.call(this, 'reports', method, endpoint, { reportRequests: [body] }, qs);
|
||||||
|
} else {
|
||||||
|
responseData = await googleApiRequest.call(this, method, endpoint, { reportRequests: [body] }, qs);
|
||||||
|
responseData = responseData.reports;
|
||||||
|
}
|
||||||
|
|
||||||
if (simple === true) {
|
if (simple === true) {
|
||||||
responseData = simplify(responseData);
|
responseData = simplify(responseData);
|
||||||
|
} else if (returnAll === true && responseData.length > 1) {
|
||||||
|
responseData = merge(responseData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(resource === 'userActivity') {
|
if (resource === 'userActivity') {
|
||||||
if(operation === 'search') {
|
if (operation === 'search') {
|
||||||
// https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/userActivity/search
|
//https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/userActivity/search
|
||||||
method = 'POST';
|
method = 'POST';
|
||||||
endpoint = '/v4/userActivity:search';
|
endpoint = '/v4/userActivity:search';
|
||||||
const viewId = this.getNodeParameter('viewId', i);
|
const viewId = this.getNodeParameter('viewId', i);
|
||||||
|
@ -237,7 +245,7 @@ export class GoogleAnalytics implements INodeType {
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if(additionalFields.activityTypes) {
|
if (additionalFields.activityTypes) {
|
||||||
Object.assign(body, { activityTypes: additionalFields.activityTypes });
|
Object.assign(body, { activityTypes: additionalFields.activityTypes });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,47 @@ export const reportFields = [
|
||||||
placeholder: '123456',
|
placeholder: '123456',
|
||||||
description: 'The View ID of Google Analytics',
|
description: 'The View ID of Google Analytics',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'get',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'report',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: false,
|
||||||
|
description: 'If all results should be returned or only up to a given limit.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'get',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'report',
|
||||||
|
],
|
||||||
|
returnAll: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
maxValue: 1000,
|
||||||
|
},
|
||||||
|
default: 1000,
|
||||||
|
description: 'How many results to return.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Simple',
|
displayName: 'Simple',
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"node": "n8n-nodes-base.googleBigQuery",
|
||||||
|
"nodeVersion": "1.0",
|
||||||
|
"codexVersion": "1.0",
|
||||||
|
"categories": [
|
||||||
|
"Data & Storage",
|
||||||
|
"Development"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"credentialDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/credentials/google"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleBigQuery/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,11 @@
|
||||||
"icon": "🎫",
|
"icon": "🎫",
|
||||||
"url": "https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/"
|
"url": "https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Hey founders! Your business doesn't need you to operate",
|
||||||
|
"icon": " 🖥️",
|
||||||
|
"url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "5 workflow automations for Mattermost that we love at n8n",
|
"label": "5 workflow automations for Mattermost that we love at n8n",
|
||||||
"icon": "🤖",
|
"icon": "🤖",
|
||||||
|
|
|
@ -180,429 +180,443 @@ export class GoogleCalendar implements INodeType {
|
||||||
const resource = this.getNodeParameter('resource', 0) as string;
|
const resource = this.getNodeParameter('resource', 0) as string;
|
||||||
const operation = this.getNodeParameter('operation', 0) as string;
|
const operation = this.getNodeParameter('operation', 0) as string;
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
if (resource === 'calendar') {
|
try {
|
||||||
//https://developers.google.com/calendar/v3/reference/freebusy/query
|
if (resource === 'calendar') {
|
||||||
if (operation === 'availability') {
|
//https://developers.google.com/calendar/v3/reference/freebusy/query
|
||||||
const timezone = this.getTimezone();
|
if (operation === 'availability') {
|
||||||
const calendarId = this.getNodeParameter('calendar', i) as string;
|
const timezone = this.getTimezone();
|
||||||
const timeMin = this.getNodeParameter('timeMin', i) as string;
|
const calendarId = this.getNodeParameter('calendar', i) as string;
|
||||||
const timeMax = this.getNodeParameter('timeMax', i) as string;
|
const timeMin = this.getNodeParameter('timeMin', i) as string;
|
||||||
const options = this.getNodeParameter('options', i) as IDataObject;
|
const timeMax = this.getNodeParameter('timeMax', i) as string;
|
||||||
const outputFormat = options.outputFormat || 'availability';
|
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
const outputFormat = options.outputFormat || 'availability';
|
||||||
|
|
||||||
const body: IDataObject = {
|
const body: IDataObject = {
|
||||||
timeMin: moment.tz(timeMin, timezone).utc().format(),
|
timeMin: moment.tz(timeMin, timezone).utc().format(),
|
||||||
timeMax: moment.tz(timeMax, timezone).utc().format(),
|
timeMax: moment.tz(timeMax, timezone).utc().format(),
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: calendarId,
|
id: calendarId,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timeZone: options.timezone || timezone,
|
timeZone: options.timezone || timezone,
|
||||||
};
|
|
||||||
|
|
||||||
responseData = await googleApiRequest.call(
|
|
||||||
this,
|
|
||||||
'POST',
|
|
||||||
`/calendar/v3/freeBusy`,
|
|
||||||
body,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (responseData.calendars[calendarId].errors) {
|
|
||||||
throw new NodeApiError(this.getNode(), responseData.calendars[calendarId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outputFormat === 'availability') {
|
|
||||||
responseData = {
|
|
||||||
available: !responseData.calendars[calendarId].busy.length,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} else if (outputFormat === 'bookedSlots') {
|
responseData = await googleApiRequest.call(
|
||||||
responseData = responseData.calendars[calendarId].busy;
|
this,
|
||||||
|
'POST',
|
||||||
|
`/calendar/v3/freeBusy`,
|
||||||
|
body,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (responseData.calendars[calendarId].errors) {
|
||||||
|
throw new NodeApiError(this.getNode(), responseData.calendars[calendarId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputFormat === 'availability') {
|
||||||
|
responseData = {
|
||||||
|
available: !responseData.calendars[calendarId].busy.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
} else if (outputFormat === 'bookedSlots') {
|
||||||
|
responseData = responseData.calendars[calendarId].busy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (resource === 'event') {
|
||||||
if (resource === 'event') {
|
//https://developers.google.com/calendar/v3/reference/events/insert
|
||||||
//https://developers.google.com/calendar/v3/reference/events/insert
|
if (operation === 'create') {
|
||||||
if (operation === 'create') {
|
const calendarId = this.getNodeParameter('calendar', i) as string;
|
||||||
const calendarId = this.getNodeParameter('calendar', i) as string;
|
const start = this.getNodeParameter('start', i) as string;
|
||||||
const start = this.getNodeParameter('start', i) as string;
|
const end = this.getNodeParameter('end', i) as string;
|
||||||
const end = this.getNodeParameter('end', i) as string;
|
const useDefaultReminders = this.getNodeParameter(
|
||||||
const useDefaultReminders = this.getNodeParameter(
|
'useDefaultReminders',
|
||||||
'useDefaultReminders',
|
|
||||||
i,
|
|
||||||
) as boolean;
|
|
||||||
const additionalFields = this.getNodeParameter(
|
|
||||||
'additionalFields',
|
|
||||||
i,
|
|
||||||
) as IDataObject;
|
|
||||||
if (additionalFields.maxAttendees) {
|
|
||||||
qs.maxAttendees = additionalFields.maxAttendees as number;
|
|
||||||
}
|
|
||||||
if (additionalFields.sendNotifications) {
|
|
||||||
qs.sendNotifications = additionalFields.sendNotifications as boolean;
|
|
||||||
}
|
|
||||||
if (additionalFields.sendUpdates) {
|
|
||||||
qs.sendUpdates = additionalFields.sendUpdates as string;
|
|
||||||
}
|
|
||||||
const body: IEvent = {
|
|
||||||
start: {
|
|
||||||
dateTime: start,
|
|
||||||
timeZone: additionalFields.timeZone || this.getTimezone(),
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
dateTime: end,
|
|
||||||
timeZone: additionalFields.timeZone || this.getTimezone(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (additionalFields.attendees) {
|
|
||||||
body.attendees = [];
|
|
||||||
(additionalFields.attendees as string[]).forEach(attendee => {
|
|
||||||
body.attendees!.push.apply(body.attendees, attendee.split(',').map(a => a.trim()).map(email => ({ email })));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (additionalFields.color) {
|
|
||||||
body.colorId = additionalFields.color as string;
|
|
||||||
}
|
|
||||||
if (additionalFields.description) {
|
|
||||||
body.description = additionalFields.description as string;
|
|
||||||
}
|
|
||||||
if (additionalFields.guestsCanInviteOthers) {
|
|
||||||
body.guestsCanInviteOthers = additionalFields.guestsCanInviteOthers as boolean;
|
|
||||||
}
|
|
||||||
if (additionalFields.guestsCanModify) {
|
|
||||||
body.guestsCanModify = additionalFields.guestsCanModify as boolean;
|
|
||||||
}
|
|
||||||
if (additionalFields.guestsCanSeeOtherGuests) {
|
|
||||||
body.guestsCanSeeOtherGuests = additionalFields.guestsCanSeeOtherGuests as boolean;
|
|
||||||
}
|
|
||||||
if (additionalFields.id) {
|
|
||||||
body.id = additionalFields.id as string;
|
|
||||||
}
|
|
||||||
if (additionalFields.location) {
|
|
||||||
body.location = additionalFields.location as string;
|
|
||||||
}
|
|
||||||
if (additionalFields.summary) {
|
|
||||||
body.summary = additionalFields.summary as string;
|
|
||||||
}
|
|
||||||
if (additionalFields.showMeAs) {
|
|
||||||
body.transparency = additionalFields.showMeAs as string;
|
|
||||||
}
|
|
||||||
if (additionalFields.visibility) {
|
|
||||||
body.visibility = additionalFields.visibility as string;
|
|
||||||
}
|
|
||||||
if (!useDefaultReminders) {
|
|
||||||
const reminders = (this.getNodeParameter(
|
|
||||||
'remindersUi',
|
|
||||||
i,
|
i,
|
||||||
) as IDataObject).remindersValues as IDataObject[];
|
) as boolean;
|
||||||
body.reminders = {
|
const additionalFields = this.getNodeParameter(
|
||||||
useDefault: false,
|
'additionalFields',
|
||||||
|
i,
|
||||||
|
) as IDataObject;
|
||||||
|
if (additionalFields.maxAttendees) {
|
||||||
|
qs.maxAttendees = additionalFields.maxAttendees as number;
|
||||||
|
}
|
||||||
|
if (additionalFields.sendNotifications) {
|
||||||
|
qs.sendNotifications = additionalFields.sendNotifications as boolean;
|
||||||
|
}
|
||||||
|
if (additionalFields.sendUpdates) {
|
||||||
|
qs.sendUpdates = additionalFields.sendUpdates as string;
|
||||||
|
}
|
||||||
|
const body: IEvent = {
|
||||||
|
start: {
|
||||||
|
dateTime: start,
|
||||||
|
timeZone: additionalFields.timeZone || this.getTimezone(),
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: end,
|
||||||
|
timeZone: additionalFields.timeZone || this.getTimezone(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
if (reminders) {
|
if (additionalFields.attendees) {
|
||||||
body.reminders.overrides = reminders;
|
body.attendees = [];
|
||||||
|
(additionalFields.attendees as string[]).forEach(attendee => {
|
||||||
|
body.attendees!.push.apply(body.attendees, attendee.split(',').map(a => a.trim()).map(email => ({ email })));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
if (additionalFields.color) {
|
||||||
if (additionalFields.allday) {
|
body.colorId = additionalFields.color as string;
|
||||||
body.start = {
|
|
||||||
date: moment(start)
|
|
||||||
.utc()
|
|
||||||
.format('YYYY-MM-DD'),
|
|
||||||
};
|
|
||||||
body.end = {
|
|
||||||
date: moment(end)
|
|
||||||
.utc()
|
|
||||||
.format('YYYY-MM-DD'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
//exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z
|
|
||||||
//https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
|
|
||||||
body.recurrence = [];
|
|
||||||
if (additionalFields.rrule) {
|
|
||||||
body.recurrence = [`RRULE:${additionalFields.rrule}`];
|
|
||||||
} else {
|
|
||||||
if (
|
|
||||||
additionalFields.repeatHowManyTimes &&
|
|
||||||
additionalFields.repeatUntil
|
|
||||||
) {
|
|
||||||
throw new NodeOperationError(this.getNode(),
|
|
||||||
`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (additionalFields.repeatFrecuency) {
|
if (additionalFields.description) {
|
||||||
body.recurrence?.push(
|
body.description = additionalFields.description as string;
|
||||||
`FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (additionalFields.repeatHowManyTimes) {
|
if (additionalFields.guestsCanInviteOthers) {
|
||||||
body.recurrence?.push(
|
body.guestsCanInviteOthers = additionalFields.guestsCanInviteOthers as boolean;
|
||||||
`COUNT=${additionalFields.repeatHowManyTimes};`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (additionalFields.repeatUntil) {
|
if (additionalFields.guestsCanModify) {
|
||||||
body.recurrence?.push(
|
body.guestsCanModify = additionalFields.guestsCanModify as boolean;
|
||||||
`UNTIL=${moment(additionalFields.repeatUntil as string)
|
}
|
||||||
|
if (additionalFields.guestsCanSeeOtherGuests) {
|
||||||
|
body.guestsCanSeeOtherGuests = additionalFields.guestsCanSeeOtherGuests as boolean;
|
||||||
|
}
|
||||||
|
if (additionalFields.id) {
|
||||||
|
body.id = additionalFields.id as string;
|
||||||
|
}
|
||||||
|
if (additionalFields.location) {
|
||||||
|
body.location = additionalFields.location as string;
|
||||||
|
}
|
||||||
|
if (additionalFields.summary) {
|
||||||
|
body.summary = additionalFields.summary as string;
|
||||||
|
}
|
||||||
|
if (additionalFields.showMeAs) {
|
||||||
|
body.transparency = additionalFields.showMeAs as string;
|
||||||
|
}
|
||||||
|
if (additionalFields.visibility) {
|
||||||
|
body.visibility = additionalFields.visibility as string;
|
||||||
|
}
|
||||||
|
if (!useDefaultReminders) {
|
||||||
|
const reminders = (this.getNodeParameter(
|
||||||
|
'remindersUi',
|
||||||
|
i,
|
||||||
|
) as IDataObject).remindersValues as IDataObject[];
|
||||||
|
body.reminders = {
|
||||||
|
useDefault: false,
|
||||||
|
};
|
||||||
|
if (reminders) {
|
||||||
|
body.reminders.overrides = reminders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (additionalFields.allday) {
|
||||||
|
body.start = {
|
||||||
|
date: moment(start)
|
||||||
.utc()
|
.utc()
|
||||||
.format('YYYYMMDDTHHmmss')}Z`,
|
.format('YYYY-MM-DD'),
|
||||||
);
|
};
|
||||||
}
|
body.end = {
|
||||||
if (body.recurrence.length !== 0) {
|
date: moment(end)
|
||||||
body.recurrence = [`RRULE:${body.recurrence.join('')}`];
|
.utc()
|
||||||
}
|
.format('YYYY-MM-DD'),
|
||||||
}
|
|
||||||
|
|
||||||
if (additionalFields.conferenceDataUi) {
|
|
||||||
const conferenceData = (additionalFields.conferenceDataUi as IDataObject).conferenceDataValues as IDataObject;
|
|
||||||
if (conferenceData) {
|
|
||||||
|
|
||||||
qs.conferenceDataVersion = 1;
|
|
||||||
body.conferenceData = {
|
|
||||||
createRequest: {
|
|
||||||
requestId: uuid(),
|
|
||||||
conferenceSolution: {
|
|
||||||
type: conferenceData.conferenceSolution as string,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
//exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z
|
||||||
|
//https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
|
||||||
|
body.recurrence = [];
|
||||||
|
if (additionalFields.rrule) {
|
||||||
|
body.recurrence = [`RRULE:${additionalFields.rrule}`];
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
additionalFields.repeatHowManyTimes &&
|
||||||
|
additionalFields.repeatUntil
|
||||||
|
) {
|
||||||
|
throw new NodeOperationError(this.getNode(),
|
||||||
|
`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (additionalFields.repeatFrecuency) {
|
||||||
|
body.recurrence?.push(
|
||||||
|
`FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (additionalFields.repeatHowManyTimes) {
|
||||||
|
body.recurrence?.push(
|
||||||
|
`COUNT=${additionalFields.repeatHowManyTimes};`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (additionalFields.repeatUntil) {
|
||||||
|
body.recurrence?.push(
|
||||||
|
`UNTIL=${moment(additionalFields.repeatUntil as string)
|
||||||
|
.utc()
|
||||||
|
.format('YYYYMMDDTHHmmss')}Z`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (body.recurrence.length !== 0) {
|
||||||
|
body.recurrence = [`RRULE:${body.recurrence.join('')}`];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
responseData = await googleApiRequest.call(
|
if (additionalFields.conferenceDataUi) {
|
||||||
this,
|
const conferenceData = (additionalFields.conferenceDataUi as IDataObject).conferenceDataValues as IDataObject;
|
||||||
'POST',
|
if (conferenceData) {
|
||||||
`/calendar/v3/calendars/${calendarId}/events`,
|
|
||||||
body,
|
qs.conferenceDataVersion = 1;
|
||||||
qs,
|
body.conferenceData = {
|
||||||
);
|
createRequest: {
|
||||||
}
|
requestId: uuid(),
|
||||||
//https://developers.google.com/calendar/v3/reference/events/delete
|
conferenceSolution: {
|
||||||
if (operation === 'delete') {
|
type: conferenceData.conferenceSolution as string,
|
||||||
const calendarId = this.getNodeParameter('calendar', i) as string;
|
},
|
||||||
const eventId = this.getNodeParameter('eventId', i) as string;
|
},
|
||||||
const options = this.getNodeParameter('options', i) as IDataObject;
|
};
|
||||||
if (options.sendUpdates) {
|
}
|
||||||
qs.sendUpdates = options.sendUpdates as number;
|
}
|
||||||
}
|
|
||||||
responseData = await googleApiRequest.call(
|
responseData = await googleApiRequest.call(
|
||||||
this,
|
|
||||||
'DELETE',
|
|
||||||
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
responseData = { success: true };
|
|
||||||
}
|
|
||||||
//https://developers.google.com/calendar/v3/reference/events/get
|
|
||||||
if (operation === 'get') {
|
|
||||||
const calendarId = this.getNodeParameter('calendar', i) as string;
|
|
||||||
const eventId = this.getNodeParameter('eventId', i) as string;
|
|
||||||
const options = this.getNodeParameter('options', i) as IDataObject;
|
|
||||||
if (options.maxAttendees) {
|
|
||||||
qs.maxAttendees = options.maxAttendees as number;
|
|
||||||
}
|
|
||||||
if (options.timeZone) {
|
|
||||||
qs.timeZone = options.timeZone as string;
|
|
||||||
}
|
|
||||||
responseData = await googleApiRequest.call(
|
|
||||||
this,
|
|
||||||
'GET',
|
|
||||||
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
|
||||||
{},
|
|
||||||
qs,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
//https://developers.google.com/calendar/v3/reference/events/list
|
|
||||||
if (operation === 'getAll') {
|
|
||||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
|
||||||
const calendarId = this.getNodeParameter('calendar', i) as string;
|
|
||||||
const options = this.getNodeParameter('options', i) as IDataObject;
|
|
||||||
if (options.iCalUID) {
|
|
||||||
qs.iCalUID = options.iCalUID as string;
|
|
||||||
}
|
|
||||||
if (options.maxAttendees) {
|
|
||||||
qs.maxAttendees = options.maxAttendees as number;
|
|
||||||
}
|
|
||||||
if (options.orderBy) {
|
|
||||||
qs.orderBy = options.orderBy as number;
|
|
||||||
}
|
|
||||||
if (options.query) {
|
|
||||||
qs.q = options.query as number;
|
|
||||||
}
|
|
||||||
if (options.showDeleted) {
|
|
||||||
qs.showDeleted = options.showDeleted as boolean;
|
|
||||||
}
|
|
||||||
if (options.showHiddenInvitations) {
|
|
||||||
qs.showHiddenInvitations = options.showHiddenInvitations as boolean;
|
|
||||||
}
|
|
||||||
if (options.singleEvents) {
|
|
||||||
qs.singleEvents = options.singleEvents as boolean;
|
|
||||||
}
|
|
||||||
if (options.timeMax) {
|
|
||||||
qs.timeMax = options.timeMax as string;
|
|
||||||
}
|
|
||||||
if (options.timeMin) {
|
|
||||||
qs.timeMin = options.timeMin as string;
|
|
||||||
}
|
|
||||||
if (options.timeZone) {
|
|
||||||
qs.timeZone = options.timeZone as string;
|
|
||||||
}
|
|
||||||
if (options.updatedMin) {
|
|
||||||
qs.updatedMin = options.updatedMin as string;
|
|
||||||
}
|
|
||||||
if (returnAll) {
|
|
||||||
responseData = await googleApiRequestAllItems.call(
|
|
||||||
this,
|
this,
|
||||||
'items',
|
'POST',
|
||||||
'GET',
|
|
||||||
`/calendar/v3/calendars/${calendarId}/events`,
|
`/calendar/v3/calendars/${calendarId}/events`,
|
||||||
{},
|
body,
|
||||||
qs,
|
qs,
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
qs.maxResults = this.getNodeParameter('limit', i) as number;
|
//https://developers.google.com/calendar/v3/reference/events/delete
|
||||||
|
if (operation === 'delete') {
|
||||||
|
const calendarId = this.getNodeParameter('calendar', i) as string;
|
||||||
|
const eventId = this.getNodeParameter('eventId', i) as string;
|
||||||
|
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
if (options.sendUpdates) {
|
||||||
|
qs.sendUpdates = options.sendUpdates as number;
|
||||||
|
}
|
||||||
|
responseData = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'DELETE',
|
||||||
|
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
responseData = { success: true };
|
||||||
|
}
|
||||||
|
//https://developers.google.com/calendar/v3/reference/events/get
|
||||||
|
if (operation === 'get') {
|
||||||
|
const calendarId = this.getNodeParameter('calendar', i) as string;
|
||||||
|
const eventId = this.getNodeParameter('eventId', i) as string;
|
||||||
|
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
if (options.maxAttendees) {
|
||||||
|
qs.maxAttendees = options.maxAttendees as number;
|
||||||
|
}
|
||||||
|
if (options.timeZone) {
|
||||||
|
qs.timeZone = options.timeZone as string;
|
||||||
|
}
|
||||||
responseData = await googleApiRequest.call(
|
responseData = await googleApiRequest.call(
|
||||||
this,
|
this,
|
||||||
'GET',
|
'GET',
|
||||||
`/calendar/v3/calendars/${calendarId}/events`,
|
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
||||||
{},
|
{},
|
||||||
qs,
|
qs,
|
||||||
);
|
);
|
||||||
responseData = responseData.items;
|
}
|
||||||
|
//https://developers.google.com/calendar/v3/reference/events/list
|
||||||
|
if (operation === 'getAll') {
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||||
|
const calendarId = this.getNodeParameter('calendar', i) as string;
|
||||||
|
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
if (options.iCalUID) {
|
||||||
|
qs.iCalUID = options.iCalUID as string;
|
||||||
|
}
|
||||||
|
if (options.maxAttendees) {
|
||||||
|
qs.maxAttendees = options.maxAttendees as number;
|
||||||
|
}
|
||||||
|
if (options.orderBy) {
|
||||||
|
qs.orderBy = options.orderBy as number;
|
||||||
|
}
|
||||||
|
if (options.query) {
|
||||||
|
qs.q = options.query as number;
|
||||||
|
}
|
||||||
|
if (options.showDeleted) {
|
||||||
|
qs.showDeleted = options.showDeleted as boolean;
|
||||||
|
}
|
||||||
|
if (options.showHiddenInvitations) {
|
||||||
|
qs.showHiddenInvitations = options.showHiddenInvitations as boolean;
|
||||||
|
}
|
||||||
|
if (options.singleEvents) {
|
||||||
|
qs.singleEvents = options.singleEvents as boolean;
|
||||||
|
}
|
||||||
|
if (options.timeMax) {
|
||||||
|
qs.timeMax = options.timeMax as string;
|
||||||
|
}
|
||||||
|
if (options.timeMin) {
|
||||||
|
qs.timeMin = options.timeMin as string;
|
||||||
|
}
|
||||||
|
if (options.timeZone) {
|
||||||
|
qs.timeZone = options.timeZone as string;
|
||||||
|
}
|
||||||
|
if (options.updatedMin) {
|
||||||
|
qs.updatedMin = options.updatedMin as string;
|
||||||
|
}
|
||||||
|
if (returnAll) {
|
||||||
|
responseData = await googleApiRequestAllItems.call(
|
||||||
|
this,
|
||||||
|
'items',
|
||||||
|
'GET',
|
||||||
|
`/calendar/v3/calendars/${calendarId}/events`,
|
||||||
|
{},
|
||||||
|
qs,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
qs.maxResults = this.getNodeParameter('limit', i) as number;
|
||||||
|
responseData = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
`/calendar/v3/calendars/${calendarId}/events`,
|
||||||
|
{},
|
||||||
|
qs,
|
||||||
|
);
|
||||||
|
responseData = responseData.items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//https://developers.google.com/calendar/v3/reference/events/patch
|
||||||
|
if (operation === 'update') {
|
||||||
|
const calendarId = this.getNodeParameter('calendar', i) as string;
|
||||||
|
const eventId = this.getNodeParameter('eventId', i) as string;
|
||||||
|
const useDefaultReminders = this.getNodeParameter(
|
||||||
|
'useDefaultReminders',
|
||||||
|
i,
|
||||||
|
) as boolean;
|
||||||
|
const updateFields = this.getNodeParameter(
|
||||||
|
'updateFields',
|
||||||
|
i,
|
||||||
|
) as IDataObject;
|
||||||
|
if (updateFields.maxAttendees) {
|
||||||
|
qs.maxAttendees = updateFields.maxAttendees as number;
|
||||||
|
}
|
||||||
|
if (updateFields.sendNotifications) {
|
||||||
|
qs.sendNotifications = updateFields.sendNotifications as boolean;
|
||||||
|
}
|
||||||
|
if (updateFields.sendUpdates) {
|
||||||
|
qs.sendUpdates = updateFields.sendUpdates as string;
|
||||||
|
}
|
||||||
|
const body: IEvent = {};
|
||||||
|
if (updateFields.start) {
|
||||||
|
body.start = {
|
||||||
|
dateTime: updateFields.start,
|
||||||
|
timeZone: updateFields.timeZone || this.getTimezone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (updateFields.end) {
|
||||||
|
body.end = {
|
||||||
|
dateTime: updateFields.end,
|
||||||
|
timeZone: updateFields.timeZone || this.getTimezone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (updateFields.attendees) {
|
||||||
|
body.attendees = [];
|
||||||
|
(updateFields.attendees as string[]).forEach(attendee => {
|
||||||
|
body.attendees!.push.apply(body.attendees, attendee.split(',').map(a => a.trim()).map(email => ({ email })));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (updateFields.color) {
|
||||||
|
body.colorId = updateFields.color as string;
|
||||||
|
}
|
||||||
|
if (updateFields.description) {
|
||||||
|
body.description = updateFields.description as string;
|
||||||
|
}
|
||||||
|
if (updateFields.guestsCanInviteOthers) {
|
||||||
|
body.guestsCanInviteOthers = updateFields.guestsCanInviteOthers as boolean;
|
||||||
|
}
|
||||||
|
if (updateFields.guestsCanModify) {
|
||||||
|
body.guestsCanModify = updateFields.guestsCanModify as boolean;
|
||||||
|
}
|
||||||
|
if (updateFields.guestsCanSeeOtherGuests) {
|
||||||
|
body.guestsCanSeeOtherGuests = updateFields.guestsCanSeeOtherGuests as boolean;
|
||||||
|
}
|
||||||
|
if (updateFields.id) {
|
||||||
|
body.id = updateFields.id as string;
|
||||||
|
}
|
||||||
|
if (updateFields.location) {
|
||||||
|
body.location = updateFields.location as string;
|
||||||
|
}
|
||||||
|
if (updateFields.summary) {
|
||||||
|
body.summary = updateFields.summary as string;
|
||||||
|
}
|
||||||
|
if (updateFields.showMeAs) {
|
||||||
|
body.transparency = updateFields.showMeAs as string;
|
||||||
|
}
|
||||||
|
if (updateFields.visibility) {
|
||||||
|
body.visibility = updateFields.visibility as string;
|
||||||
|
}
|
||||||
|
if (!useDefaultReminders) {
|
||||||
|
const reminders = (this.getNodeParameter(
|
||||||
|
'remindersUi',
|
||||||
|
i,
|
||||||
|
) as IDataObject).remindersValues as IDataObject[];
|
||||||
|
body.reminders = {
|
||||||
|
useDefault: false,
|
||||||
|
};
|
||||||
|
if (reminders) {
|
||||||
|
body.reminders.overrides = reminders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updateFields.allday && updateFields.start && updateFields.end) {
|
||||||
|
body.start = {
|
||||||
|
date: moment(updateFields.start as string)
|
||||||
|
.utc()
|
||||||
|
.format('YYYY-MM-DD'),
|
||||||
|
};
|
||||||
|
body.end = {
|
||||||
|
date: moment(updateFields.end as string)
|
||||||
|
.utc()
|
||||||
|
.format('YYYY-MM-DD'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z
|
||||||
|
//https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
|
||||||
|
body.recurrence = [];
|
||||||
|
if (updateFields.rrule) {
|
||||||
|
body.recurrence = [`RRULE:${updateFields.rrule}`];
|
||||||
|
} else {
|
||||||
|
if (updateFields.repeatHowManyTimes && updateFields.repeatUntil) {
|
||||||
|
throw new NodeOperationError(this.getNode(),
|
||||||
|
`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (updateFields.repeatFrecuency) {
|
||||||
|
body.recurrence?.push(
|
||||||
|
`FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (updateFields.repeatHowManyTimes) {
|
||||||
|
body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`);
|
||||||
|
}
|
||||||
|
if (updateFields.repeatUntil) {
|
||||||
|
body.recurrence?.push(
|
||||||
|
`UNTIL=${moment(updateFields.repeatUntil as string)
|
||||||
|
.utc()
|
||||||
|
.format('YYYYMMDDTHHmmss')}Z`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (body.recurrence.length !== 0) {
|
||||||
|
body.recurrence = [`RRULE:${body.recurrence.join('')}`];
|
||||||
|
} else {
|
||||||
|
delete body.recurrence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responseData = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'PATCH',
|
||||||
|
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
||||||
|
body,
|
||||||
|
qs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//https://developers.google.com/calendar/v3/reference/events/patch
|
} catch (error) {
|
||||||
if (operation === 'update') {
|
if (this.continueOnFail() !== true) {
|
||||||
const calendarId = this.getNodeParameter('calendar', i) as string;
|
throw error;
|
||||||
const eventId = this.getNodeParameter('eventId', i) as string;
|
} else {
|
||||||
const useDefaultReminders = this.getNodeParameter(
|
// Return the actual reason as error
|
||||||
'useDefaultReminders',
|
returnData.push(
|
||||||
i,
|
{
|
||||||
) as boolean;
|
error: error.message,
|
||||||
const updateFields = this.getNodeParameter(
|
},
|
||||||
'updateFields',
|
|
||||||
i,
|
|
||||||
) as IDataObject;
|
|
||||||
if (updateFields.maxAttendees) {
|
|
||||||
qs.maxAttendees = updateFields.maxAttendees as number;
|
|
||||||
}
|
|
||||||
if (updateFields.sendNotifications) {
|
|
||||||
qs.sendNotifications = updateFields.sendNotifications as boolean;
|
|
||||||
}
|
|
||||||
if (updateFields.sendUpdates) {
|
|
||||||
qs.sendUpdates = updateFields.sendUpdates as string;
|
|
||||||
}
|
|
||||||
const body: IEvent = {};
|
|
||||||
if (updateFields.start) {
|
|
||||||
body.start = {
|
|
||||||
dateTime: updateFields.start,
|
|
||||||
timeZone: updateFields.timeZone || this.getTimezone(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (updateFields.end) {
|
|
||||||
body.end = {
|
|
||||||
dateTime: updateFields.end,
|
|
||||||
timeZone: updateFields.timeZone || this.getTimezone(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (updateFields.attendees) {
|
|
||||||
body.attendees = [];
|
|
||||||
(updateFields.attendees as string[]).forEach(attendee => {
|
|
||||||
body.attendees!.push.apply(body.attendees, attendee.split(',').map(a => a.trim()).map(email => ({ email })));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (updateFields.color) {
|
|
||||||
body.colorId = updateFields.color as string;
|
|
||||||
}
|
|
||||||
if (updateFields.description) {
|
|
||||||
body.description = updateFields.description as string;
|
|
||||||
}
|
|
||||||
if (updateFields.guestsCanInviteOthers) {
|
|
||||||
body.guestsCanInviteOthers = updateFields.guestsCanInviteOthers as boolean;
|
|
||||||
}
|
|
||||||
if (updateFields.guestsCanModify) {
|
|
||||||
body.guestsCanModify = updateFields.guestsCanModify as boolean;
|
|
||||||
}
|
|
||||||
if (updateFields.guestsCanSeeOtherGuests) {
|
|
||||||
body.guestsCanSeeOtherGuests = updateFields.guestsCanSeeOtherGuests as boolean;
|
|
||||||
}
|
|
||||||
if (updateFields.id) {
|
|
||||||
body.id = updateFields.id as string;
|
|
||||||
}
|
|
||||||
if (updateFields.location) {
|
|
||||||
body.location = updateFields.location as string;
|
|
||||||
}
|
|
||||||
if (updateFields.summary) {
|
|
||||||
body.summary = updateFields.summary as string;
|
|
||||||
}
|
|
||||||
if (updateFields.showMeAs) {
|
|
||||||
body.transparency = updateFields.showMeAs as string;
|
|
||||||
}
|
|
||||||
if (updateFields.visibility) {
|
|
||||||
body.visibility = updateFields.visibility as string;
|
|
||||||
}
|
|
||||||
if (!useDefaultReminders) {
|
|
||||||
const reminders = (this.getNodeParameter(
|
|
||||||
'remindersUi',
|
|
||||||
i,
|
|
||||||
) as IDataObject).remindersValues as IDataObject[];
|
|
||||||
body.reminders = {
|
|
||||||
useDefault: false,
|
|
||||||
};
|
|
||||||
if (reminders) {
|
|
||||||
body.reminders.overrides = reminders;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updateFields.allday && updateFields.start && updateFields.end) {
|
|
||||||
body.start = {
|
|
||||||
date: moment(updateFields.start as string)
|
|
||||||
.utc()
|
|
||||||
.format('YYYY-MM-DD'),
|
|
||||||
};
|
|
||||||
body.end = {
|
|
||||||
date: moment(updateFields.end as string)
|
|
||||||
.utc()
|
|
||||||
.format('YYYY-MM-DD'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
//exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z
|
|
||||||
//https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
|
|
||||||
body.recurrence = [];
|
|
||||||
if (updateFields.rrule) {
|
|
||||||
body.recurrence = [`RRULE:${updateFields.rrule}`];
|
|
||||||
} else {
|
|
||||||
if (updateFields.repeatHowManyTimes && updateFields.repeatUntil) {
|
|
||||||
throw new NodeOperationError(this.getNode(),
|
|
||||||
`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (updateFields.repeatFrecuency) {
|
|
||||||
body.recurrence?.push(
|
|
||||||
`FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (updateFields.repeatHowManyTimes) {
|
|
||||||
body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`);
|
|
||||||
}
|
|
||||||
if (updateFields.repeatUntil) {
|
|
||||||
body.recurrence?.push(
|
|
||||||
`UNTIL=${moment(updateFields.repeatUntil as string)
|
|
||||||
.utc()
|
|
||||||
.format('YYYYMMDDTHHmmss')}Z`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (body.recurrence.length !== 0) {
|
|
||||||
body.recurrence = [`RRULE:${body.recurrence.join('')}`];
|
|
||||||
} else {
|
|
||||||
delete body.recurrence;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
responseData = await googleApiRequest.call(
|
|
||||||
this,
|
|
||||||
'PATCH',
|
|
||||||
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
|
||||||
body,
|
|
||||||
qs,
|
|
||||||
);
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,11 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generic": [
|
"generic": [
|
||||||
|
{
|
||||||
|
"label": "Hey founders! Your business doesn't need you to operate",
|
||||||
|
"icon": " 🖥️",
|
||||||
|
"url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Why this Product Manager loves workflow automation with n8n",
|
"label": "Why this Product Manager loves workflow automation with n8n",
|
||||||
"icon": "🧠",
|
"icon": "🧠",
|
||||||
|
|
|
@ -274,6 +274,32 @@ export class GoogleDrive implements INodeType {
|
||||||
},
|
},
|
||||||
description: 'Name of the binary property to which to<br />write the data of the read file.',
|
description: 'Name of the binary property to which to<br />write the data of the read file.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'options',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Option',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'download',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'File Name',
|
||||||
|
name: 'fileName',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'File name. Ex: data.pdf',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -2011,6 +2037,7 @@ export class GoogleDrive implements INodeType {
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
||||||
const fileId = this.getNodeParameter('fileId', i) as string;
|
const fileId = this.getNodeParameter('fileId', i) as string;
|
||||||
|
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
|
@ -2021,10 +2048,15 @@ export class GoogleDrive implements INodeType {
|
||||||
const response = await googleApiRequest.call(this, 'GET', `/drive/v3/files/${fileId}`, {}, { alt: 'media' }, undefined, requestOptions);
|
const response = await googleApiRequest.call(this, 'GET', `/drive/v3/files/${fileId}`, {}, { alt: 'media' }, undefined, requestOptions);
|
||||||
|
|
||||||
let mimeType: string | undefined;
|
let mimeType: string | undefined;
|
||||||
|
let fileName: string | undefined = undefined;
|
||||||
if (response.headers['content-type']) {
|
if (response.headers['content-type']) {
|
||||||
mimeType = response.headers['content-type'];
|
mimeType = response.headers['content-type'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.fileName) {
|
||||||
|
fileName = options.fileName as string;
|
||||||
|
}
|
||||||
|
|
||||||
const newItem: INodeExecutionData = {
|
const newItem: INodeExecutionData = {
|
||||||
json: items[i].json,
|
json: items[i].json,
|
||||||
binary: {},
|
binary: {},
|
||||||
|
@ -2043,7 +2075,7 @@ export class GoogleDrive implements INodeType {
|
||||||
|
|
||||||
const data = Buffer.from(response.body as string);
|
const data = Buffer.from(response.body as string);
|
||||||
|
|
||||||
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, undefined, mimeType);
|
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, fileName, mimeType);
|
||||||
|
|
||||||
} else if (operation === 'list') {
|
} else if (operation === 'list') {
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
|
@ -112,9 +112,9 @@ export function documentToJson(fields: IDataObject): IDataObject {
|
||||||
for (const f of Object.keys(fields)) {
|
for (const f of Object.keys(fields)) {
|
||||||
const key = f, value = fields[f],
|
const key = f, value = fields[f],
|
||||||
isDocumentType = ['stringValue', 'booleanValue', 'doubleValue',
|
isDocumentType = ['stringValue', 'booleanValue', 'doubleValue',
|
||||||
'integerValue', 'timestampValue', 'mapValue', 'arrayValue'].find(t => t === key);
|
'integerValue', 'timestampValue', 'mapValue', 'arrayValue', 'nullValue'].find(t => t === key);
|
||||||
if (isDocumentType) {
|
if (isDocumentType) {
|
||||||
const item = ['stringValue', 'booleanValue', 'doubleValue', 'integerValue', 'timestampValue']
|
const item = ['stringValue', 'booleanValue', 'doubleValue', 'integerValue', 'timestampValue', 'nullValue']
|
||||||
.find(t => t === key);
|
.find(t => t === key);
|
||||||
if (item) {
|
if (item) {
|
||||||
return value as IDataObject;
|
return value as IDataObject;
|
||||||
|
|
|
@ -209,7 +209,8 @@ export const draftFields = [
|
||||||
name: 'property',
|
name: 'property',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
description: 'Name of the binary property containing the data to be added to the email as an attachment',
|
description: `Name of the binary property containing the data to be added to the email as an attachment.</br>
|
||||||
|
Multiples can be set separated by comma.`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,6 +27,11 @@
|
||||||
"icon": "🎫",
|
"icon": "🎫",
|
||||||
"url": "https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/"
|
"url": "https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Hey founders! Your business doesn't need you to operate",
|
||||||
|
"icon": " 🖥️",
|
||||||
|
"url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Using Automation to Boost Productivity in the Workplace",
|
"label": "Using Automation to Boost Productivity in the Workplace",
|
||||||
"icon": "💪",
|
"icon": "💪",
|
||||||
|
|
|
@ -325,28 +325,29 @@ export class Gmail implements INodeType {
|
||||||
|
|
||||||
if (additionalFields.attachmentsUi) {
|
if (additionalFields.attachmentsUi) {
|
||||||
const attachmentsUi = additionalFields.attachmentsUi as IDataObject;
|
const attachmentsUi = additionalFields.attachmentsUi as IDataObject;
|
||||||
let attachmentsBinary = [];
|
const attachmentsBinary = [];
|
||||||
if (!isEmpty(attachmentsUi)) {
|
if (!isEmpty(attachmentsUi)) {
|
||||||
if (attachmentsUi.hasOwnProperty('attachmentsBinary')
|
if (attachmentsUi.hasOwnProperty('attachmentsBinary')
|
||||||
&& !isEmpty(attachmentsUi.attachmentsBinary)
|
&& !isEmpty(attachmentsUi.attachmentsBinary)
|
||||||
&& items[i].binary) {
|
&& items[i].binary) {
|
||||||
// @ts-ignore
|
for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) {
|
||||||
attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => {
|
for (const binaryProperty of (property as string).split(',')) {
|
||||||
if (items[i].binary!.hasOwnProperty(value.property)) {
|
if (items[i].binary![binaryProperty] !== undefined) {
|
||||||
const aux: IAttachments = { name: '', content: '', type: '' };
|
const binaryData = items[i].binary![binaryProperty];
|
||||||
aux.name = items[i].binary![value.property].fileName || 'unknown';
|
attachmentsBinary.push({
|
||||||
aux.content = items[i].binary![value.property].data;
|
name: binaryData.fileName || 'unknown',
|
||||||
aux.type = items[i].binary![value.property].mimeType;
|
content: binaryData.data,
|
||||||
return aux;
|
type: binaryData.mimeType,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
qs = {
|
qs = {
|
||||||
userId: 'me',
|
userId: 'me',
|
||||||
uploadType: 'media',
|
uploadType: 'media',
|
||||||
};
|
};
|
||||||
|
|
||||||
attachmentsList = attachmentsBinary;
|
attachmentsList = attachmentsBinary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -408,32 +409,32 @@ export class Gmail implements INodeType {
|
||||||
|
|
||||||
if (additionalFields.attachmentsUi) {
|
if (additionalFields.attachmentsUi) {
|
||||||
const attachmentsUi = additionalFields.attachmentsUi as IDataObject;
|
const attachmentsUi = additionalFields.attachmentsUi as IDataObject;
|
||||||
let attachmentsBinary = [];
|
const attachmentsBinary = [];
|
||||||
if (!isEmpty(attachmentsUi)) {
|
if (!isEmpty(attachmentsUi)) {
|
||||||
if (attachmentsUi.hasOwnProperty('attachmentsBinary')
|
if (attachmentsUi.hasOwnProperty('attachmentsBinary')
|
||||||
&& !isEmpty(attachmentsUi.attachmentsBinary)
|
&& !isEmpty(attachmentsUi.attachmentsBinary)
|
||||||
&& items[i].binary) {
|
&& items[i].binary) {
|
||||||
// @ts-ignore
|
for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) {
|
||||||
attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => {
|
for (const binaryProperty of (property as string).split(',')) {
|
||||||
if (items[i].binary!.hasOwnProperty(value.property)) {
|
if (items[i].binary![binaryProperty] !== undefined) {
|
||||||
const aux: IAttachments = { name: '', content: '', type: '' };
|
const binaryData = items[i].binary![binaryProperty];
|
||||||
aux.name = items[i].binary![value.property].fileName || 'unknown';
|
attachmentsBinary.push({
|
||||||
aux.content = items[i].binary![value.property].data;
|
name: binaryData.fileName || 'unknown',
|
||||||
aux.type = items[i].binary![value.property].mimeType;
|
content: binaryData.data,
|
||||||
return aux;
|
type: binaryData.mimeType,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
qs = {
|
qs = {
|
||||||
userId: 'me',
|
userId: 'me',
|
||||||
uploadType: 'media',
|
uploadType: 'media',
|
||||||
};
|
};
|
||||||
|
|
||||||
attachmentsList = attachmentsBinary;
|
attachmentsList = attachmentsBinary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no recipient is defined then grab the one who sent the email
|
// if no recipient is defined then grab the one who sent the email
|
||||||
if (toStr === '') {
|
if (toStr === '') {
|
||||||
endpoint = `/gmail/v1/users/me/messages/${id}`;
|
endpoint = `/gmail/v1/users/me/messages/${id}`;
|
||||||
|
@ -500,7 +501,7 @@ export class Gmail implements INodeType {
|
||||||
const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_';
|
const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_';
|
||||||
|
|
||||||
nodeExecutionData = await parseRawEmail.call(this, responseData, dataPropertyNameDownload);
|
nodeExecutionData = await parseRawEmail.call(this, responseData, dataPropertyNameDownload);
|
||||||
} else {
|
} else {
|
||||||
nodeExecutionData = {
|
nodeExecutionData = {
|
||||||
json: responseData,
|
json: responseData,
|
||||||
};
|
};
|
||||||
|
@ -628,28 +629,29 @@ export class Gmail implements INodeType {
|
||||||
|
|
||||||
if (additionalFields.attachmentsUi) {
|
if (additionalFields.attachmentsUi) {
|
||||||
const attachmentsUi = additionalFields.attachmentsUi as IDataObject;
|
const attachmentsUi = additionalFields.attachmentsUi as IDataObject;
|
||||||
let attachmentsBinary = [];
|
const attachmentsBinary = [];
|
||||||
if (!isEmpty(attachmentsUi)) {
|
if (!isEmpty(attachmentsUi)) {
|
||||||
if (attachmentsUi.hasOwnProperty('attachmentsBinary')
|
if (attachmentsUi.hasOwnProperty('attachmentsBinary')
|
||||||
&& !isEmpty(attachmentsUi.attachmentsBinary)
|
&& !isEmpty(attachmentsUi.attachmentsBinary)
|
||||||
&& items[i].binary) {
|
&& items[i].binary) {
|
||||||
// @ts-ignore
|
for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) {
|
||||||
attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => {
|
for (const binaryProperty of (property as string).split(',')) {
|
||||||
if (items[i].binary!.hasOwnProperty(value.property)) {
|
if (items[i].binary![binaryProperty] !== undefined) {
|
||||||
const aux: IAttachments = { name: '', content: '', type: '' };
|
const binaryData = items[i].binary![binaryProperty];
|
||||||
aux.name = items[i].binary![value.property].fileName || 'unknown';
|
attachmentsBinary.push({
|
||||||
aux.content = items[i].binary![value.property].data;
|
name: binaryData.fileName || 'unknown',
|
||||||
aux.type = items[i].binary![value.property].mimeType;
|
content: binaryData.data,
|
||||||
return aux;
|
type: binaryData.mimeType,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
qs = {
|
qs = {
|
||||||
userId: 'me',
|
userId: 'me',
|
||||||
uploadType: 'media',
|
uploadType: 'media',
|
||||||
};
|
};
|
||||||
|
|
||||||
attachmentsList = attachmentsBinary;
|
attachmentsList = attachmentsBinary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,30 +226,6 @@ export const messageFields = [
|
||||||
},
|
},
|
||||||
default: {},
|
default: {},
|
||||||
options: [
|
options: [
|
||||||
{
|
|
||||||
displayName: 'CC Email',
|
|
||||||
name: 'ccList',
|
|
||||||
type: 'string',
|
|
||||||
description: 'The email addresses of the copy recipients.',
|
|
||||||
typeOptions: {
|
|
||||||
multipleValues: true,
|
|
||||||
multipleValueButtonText: 'Add CC Email',
|
|
||||||
},
|
|
||||||
placeholder: 'info@example.com',
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'BCC Email',
|
|
||||||
name: 'bccList',
|
|
||||||
type: 'string',
|
|
||||||
description: 'The email addresses of the blind copy recipients.',
|
|
||||||
typeOptions: {
|
|
||||||
multipleValues: true,
|
|
||||||
multipleValueButtonText: 'Add BCC Email',
|
|
||||||
},
|
|
||||||
placeholder: 'info@example.com',
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
displayName: 'Attachments',
|
displayName: 'Attachments',
|
||||||
name: 'attachmentsUi',
|
name: 'attachmentsUi',
|
||||||
|
@ -268,7 +244,8 @@ export const messageFields = [
|
||||||
name: 'property',
|
name: 'property',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
description: 'Name of the binary properties which contain data which should be added to email as attachment',
|
description: `Name of the binary property containing the data to be added to the email as an attachment.</br>
|
||||||
|
Multiples can be set separated by comma.`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -276,6 +253,30 @@ export const messageFields = [
|
||||||
default: '',
|
default: '',
|
||||||
description: 'Array of supported attachments to add to the message.',
|
description: 'Array of supported attachments to add to the message.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'BCC Email',
|
||||||
|
name: 'bccList',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The email addresses of the blind copy recipients.',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
multipleValueButtonText: 'Add BCC Email',
|
||||||
|
},
|
||||||
|
placeholder: 'info@example.com',
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'CC Email',
|
||||||
|
name: 'ccList',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The email addresses of the copy recipients.',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
multipleValueButtonText: 'Add CC Email',
|
||||||
|
},
|
||||||
|
placeholder: 'info@example.com',
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -415,6 +415,12 @@ export class GoogleSheet {
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
inputData[rowIndex][i] = '';
|
inputData[rowIndex][i] = '';
|
||||||
}
|
}
|
||||||
|
} else if (inputData[rowIndex].length < keys.length) {
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
if (inputData[rowIndex][i] === undefined) {
|
||||||
|
inputData[rowIndex].push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Loop over all the lookup values and try to find a row to return
|
// Loop over all the lookup values and try to find a row to return
|
||||||
|
|
|
@ -48,6 +48,11 @@
|
||||||
"icon": "📈",
|
"icon": "📈",
|
||||||
"url": "https://n8n.io/blog/migrating-community-metrics-to-orbit-using-n8n/"
|
"url": "https://n8n.io/blog/migrating-community-metrics-to-orbit-using-n8n/"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Hey founders! Your business doesn't need you to operate",
|
||||||
|
"icon": " 🖥️",
|
||||||
|
"url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "How Honest Burgers Use Automation to Save $100k per year",
|
"label": "How Honest Burgers Use Automation to Save $100k per year",
|
||||||
"icon": "🍔",
|
"icon": "🍔",
|
||||||
|
|
|
@ -17,6 +17,11 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generic": [
|
"generic": [
|
||||||
|
{
|
||||||
|
"label": "Hey founders! Your business doesn't need you to operate",
|
||||||
|
"icon": " 🖥️",
|
||||||
|
"url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin",
|
"label": "Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin",
|
||||||
"icon": "🎖",
|
"icon": "🎖",
|
||||||
|
|
85
packages/nodes-base/nodes/Kitemaker/GenericFunctions.ts
Normal file
85
packages/nodes-base/nodes/Kitemaker/GenericFunctions.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
IHookFunctions,
|
||||||
|
NodeApiError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export async function kitemakerRequest(
|
||||||
|
this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions,
|
||||||
|
body: IDataObject = {},
|
||||||
|
) {
|
||||||
|
const { personalAccessToken } = this.getCredentials('kitemakerApi') as { personalAccessToken: string };
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${personalAccessToken}`,
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
uri: 'https://toil.kitemaker.co/developers/graphql',
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseData = await this.helpers.request!.call(this, options);
|
||||||
|
|
||||||
|
if (responseData.errors) {
|
||||||
|
throw new NodeApiError(this.getNode(), responseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function kitemakerRequestAllItems(
|
||||||
|
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||||
|
body: { query: string; variables: { [key: string]: string } },
|
||||||
|
) {
|
||||||
|
const resource = this.getNodeParameter('resource', 0) as 'space' | 'user' | 'workItem';
|
||||||
|
const [group, items] = getGroupAndItems(resource);
|
||||||
|
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean;
|
||||||
|
const limit = this.getNodeParameter('limit', 0, 0) as number;
|
||||||
|
|
||||||
|
const returnData: IDataObject[] = [];
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
do {
|
||||||
|
responseData = await kitemakerRequest.call(this, body);
|
||||||
|
body.variables.cursor = responseData.data[group].cursor;
|
||||||
|
returnData.push(...responseData.data[group][items]);
|
||||||
|
|
||||||
|
if (!returnAll && returnData.length > limit) {
|
||||||
|
return returnData.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
} while (responseData.data[group].hasMore);
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGroupAndItems(resource: 'space' | 'user' | 'workItem') {
|
||||||
|
const map: { [key: string]: { [key: string]: string } } = {
|
||||||
|
space: { group: 'organization', items: 'spaces' },
|
||||||
|
user: { group: 'organization', items: 'users' },
|
||||||
|
workItem: { group: 'workItems', items: 'workItems' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
map[resource]['group'],
|
||||||
|
map[resource]['items'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLoadOptions(
|
||||||
|
resources: Array<{ name?: string; username?: string; title?: string; id: string }>,
|
||||||
|
): Array<{ name: string; value: string }> {
|
||||||
|
return resources.map(option => {
|
||||||
|
if (option.username) return ({ name: option.username, value: option.id });
|
||||||
|
if (option.title) return ({ name: option.title, value: option.id });
|
||||||
|
return ({ name: option.name ?? 'Unnamed', value: option.id });
|
||||||
|
});
|
||||||
|
}
|
321
packages/nodes-base/nodes/Kitemaker/Kitemaker.node.ts
Normal file
321
packages/nodes-base/nodes/Kitemaker/Kitemaker.node.ts
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
import {
|
||||||
|
IExecuteFunctions
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
organizationOperations,
|
||||||
|
spaceFields,
|
||||||
|
spaceOperations,
|
||||||
|
userFields,
|
||||||
|
userOperations,
|
||||||
|
workItemFields,
|
||||||
|
workItemOperations,
|
||||||
|
} from './descriptions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createLoadOptions,
|
||||||
|
kitemakerRequest,
|
||||||
|
kitemakerRequestAllItems,
|
||||||
|
} from './GenericFunctions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAllSpaces,
|
||||||
|
getAllUsers,
|
||||||
|
getAllWorkItems,
|
||||||
|
getLabels,
|
||||||
|
getOrganization,
|
||||||
|
getSpaces,
|
||||||
|
getStatuses,
|
||||||
|
getUsers,
|
||||||
|
getWorkItem,
|
||||||
|
getWorkItems,
|
||||||
|
} from './queries';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createWorkItem,
|
||||||
|
editWorkItem,
|
||||||
|
} from './mutations';
|
||||||
|
|
||||||
|
export class Kitemaker implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Kitemaker',
|
||||||
|
name: 'kitemaker',
|
||||||
|
icon: 'file:kitemaker.svg',
|
||||||
|
group: ['input'],
|
||||||
|
version: 1,
|
||||||
|
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
|
||||||
|
description: 'Consume the Kitemaker GraphQL API',
|
||||||
|
defaults: {
|
||||||
|
name: 'Kitemaker',
|
||||||
|
color: '#662482',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'kitemakerApi',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Resource',
|
||||||
|
name: 'resource',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Organization',
|
||||||
|
value: 'organization',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Space',
|
||||||
|
value: 'space',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'User',
|
||||||
|
value: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Work Item',
|
||||||
|
value: 'workItem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'workItem',
|
||||||
|
required: true,
|
||||||
|
description: 'Resource to operate on.',
|
||||||
|
},
|
||||||
|
...organizationOperations,
|
||||||
|
...spaceOperations,
|
||||||
|
...spaceFields,
|
||||||
|
...userOperations,
|
||||||
|
...userFields,
|
||||||
|
...workItemOperations,
|
||||||
|
...workItemFields,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
methods = {
|
||||||
|
loadOptions: {
|
||||||
|
async getLabels(this: ILoadOptionsFunctions) {
|
||||||
|
const responseData = await kitemakerRequest.call(this, { query: getLabels });
|
||||||
|
const { data: { organization: { spaces } } } = responseData;
|
||||||
|
|
||||||
|
return createLoadOptions(spaces[0].labels);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSpaces(this: ILoadOptionsFunctions) {
|
||||||
|
const responseData = await kitemakerRequest.call(this, { query: getSpaces });
|
||||||
|
const { data: { organization: { spaces } } } = responseData;
|
||||||
|
|
||||||
|
return createLoadOptions(spaces);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getStatuses(this: ILoadOptionsFunctions) {
|
||||||
|
const responseData = await kitemakerRequest.call(this, { query: getStatuses });
|
||||||
|
const { data: { organization: { spaces } } } = responseData;
|
||||||
|
|
||||||
|
return createLoadOptions(spaces[0].statuses);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUsers(this: ILoadOptionsFunctions) {
|
||||||
|
const responseData = await kitemakerRequest.call(this, { query: getUsers });
|
||||||
|
const { data: { organization: { users } } } = responseData;
|
||||||
|
|
||||||
|
return createLoadOptions(users);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getWorkItems(this: ILoadOptionsFunctions) {
|
||||||
|
const spaceId = this.getNodeParameter('spaceId', 0) as string;
|
||||||
|
|
||||||
|
const responseData = await kitemakerRequest.call(this, {
|
||||||
|
query: getWorkItems,
|
||||||
|
variables: { spaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: { workItems: { workItems } } } = responseData;
|
||||||
|
|
||||||
|
return createLoadOptions(workItems);
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
const resource = this.getNodeParameter('resource', 0);
|
||||||
|
const operation = this.getNodeParameter('operation', 0);
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
const returnData: IDataObject[] = [];
|
||||||
|
|
||||||
|
// https://github.com/kitemakerhq/docs/blob/main/kitemaker.graphql
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
|
||||||
|
if (resource === 'organization') {
|
||||||
|
|
||||||
|
// *********************************************************************
|
||||||
|
// organization
|
||||||
|
// *********************************************************************
|
||||||
|
|
||||||
|
if (operation === 'get') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// organization: get
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
responseData = await kitemakerRequest.call(this, {
|
||||||
|
query: getOrganization,
|
||||||
|
});
|
||||||
|
|
||||||
|
returnData.push(responseData.data.organization);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (resource === 'space') {
|
||||||
|
|
||||||
|
// *********************************************************************
|
||||||
|
// space
|
||||||
|
// *********************************************************************
|
||||||
|
|
||||||
|
if (operation === 'getAll') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// space: getAll
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
const allItems = await kitemakerRequestAllItems.call(this, {
|
||||||
|
query: getAllSpaces,
|
||||||
|
variables: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
returnData.push(...allItems);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (resource === 'user') {
|
||||||
|
|
||||||
|
// *********************************************************************
|
||||||
|
// user
|
||||||
|
// *********************************************************************
|
||||||
|
|
||||||
|
if (operation === 'getAll') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// user: getAll
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
const allItems = await kitemakerRequestAllItems.call(this, {
|
||||||
|
query: getAllUsers,
|
||||||
|
variables: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
returnData.push(...allItems);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (resource === 'workItem') {
|
||||||
|
|
||||||
|
// *********************************************************************
|
||||||
|
// workItem
|
||||||
|
// *********************************************************************
|
||||||
|
|
||||||
|
if (operation === 'create') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// workItem: create
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
title: this.getNodeParameter('title', i) as string,
|
||||||
|
statusId: this.getNodeParameter('statusId', i) as string[],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!input.statusId.length) {
|
||||||
|
throw new Error('Please enter a status to set for the work item to create.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||||
|
|
||||||
|
if (Object.keys(additionalFields).length) {
|
||||||
|
Object.assign(input, additionalFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await kitemakerRequest.call(this, {
|
||||||
|
query: createWorkItem,
|
||||||
|
variables: { input },
|
||||||
|
});
|
||||||
|
|
||||||
|
returnData.push(responseData.data.createWorkItem.workItem);
|
||||||
|
|
||||||
|
} else if (operation === 'get') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// workItem: get
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
const workItemId = this.getNodeParameter('workItemId', i) as string;
|
||||||
|
|
||||||
|
responseData = await kitemakerRequest.call(this, {
|
||||||
|
query: getWorkItem,
|
||||||
|
variables: { workItemId },
|
||||||
|
});
|
||||||
|
|
||||||
|
returnData.push(responseData.data.workItem);
|
||||||
|
|
||||||
|
} else if (operation === 'getAll') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// workItem: getAll
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
const allItems = await kitemakerRequestAllItems.call(this, {
|
||||||
|
query: getAllWorkItems,
|
||||||
|
variables: {
|
||||||
|
spaceId: this.getNodeParameter('spaceId', i) as string,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
returnData.push(...allItems);
|
||||||
|
|
||||||
|
} else if (operation === 'update') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// workItem: update
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
id: this.getNodeParameter('workItemId', i),
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
|
||||||
|
|
||||||
|
if (!Object.keys(updateFields).length) {
|
||||||
|
throw new Error('Please enter at least one field to update for the work item.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(input, updateFields);
|
||||||
|
|
||||||
|
responseData = await kitemakerRequest.call(this, {
|
||||||
|
query: editWorkItem,
|
||||||
|
variables: { input },
|
||||||
|
});
|
||||||
|
|
||||||
|
returnData.push(responseData.data.editWorkItem.workItem);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [this.helpers.returnJsonArray(returnData)];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import {
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const organizationOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
default: 'get',
|
||||||
|
description: 'Operation to perform.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Get',
|
||||||
|
value: 'get',
|
||||||
|
description: 'Retrieve data on the logged-in user\'s organization.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'organization',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
|
@ -0,0 +1,71 @@
|
||||||
|
import {
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const spaceOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
default: 'getAll',
|
||||||
|
description: 'Operation to perform.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Get All',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Retrieve data on all the spaces in the<br>logged-in user\'s organization.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'space',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
||||||
|
|
||||||
|
export const spaceFields = [
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Return all results.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'space',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
default: 5,
|
||||||
|
description: 'The number of results to return.',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
maxValue: 1000,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'space',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
returnAll: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
|
@ -0,0 +1,71 @@
|
||||||
|
import {
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const userOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
default: 'getAll',
|
||||||
|
description: 'Operation to perform.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Get All',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Retrieve data on all the users in the<br>logged-in user\'s organization.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'user',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
||||||
|
|
||||||
|
export const userFields = [
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Return all results.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'user',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
default: 5,
|
||||||
|
description: 'The number of results to return.',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
maxValue: 1000,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'user',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
returnAll: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
|
@ -0,0 +1,372 @@
|
||||||
|
import {
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const workItemOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
default: 'get',
|
||||||
|
description: 'Operation to perform.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get',
|
||||||
|
value: 'get',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get All',
|
||||||
|
value: 'getAll',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Update',
|
||||||
|
value: 'update',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'workItem',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
||||||
|
|
||||||
|
export const workItemFields = [
|
||||||
|
// ----------------------------------
|
||||||
|
// workItem: create
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Title',
|
||||||
|
name: 'title',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'Title of the work item to create.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'workItem',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Status ID',
|
||||||
|
name: 'statusId',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getStatuses',
|
||||||
|
},
|
||||||
|
default: [],
|
||||||
|
required: true,
|
||||||
|
description: 'ID of the status to set on the item to create.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'workItem',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'workItem',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Description',
|
||||||
|
name: 'description',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
typeOptions: {
|
||||||
|
alwaysOpenEditWindow: true,
|
||||||
|
},
|
||||||
|
description: 'Description of the item to create. Markdown supported.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Effort',
|
||||||
|
name: 'effort',
|
||||||
|
type: 'options',
|
||||||
|
default: 'SMALL',
|
||||||
|
description: 'Effort to set for the item to create.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Small',
|
||||||
|
value: 'SMALL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Medium',
|
||||||
|
value: 'MEDIUM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Large',
|
||||||
|
value: 'LARGE',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Impact',
|
||||||
|
name: 'impact',
|
||||||
|
type: 'options',
|
||||||
|
default: 'SMALL',
|
||||||
|
description: 'Impact to set for the item to create.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Small',
|
||||||
|
value: 'SMALL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Medium',
|
||||||
|
value: 'MEDIUM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Large',
|
||||||
|
value: 'LARGE',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Label IDs',
|
||||||
|
name: 'labelIds',
|
||||||
|
type: 'multiOptions',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getLabels',
|
||||||
|
},
|
||||||
|
default: [],
|
||||||
|
description: 'ID of the label to set on the item to create.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Member IDs',
|
||||||
|
name: 'memberIds',
|
||||||
|
type: 'multiOptions',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getUsers',
|
||||||
|
},
|
||||||
|
default: [],
|
||||||
|
description: 'ID of the user to assign to the item to create.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// workItem: get
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Work Item ID',
|
||||||
|
name: 'workItemId',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'ID of the work item to retrieve.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'workItem',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'get',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// workItem: getAll
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Space ID',
|
||||||
|
name: 'spaceId',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getSpaces',
|
||||||
|
},
|
||||||
|
default: [],
|
||||||
|
required: true,
|
||||||
|
description: 'ID of the space to retrieve the work items from.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'workItem',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Return all results.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'workItem',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
default: 5,
|
||||||
|
description: 'The number of results to return.',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
maxValue: 1000,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'workItem',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
returnAll: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// workItem: update
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Work Item ID',
|
||||||
|
name: 'workItemId',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'ID of the work item to update.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'workItem',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Update Fields',
|
||||||
|
name: 'updateFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'workItem',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Description',
|
||||||
|
name: 'description',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
typeOptions: {
|
||||||
|
alwaysOpenEditWindow: true,
|
||||||
|
},
|
||||||
|
description: 'Description of the item to update. Markdown supported.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Effort',
|
||||||
|
name: 'effort',
|
||||||
|
type: 'options',
|
||||||
|
default: 'SMALL',
|
||||||
|
description: 'Effort to set for the item to update.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Small',
|
||||||
|
value: 'SMALL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Medium',
|
||||||
|
value: 'MEDIUM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Large',
|
||||||
|
value: 'LARGE',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Impact',
|
||||||
|
name: 'impact',
|
||||||
|
type: 'options',
|
||||||
|
default: 'SMALL',
|
||||||
|
description: 'Impact to set for the item to update.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Small',
|
||||||
|
value: 'SMALL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Medium',
|
||||||
|
value: 'MEDIUM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Large',
|
||||||
|
value: 'LARGE',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Status ID',
|
||||||
|
name: 'statusId',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getStatuses',
|
||||||
|
},
|
||||||
|
default: [],
|
||||||
|
description: 'ID of the status to set on the item to update.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Title',
|
||||||
|
name: 'title',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Title to set for the work item to update.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './OrganizationDescription';
|
||||||
|
export * from './SpaceDescription';
|
||||||
|
export * from './UserDescription';
|
||||||
|
export * from './WorkItemDescription';
|
1
packages/nodes-base/nodes/Kitemaker/kitemaker.svg
Normal file
1
packages/nodes-base/nodes/Kitemaker/kitemaker.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="226.667" height="226.667" viewBox="-30 -25 220 220"><path fill="#662482" d="M106.5 25.5c-30.2 14-56 25.4-57.2 25.4-1.7.1-6.1-5-20-22.9C11.5 5.1 8.7 2.4 5.1 4.7c-1.4.9-1.6 8.9-1.9 79.3-.1 57 .1 78.6.9 79.6 3.5 4.3 3.3 4.6 85.2-77.4C145.1 30.4 168 6.8 168 5.3c0-2.6-2.5-5.3-4.8-5.2-.9 0-26.4 11.5-56.7 25.4z"/><path fill="#e61b73" d="M69.4 125.2c-13.3 13.4-24.4 25.1-24.7 26-.3.9.2 2.6 1.1 3.8 1.6 1.8 6.7 2.6 59.9 8.5 32 3.5 58.9 6.2 59.7 5.9 1.9-.7 2.9-3.8 2.1-6.2-.3-.9-15.9-15.2-34.6-31.7-24.1-21.2-34.8-30.1-36.7-30.3-2.3-.2-5.9 3-26.8 24z"/></svg>
|
After Width: | Height: | Size: 612 B |
69
packages/nodes-base/nodes/Kitemaker/mutations.ts
Normal file
69
packages/nodes-base/nodes/Kitemaker/mutations.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
// ----------------------------------
|
||||||
|
// mutations
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
export const createWorkItem = `
|
||||||
|
mutation($input: CreateWorkItemInput!) {
|
||||||
|
createWorkItem(input: $input) {
|
||||||
|
workItem {
|
||||||
|
id
|
||||||
|
number
|
||||||
|
title
|
||||||
|
description
|
||||||
|
status {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
members {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
watchers {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
labels {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
effort
|
||||||
|
impact
|
||||||
|
updatedAt
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const editWorkItem = `
|
||||||
|
mutation ($input: EditWorkItemInput!) {
|
||||||
|
editWorkItem(input: $input) {
|
||||||
|
workItem {
|
||||||
|
id
|
||||||
|
number
|
||||||
|
title
|
||||||
|
description
|
||||||
|
status {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
members {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
watchers {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
labels {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
effort
|
||||||
|
impact
|
||||||
|
updatedAt
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
199
packages/nodes-base/nodes/Kitemaker/queries.ts
Normal file
199
packages/nodes-base/nodes/Kitemaker/queries.ts
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
// ----------------------------------
|
||||||
|
// queries
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
export const getAllSpaces = `
|
||||||
|
query {
|
||||||
|
organization {
|
||||||
|
spaces {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
labels {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
color
|
||||||
|
}
|
||||||
|
statuses {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
type
|
||||||
|
default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getAllUsers = `
|
||||||
|
query {
|
||||||
|
organization {
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getLabels = `
|
||||||
|
query {
|
||||||
|
organization {
|
||||||
|
spaces {
|
||||||
|
labels {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getOrganization = `
|
||||||
|
query {
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getSpaces = `
|
||||||
|
query {
|
||||||
|
organization {
|
||||||
|
spaces {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
labels {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
color
|
||||||
|
}
|
||||||
|
statuses {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
type
|
||||||
|
default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getStatuses = `
|
||||||
|
query {
|
||||||
|
organization {
|
||||||
|
spaces {
|
||||||
|
statuses {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
type
|
||||||
|
default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getUsers = `
|
||||||
|
query {
|
||||||
|
organization {
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getWorkItems = `
|
||||||
|
query($spaceId: ID!) {
|
||||||
|
workItems(spaceId: $spaceId) {
|
||||||
|
workItems {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getWorkItem = `
|
||||||
|
query($workItemId: ID!) {
|
||||||
|
workItem(id: $workItemId) {
|
||||||
|
id
|
||||||
|
number
|
||||||
|
title
|
||||||
|
description
|
||||||
|
status {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
sort
|
||||||
|
members {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
watchers {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
labels {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
comments {
|
||||||
|
id
|
||||||
|
actor {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
body
|
||||||
|
threadId
|
||||||
|
updatedAt
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
effort
|
||||||
|
impact
|
||||||
|
updatedAt
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getAllWorkItems = `
|
||||||
|
query($spaceId: ID!, $cursor: String) {
|
||||||
|
workItems(spaceId: $spaceId, cursor: $cursor) {
|
||||||
|
hasMore,
|
||||||
|
cursor,
|
||||||
|
workItems {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
labels {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
comments {
|
||||||
|
id
|
||||||
|
body
|
||||||
|
actor {
|
||||||
|
... on User {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
... on IntegrationUser {
|
||||||
|
id
|
||||||
|
externalName
|
||||||
|
}
|
||||||
|
... on Integration {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
}
|
||||||
|
... on Application {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
21
packages/nodes-base/nodes/MQTT/Mqtt.node.json
Normal file
21
packages/nodes-base/nodes/MQTT/Mqtt.node.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"node": "n8n-nodes-base.mqtt",
|
||||||
|
"nodeVersion": "1.0",
|
||||||
|
"codexVersion": "1.0",
|
||||||
|
"categories": [
|
||||||
|
"Communication",
|
||||||
|
"Development"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"credentialDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/credentials/mqtt"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.mqtt/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
171
packages/nodes-base/nodes/MQTT/Mqtt.node.ts
Normal file
171
packages/nodes-base/nodes/MQTT/Mqtt.node.ts
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import * as mqtt from 'mqtt';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IClientOptions,
|
||||||
|
} from 'mqtt';
|
||||||
|
|
||||||
|
export class Mqtt implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'MQTT',
|
||||||
|
name: 'mqtt',
|
||||||
|
icon: 'file:mqtt.svg',
|
||||||
|
group: ['input'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Push messages to MQTT',
|
||||||
|
defaults: {
|
||||||
|
name: 'MQTT',
|
||||||
|
color: '#9b27af',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'mqtt',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Topic',
|
||||||
|
name: 'topic',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
description: `The topic to publish to`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Send Input Data',
|
||||||
|
name: 'sendInputData',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
description: 'Send the the data the node receives as JSON.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Message',
|
||||||
|
name: 'message',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
sendInputData: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The message to publish',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'options',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Option',
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'QoS',
|
||||||
|
name: 'qos',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Received at Most Once',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Received at Least Once',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Exactly Once',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 0,
|
||||||
|
description: 'QoS subscription level',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Retain',
|
||||||
|
name: 'retain',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: `Normally if a publisher publishes a message to a topic, and no one is subscribed to<br>
|
||||||
|
that topic the message is simply discarded by the broker. However the publisher can tell the broker<br>
|
||||||
|
to keep the last message on that topic by setting the retain flag to true.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
const length = (items.length as unknown) as number;
|
||||||
|
const credentials = this.getCredentials('mqtt') as IDataObject;
|
||||||
|
|
||||||
|
const protocol = credentials.protocol as string || 'mqtt';
|
||||||
|
const host = credentials.host as string;
|
||||||
|
const brokerUrl = `${protocol}://${host}`;
|
||||||
|
const port = credentials.port as number || 1883;
|
||||||
|
const clientId = credentials.clientId as string || `mqttjs_${Math.random().toString(16).substr(2, 8)}`;
|
||||||
|
const clean = credentials.clean as boolean;
|
||||||
|
|
||||||
|
const clientOptions: IClientOptions = {
|
||||||
|
port,
|
||||||
|
clean,
|
||||||
|
clientId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (credentials.username && credentials.password) {
|
||||||
|
clientOptions.username = credentials.username as string;
|
||||||
|
clientOptions.password = credentials.password as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = mqtt.connect(brokerUrl, clientOptions);
|
||||||
|
const sendInputData = this.getNodeParameter('sendInputData', 0) as boolean;
|
||||||
|
|
||||||
|
// tslint:disable-next-line: no-any
|
||||||
|
const data = await new Promise((resolve, reject): any => {
|
||||||
|
client.on('connect', () => {
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
|
||||||
|
let message;
|
||||||
|
const topic = (this.getNodeParameter('topic', i) as string);
|
||||||
|
const options = (this.getNodeParameter('options', i) as IDataObject);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (sendInputData === true) {
|
||||||
|
message = JSON.stringify(items[i].json);
|
||||||
|
} else {
|
||||||
|
message = this.getNodeParameter('message', i) as string;
|
||||||
|
}
|
||||||
|
client.publish(topic, message, options);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//wait for the in-flight messages to be acked.
|
||||||
|
//needed for messages with QoS 1 & 2
|
||||||
|
client.end(false, {}, () => {
|
||||||
|
resolve([items]);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (e: string | undefined) => {
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return data as INodeExecutionData[][];
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,14 +13,14 @@ import {
|
||||||
import * as mqtt from 'mqtt';
|
import * as mqtt from 'mqtt';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IClientOptions,
|
IClientOptions, ISubscriptionMap,
|
||||||
} from 'mqtt';
|
} from 'mqtt';
|
||||||
|
|
||||||
export class MqttTrigger implements INodeType {
|
export class MqttTrigger implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
displayName: 'MQTT Trigger',
|
displayName: 'MQTT Trigger',
|
||||||
name: 'mqttTrigger',
|
name: 'mqttTrigger',
|
||||||
icon: 'file:mqtt.png',
|
icon: 'file:mqtt.svg',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Listens to MQTT events',
|
description: 'Listens to MQTT events',
|
||||||
|
@ -43,7 +43,9 @@ export class MqttTrigger implements INodeType {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
description: `Topics to subscribe to, multiple can be defined with comma.<br/>
|
description: `Topics to subscribe to, multiple can be defined with comma.<br/>
|
||||||
wildcard characters are supported (+ - for single level and # - for multi level)`,
|
wildcard characters are supported (+ - for single level and # - for multi level)<br>
|
||||||
|
By default all subscription used QoS=0. To set a different QoS, write the QoS desired<br>
|
||||||
|
after the topic preceded by a colom. For Example: topicA:1,topicB:2`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Options',
|
displayName: 'Options',
|
||||||
|
@ -52,6 +54,13 @@ export class MqttTrigger implements INodeType {
|
||||||
placeholder: 'Add Option',
|
placeholder: 'Add Option',
|
||||||
default: {},
|
default: {},
|
||||||
options: [
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'JSON Parse Body',
|
||||||
|
name: 'jsonParseBody',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Try to parse the message to an object.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Only Message',
|
displayName: 'Only Message',
|
||||||
name: 'onlyMessage',
|
name: 'onlyMessage',
|
||||||
|
@ -59,13 +68,6 @@ export class MqttTrigger implements INodeType {
|
||||||
default: false,
|
default: false,
|
||||||
description: 'Returns only the message property.',
|
description: 'Returns only the message property.',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
displayName: 'JSON Parse Message',
|
|
||||||
name: 'jsonParseMessage',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
description: 'Try to parse the message to an object.',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -81,6 +83,13 @@ export class MqttTrigger implements INodeType {
|
||||||
|
|
||||||
const topics = (this.getNodeParameter('topics') as string).split(',');
|
const topics = (this.getNodeParameter('topics') as string).split(',');
|
||||||
|
|
||||||
|
const topicsQoS: IDataObject = {};
|
||||||
|
|
||||||
|
for (const data of topics) {
|
||||||
|
const [topic, qos] = data.split(':');
|
||||||
|
topicsQoS[topic] = (qos) ? { qos: parseInt(qos, 10) } : { qos: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
const options = this.getNodeParameter('options') as IDataObject;
|
const options = this.getNodeParameter('options') as IDataObject;
|
||||||
|
|
||||||
if (!topics) {
|
if (!topics) {
|
||||||
|
@ -91,9 +100,13 @@ export class MqttTrigger implements INodeType {
|
||||||
const host = credentials.host as string;
|
const host = credentials.host as string;
|
||||||
const brokerUrl = `${protocol}://${host}`;
|
const brokerUrl = `${protocol}://${host}`;
|
||||||
const port = credentials.port as number || 1883;
|
const port = credentials.port as number || 1883;
|
||||||
|
const clientId = credentials.clientId as string || `mqttjs_${Math.random().toString(16).substr(2, 8)}`;
|
||||||
|
const clean = credentials.clean as boolean;
|
||||||
|
|
||||||
const clientOptions: IClientOptions = {
|
const clientOptions: IClientOptions = {
|
||||||
port,
|
port,
|
||||||
|
clean,
|
||||||
|
clientId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (credentials.username && credentials.password) {
|
if (credentials.username && credentials.password) {
|
||||||
|
@ -108,20 +121,19 @@ export class MqttTrigger implements INodeType {
|
||||||
async function manualTriggerFunction() {
|
async function manualTriggerFunction() {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
client.on('connect', () => {
|
client.on('connect', () => {
|
||||||
client.subscribe(topics, (err, granted) => {
|
client.subscribe(topicsQoS as ISubscriptionMap, (err, granted) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
client.on('message', (topic: string, message: Buffer | string) => { // tslint:disable-line:no-any
|
client.on('message', (topic: string, message: Buffer | string) => { // tslint:disable-line:no-any
|
||||||
|
|
||||||
let result: IDataObject = {};
|
let result: IDataObject = {};
|
||||||
|
|
||||||
message = message.toString() as string;
|
message = message.toString() as string;
|
||||||
|
|
||||||
if (options.jsonParseMessage) {
|
if (options.jsonParseBody) {
|
||||||
try {
|
try {
|
||||||
message = JSON.parse(message.toString());
|
message = JSON.parse(message.toString());
|
||||||
} catch (error) { }
|
} catch (err) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
result.message = message;
|
result.message = message;
|
||||||
|
@ -129,10 +141,9 @@ export class MqttTrigger implements INodeType {
|
||||||
|
|
||||||
if (options.onlyMessage) {
|
if (options.onlyMessage) {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
result = message;
|
result = [message as string];
|
||||||
}
|
}
|
||||||
|
self.emit([self.helpers.returnJsonArray(result)]);
|
||||||
self.emit([self.helpers.returnJsonArray([result])]);
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -144,7 +155,9 @@ export class MqttTrigger implements INodeType {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
manualTriggerFunction();
|
if (this.getMode() === 'trigger') {
|
||||||
|
manualTriggerFunction();
|
||||||
|
}
|
||||||
|
|
||||||
async function closeFunction() {
|
async function closeFunction() {
|
||||||
client.end();
|
client.end();
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue