mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge master
This commit is contained in:
commit
2a6d98b4ba
10
.github/workflows/docker-images-rpi.yml
vendored
10
.github/workflows/docker-images-rpi.yml
vendored
|
@ -4,6 +4,12 @@ on:
|
|||
push:
|
||||
tags:
|
||||
- n8n@*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'n8n version to build docker image for.'
|
||||
required: true
|
||||
default: '0.112.0'
|
||||
|
||||
jobs:
|
||||
armv7_job:
|
||||
|
@ -28,7 +34,7 @@ jobs:
|
|||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/arm/v7 \
|
||||
--build-arg N8N_VERSION=${{steps.vars.outputs.tag}} \
|
||||
-t ${{ secrets.DOCKER_USERNAME }}/n8n:${{steps.vars.outputs.tag}}-rpi \
|
||||
--build-arg N8N_VERSION=${{github.event.inputs.version || steps.vars.outputs.tag}} \
|
||||
-t ${{ secrets.DOCKER_USERNAME }}/n8n:${{github.event.inputs.version || steps.vars.outputs.tag}}-rpi \
|
||||
-t ${{ secrets.DOCKER_USERNAME }}/n8n:latest-rpi \
|
||||
--output type=image,push=true docker/images/n8n-rpi
|
||||
|
|
4
.github/workflows/docker-images.yml
vendored
4
.github/workflows/docker-images.yml
vendored
|
@ -32,3 +32,7 @@ jobs:
|
|||
run: docker build --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-debian docker/images/n8n-debian
|
||||
- name: Push Docker image of version (Debian)
|
||||
run: docker push n8nio/n8n:${{steps.vars.outputs.tag}}-debian
|
||||
- name: Tag Docker image with latest (Debian)
|
||||
run: docker tag n8nio/n8n:${{steps.vars.outputs.tag}}-debian n8nio/n8n:latest-debian
|
||||
- name: Push docker images of latest (Debian)
|
||||
run: docker push n8nio/n8n:latest-debian
|
||||
|
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
node-version: [12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
|
|
@ -139,7 +139,7 @@ automatically build your code, restart the backend and refresh the frontend
|
|||
```
|
||||
npm run dev
|
||||
```
|
||||
1. hack, hack, hack
|
||||
1. Hack, hack, hack
|
||||
1. Check if everything still runs in production mode
|
||||
```
|
||||
npm run build
|
||||
|
@ -168,61 +168,28 @@ tests of all packages.
|
|||
|
||||
## Create Custom Nodes
|
||||
|
||||
It is very straightforward to create your own nodes for n8n. More information about that can
|
||||
be found in the documentation of "n8n-node-dev" which is a small CLI which
|
||||
helps with n8n-node-development.
|
||||
Learn about [using the node dev CLI](https://docs.n8n.io/nodes/creating-nodes/node-dev-cli.html) to create custom nodes for n8n.
|
||||
|
||||
[To n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev)
|
||||
More information can
|
||||
be found in the documentation of [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev), which is a small CLI which
|
||||
helps with n8n-node-development.
|
||||
|
||||
|
||||
|
||||
## Create a new node to contribute to n8n
|
||||
|
||||
If you want to create a node which should be added to n8n follow these steps:
|
||||
Follow this tutorial on [creating your first node](https://docs.n8n.io/nodes/creating-nodes/create-node.html) for n8n.
|
||||
|
||||
1. Read the information in the [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) package as it contains a lot of generic information about node development.
|
||||
|
||||
1. Create the n8n development setup like described above and start n8n in develoment mode `npm run dev`
|
||||
|
||||
1. Create a new folder for the new node. For a service named "Example" the folder would be called: `/packages/nodes-base/nodes/Example`
|
||||
|
||||
1. If there is already a similar node, copy the existing one in the new folder and rename it. If none exists yet, create a boilerplate node with [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) and copy that one in the folder.
|
||||
|
||||
1. If the node needs credentials because it has to authenticate with an API or similar create new ones. Existing ones can be found in folder `/packages/nodes-base/credentials`. Also there it is the easiest to copy existing similar ones.
|
||||
|
||||
1. Add the path to the new node (and optionally credentials) to package.json of `nodes-base`. It already contains a property `n8n` with its own keys `credentials` and `nodes`.
|
||||
|
||||
1. Add icon for the node (60x60 PNG)
|
||||
|
||||
1. Start n8n. The new node will then be available via the editor UI and can be tested.
|
||||
|
||||
|
||||
When developing n8n must get restarted and the browser reloaded every time parameters of a node change (like new ones added, removed or changed). Only then will the new data be loaded and the node displayed correctly.
|
||||
|
||||
If only the code of the node changes (the execute method) than it is not needed as each workflow automatically starts a new process and so will always load the latest code.
|
||||
|
||||
|
||||
## Checklist before submitting a new node
|
||||
|
||||
If you'd like to submit a new node, please go through the following checklist. This will help us be quicker to review and merge your PR.
|
||||
|
||||
- [ ] Make failing requests to the API to ensure that the errors get displayed correctly (like malformed requests or requests with invalid credentials)
|
||||
- [ ] Ensure that the default values do not change and that the parameters do not get renamed, as it would break the existing workflows of the users
|
||||
- [ ] Ensure that all the top-level parameters use camelCase
|
||||
- [ ] Ensure that all the options are ordered alphabetically, unless a different order is needed for a specific reason
|
||||
- [ ] Ensure that the parameters have the correct type
|
||||
- [ ] Make sure that the file-name and the Class name are identical (case sensitive). The name under "description" in the node-code should also be identical (except that it starts with a lower-case letter and that it will never have a space)
|
||||
- [ ] Names of Trigger-Nodes always have to end with "Trigger"
|
||||
- [ ] Add credentials and nodes to the `package.json` file in alphanumerical order
|
||||
- [ ] Use tabs in all the files except in the `package.json` file, where 4-spaces have to get used
|
||||
- [ ] To make it as simple as possible for the users, check other similar nodes to ensure that they all behave similarly
|
||||
- [ ] Try to add as few parameters as possible on the main level to ensure that the node doesn't appear overwhelming. It should only contain the required parameters. All the other ones should be hidden on lower levels as "Additional Parameters" or "Options"
|
||||
- [ ] Create only one node per service which can do everything via "Resource" and "Options" and not a separate one for each possible operation.
|
||||
There are several things to keep in mind when creating a node. To help you, we prepared a [checklist](https://docs.n8n.io/nodes/creating-nodes/node-review-checklist.html) that covers the requirements for creating nodes, from preparation to submission. This will help us be quicker to review and merge your PR.
|
||||
|
||||
|
||||
## Extend Documentation
|
||||
|
||||
The repository for the n8n documentation on https://docs.n8n.io can be found [here](https://github.com/n8n-io/n8n-docs).
|
||||
The repository for the n8n documentation on [docs.n8n.io](https://docs.n8n.io) can be found [here](https://github.com/n8n-io/n8n-docs).
|
||||
|
||||
|
||||
## Contributor License Agreement
|
||||
|
|
|
@ -15,6 +15,7 @@ ENV NODE_ENV production
|
|||
|
||||
WORKDIR /data
|
||||
|
||||
USER node
|
||||
USER root
|
||||
|
||||
CMD n8n
|
||||
CMD chown -R node:node /home/node/.n8n \
|
||||
&& gosu node n8n
|
||||
|
|
|
@ -2,6 +2,36 @@
|
|||
|
||||
This list shows all the versions which include breaking changes and how to upgrade.
|
||||
|
||||
## 0.113.0
|
||||
|
||||
### What changed?
|
||||
In the Dropbox node, both credential types (Access Token & OAuth2) have a new parameter called "APP Access Type".
|
||||
|
||||
### When is action necessary?
|
||||
|
||||
If you are using a Dropbox APP with permission type, "App Folder".
|
||||
|
||||
### How to upgrade:
|
||||
|
||||
Open your Dropbox node's credentials and set the "APP Access Type" parameter to "App Folder".
|
||||
|
||||
## 0.111.0
|
||||
|
||||
### What changed?
|
||||
In the Dropbox node, now all operations are performed relative to the user's root directory.
|
||||
|
||||
### When is action necessary?
|
||||
|
||||
If you are using any resource/operation with OAuth2 authentication.
|
||||
|
||||
If you are using the `folder:list` operation with the parameter `Folder Path` empty (root path) and have a Team Space in your Dropbox account.
|
||||
|
||||
### How to upgrade:
|
||||
|
||||
Open the Dropbox node, go to the OAuth2 credential you are using and reconnect it again.
|
||||
|
||||
Also, if you are using the `folder:list` operation, make sure your logic is taking into account the team folders in the response.
|
||||
|
||||
## 0.105.0
|
||||
|
||||
### What changed?
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
WorkflowCredentials,
|
||||
WorkflowHelpers,
|
||||
WorkflowRunner,
|
||||
} from "../src";
|
||||
} from '../src';
|
||||
|
||||
|
||||
export class Execute extends Command {
|
||||
|
@ -130,7 +130,7 @@ export class Execute extends Command {
|
|||
// Check if the workflow contains the required "Start" node
|
||||
// "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue
|
||||
const requiredNodeTypes = ['n8n-nodes-base.start'];
|
||||
let startNode: INode | undefined= undefined;
|
||||
let startNode: INode | undefined = undefined;
|
||||
for (const node of workflowData!.nodes) {
|
||||
if (requiredNodeTypes.includes(node.type)) {
|
||||
startNode = node;
|
||||
|
|
|
@ -3,6 +3,11 @@ import {
|
|||
flags,
|
||||
} from '@oclif/command';
|
||||
|
||||
import {
|
||||
Credentials,
|
||||
UserSettings,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject
|
||||
} from 'n8n-workflow';
|
||||
|
@ -10,6 +15,7 @@ import {
|
|||
import {
|
||||
Db,
|
||||
GenericHelpers,
|
||||
ICredentialsDecryptedDb,
|
||||
} from '../../src';
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
@ -21,8 +27,9 @@ export class ExportCredentialsCommand extends Command {
|
|||
static examples = [
|
||||
`$ n8n export:credentials --all`,
|
||||
`$ n8n export:credentials --id=5 --output=file.json`,
|
||||
`$ n8n export:credentials --all --output=backups/latest/`,
|
||||
`$ n8n export:credentials --all --output=backups/latest.json`,
|
||||
`$ n8n export:credentials --backup --output=backups/latest/`,
|
||||
`$ n8n export:credentials --all --decrypted --output=backups/decrypted.json`,
|
||||
];
|
||||
|
||||
static flags = {
|
||||
|
@ -46,6 +53,9 @@ export class ExportCredentialsCommand extends Command {
|
|||
separate: flags.boolean({
|
||||
description: 'Exports one file per credential (useful for versioning). Must inform a directory via --output.',
|
||||
}),
|
||||
decrypted: flags.boolean({
|
||||
description: 'Exports data decrypted / in plain text. ALL SENSITIVE INFORMATION WILL BE VISIBLE IN THE FILES. Use to migrate from a installation to another that have a different secret key (in the config file).',
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
|
@ -108,6 +118,20 @@ export class ExportCredentialsCommand extends Command {
|
|||
|
||||
const credentials = await Db.collections.Credentials!.find(findQuery);
|
||||
|
||||
if (flags.decrypted) {
|
||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||
if (encryptionKey === undefined) {
|
||||
throw new Error('No encryption key got found to decrypt the credentials!');
|
||||
}
|
||||
|
||||
for (let i = 0; i < credentials.length; i++) {
|
||||
const { name, type, nodesAccess, data } = credentials[i];
|
||||
const credential = new Credentials(name, type, nodesAccess, data);
|
||||
const plainData = credential.getData(encryptionKey);
|
||||
(credentials[i] as ICredentialsDecryptedDb).data = plainData;
|
||||
}
|
||||
}
|
||||
|
||||
if (credentials.length === 0) {
|
||||
throw new Error('No credentials found with specified filters.');
|
||||
}
|
||||
|
@ -116,7 +140,7 @@ export class ExportCredentialsCommand extends Command {
|
|||
let fileContents: string, i: number;
|
||||
for (i = 0; i < credentials.length; i++) {
|
||||
fileContents = JSON.stringify(credentials[i], null, flags.pretty ? 2 : undefined);
|
||||
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);
|
||||
}
|
||||
console.log('Successfully exported', i, 'credentials.');
|
||||
|
|
|
@ -116,7 +116,7 @@ export class ExportWorkflowsCommand extends Command {
|
|||
let fileContents: string, i: number;
|
||||
for (i = 0; i < workflows.length; i++) {
|
||||
fileContents = JSON.stringify(workflows[i], null, flags.pretty ? 2 : undefined);
|
||||
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);
|
||||
}
|
||||
console.log('Successfully exported', i, 'workflows.');
|
||||
|
|
|
@ -3,6 +3,11 @@ import {
|
|||
flags,
|
||||
} from '@oclif/command';
|
||||
|
||||
import {
|
||||
Credentials,
|
||||
UserSettings,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
Db,
|
||||
GenericHelpers,
|
||||
|
@ -51,10 +56,22 @@ export class ImportCredentialsCommand extends Command {
|
|||
try {
|
||||
await Db.init();
|
||||
let i;
|
||||
|
||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||
if (encryptionKey === undefined) {
|
||||
throw new Error('No encryption key got found to encrypt the credentials!');
|
||||
}
|
||||
|
||||
if (flags.separate) {
|
||||
const files = await glob((flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep) + '*.json');
|
||||
for (i = 0; i < files.length; i++) {
|
||||
const credential = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' }));
|
||||
|
||||
if (typeof credential.data === 'object') {
|
||||
// plain data / decrypted input. Should be encrypted first.
|
||||
Credentials.prototype.setData.call(credential, credential.data, encryptionKey);
|
||||
}
|
||||
|
||||
await Db.collections.Credentials!.save(credential);
|
||||
}
|
||||
} else {
|
||||
|
@ -65,6 +82,10 @@ export class ImportCredentialsCommand extends Command {
|
|||
}
|
||||
|
||||
for (i = 0; i < fileContents.length; i++) {
|
||||
if (typeof fileContents[i].data === 'object') {
|
||||
// plain data / decrypted input. Should be encrypted first.
|
||||
Credentials.prototype.setData.call(fileContents[i], fileContents[i].data, encryptionKey);
|
||||
}
|
||||
await Db.collections.Credentials!.save(fileContents[i]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,11 +17,12 @@ import {
|
|||
Db,
|
||||
ExternalHooks,
|
||||
GenericHelpers,
|
||||
IExecutionsCurrentSummary,
|
||||
LoadNodesAndCredentials,
|
||||
NodeTypes,
|
||||
Server,
|
||||
TestWebhooks,
|
||||
} from "../src";
|
||||
} from '../src';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
|
||||
|
||||
|
@ -97,12 +98,15 @@ export class Start extends Command {
|
|||
|
||||
// Wait for active workflow executions to finish
|
||||
const activeExecutionsInstance = ActiveExecutions.getInstance();
|
||||
let executingWorkflows = activeExecutionsInstance.getActiveExecutions();
|
||||
let executingWorkflows = activeExecutionsInstance.getActiveExecutions() as IExecutionsCurrentSummary[];
|
||||
|
||||
let count = 0;
|
||||
while (executingWorkflows.length !== 0) {
|
||||
if (count++ % 4 === 0) {
|
||||
console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`);
|
||||
executingWorkflows.map(execution => {
|
||||
console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`);
|
||||
});
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
|
@ -129,7 +133,7 @@ export class Start extends Command {
|
|||
await (async () => {
|
||||
try {
|
||||
// Start directly with the init of the database to improve startup time
|
||||
const startDbInitPromise = Db.init().catch(error => {
|
||||
const startDbInitPromise = Db.init().catch((error: Error) => {
|
||||
console.error(`There was an error initializing DB: ${error.message}`);
|
||||
|
||||
processExistCode = 1;
|
||||
|
@ -180,7 +184,7 @@ export class Start extends Command {
|
|||
cumulativeTimeout += now - lastTimer;
|
||||
lastTimer = now;
|
||||
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
||||
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
|
||||
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
@ -221,7 +225,7 @@ export class Start extends Command {
|
|||
if (dbType === 'sqlite') {
|
||||
const shouldRunVacuum = config.get('database.sqlite.executeVacuumOnStartup') as number;
|
||||
if (shouldRunVacuum) {
|
||||
Db.collections.Execution!.query("VACUUM;");
|
||||
Db.collections.Execution!.query('VACUUM;');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -280,7 +284,7 @@ export class Start extends Command {
|
|||
Start.openBrowser();
|
||||
}
|
||||
this.log(`\nPress "o" to open in Browser.`);
|
||||
process.stdin.on("data", (key : string) => {
|
||||
process.stdin.on('data', (key: string) => {
|
||||
if (key === 'o') {
|
||||
Start.openBrowser();
|
||||
inputText = '';
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
import {
|
||||
Db,
|
||||
GenericHelpers,
|
||||
} from "../../src";
|
||||
} from '../../src';
|
||||
|
||||
|
||||
export class UpdateWorkflowCommand extends Command {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
NodeTypes,
|
||||
TestWebhooks,
|
||||
WebhookServer,
|
||||
} from "../src";
|
||||
} from '../src';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
|
||||
|
||||
|
@ -166,7 +166,7 @@ export class Webhook extends Command {
|
|||
cumulativeTimeout += now - lastTimer;
|
||||
lastTimer = now;
|
||||
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
||||
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
|
||||
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import {
|
|||
ResponseHelper,
|
||||
WorkflowCredentials,
|
||||
WorkflowExecuteAdditionalData,
|
||||
} from "../src";
|
||||
} from '../src';
|
||||
|
||||
import * as config from '../config';
|
||||
import * as Bull from 'bull';
|
||||
|
@ -132,7 +132,7 @@ export class Worker extends Command {
|
|||
const credentials = await WorkflowCredentials(currentExecutionDb.workflowData.nodes);
|
||||
|
||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
|
||||
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksIntegrated(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string });
|
||||
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string });
|
||||
|
||||
let workflowExecute: WorkflowExecute;
|
||||
let workflowRun: PCancelable<IRun>;
|
||||
|
@ -241,7 +241,7 @@ export class Worker extends Command {
|
|||
cumulativeTimeout += now - lastTimer;
|
||||
lastTimer = now;
|
||||
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
||||
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process.");
|
||||
console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -446,6 +446,20 @@ const config = convict({
|
|||
},
|
||||
|
||||
endpoints: {
|
||||
metrics: {
|
||||
enable: {
|
||||
format: 'Boolean',
|
||||
default: false,
|
||||
env: 'N8N_METRICS',
|
||||
doc: 'Enable metrics endpoint',
|
||||
},
|
||||
prefix: {
|
||||
format: String,
|
||||
default: 'n8n_',
|
||||
env: 'N8N_METRICS_PREFIX',
|
||||
doc: 'An optional prefix for metric names. Default: n8n_',
|
||||
},
|
||||
},
|
||||
rest: {
|
||||
format: String,
|
||||
default: 'rest',
|
||||
|
@ -483,10 +497,10 @@ const config = convict({
|
|||
* WARNING: Trigger nodes (like Cron) will cause duplication
|
||||
* of work, so be aware when using.
|
||||
*/
|
||||
doc: 'Deregister webhooks on external services only when workflows are deactivated. Useful for blue/green deployments.',
|
||||
doc: 'Deregister webhooks on external services only when workflows are deactivated.',
|
||||
format: Boolean,
|
||||
default: false,
|
||||
env: 'N8N_SKIP_WEBHOOK_DEREGISTRATION_STARTUP_SHUTDOWN',
|
||||
env: 'N8N_SKIP_WEBHOOK_DEREGISTRATION_SHUTDOWN',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -1,139 +1,140 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "0.107.0",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
"name": "n8n",
|
||||
"version": "0.114.0",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"main": "dist/index",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"oclif": {
|
||||
"commands": "./dist/commands",
|
||||
"bin": "n8n"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"",
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
|
||||
"start": "run-script-os",
|
||||
"start:default": "cd bin && ./n8n",
|
||||
"start:windows": "cd bin && n8n",
|
||||
"test": "jest",
|
||||
"tslint": "tslint -p tsconfig.json -c tslint.json",
|
||||
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
|
||||
"watch": "tsc --watch",
|
||||
"typeorm": "ts-node ./node_modules/typeorm/cli.js"
|
||||
},
|
||||
"bin": {
|
||||
"n8n": "./bin/n8n"
|
||||
},
|
||||
"keywords": [
|
||||
"automate",
|
||||
"automation",
|
||||
"IaaS",
|
||||
"iPaaS",
|
||||
"n8n",
|
||||
"workflow"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"templates",
|
||||
"dist",
|
||||
"oclif.manifest.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@oclif/dev-cli": "^1.22.2",
|
||||
"@types/basic-auth": "^1.1.2",
|
||||
"@types/bcryptjs": "^2.4.1",
|
||||
"@types/bull": "^3.3.10",
|
||||
"@types/compression": "1.0.1",
|
||||
"@types/connect-history-api-fallback": "^1.3.1",
|
||||
"@types/convict": "^4.2.1",
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jest": "^26.0.13",
|
||||
"@types/localtunnel": "^1.9.0",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/node": "14.0.27",
|
||||
"@types/open": "^6.1.0",
|
||||
"@types/parseurl": "^1.3.1",
|
||||
"@types/request-promise-native": "~1.0.15",
|
||||
"concurrently": "^5.1.0",
|
||||
"jest": "^26.4.2",
|
||||
"nodemon": "^2.0.2",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"run-script-os": "^1.0.7",
|
||||
"ts-jest": "^26.3.0",
|
||||
"ts-node": "^8.9.1",
|
||||
"tslint": "^6.1.2",
|
||||
"typescript": "~3.9.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/command": "^1.5.18",
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@types/json-diff": "^0.5.1",
|
||||
"@types/jsonwebtoken": "^8.3.4",
|
||||
"basic-auth": "^2.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.18.3",
|
||||
"body-parser-xml": "^1.1.0",
|
||||
"bull": "^3.19.0",
|
||||
"client-oauth2": "^4.2.5",
|
||||
"compression": "^1.7.4",
|
||||
"connect-history-api-fallback": "^1.6.0",
|
||||
"convict": "^6.0.1",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^8.0.0",
|
||||
"express": "^4.16.4",
|
||||
"flatted": "^2.0.0",
|
||||
"glob-promise": "^3.4.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"inquirer": "^7.0.1",
|
||||
"json-diff": "^0.5.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "~1.12.1",
|
||||
"localtunnel": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"mysql2": "~2.1.0",
|
||||
"n8n-core": "~0.67.0",
|
||||
"n8n-editor-ui": "~0.84.0",
|
||||
"n8n-nodes-base": "~0.111.0",
|
||||
"n8n-workflow": "~0.55.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"open": "^7.0.0",
|
||||
"pg": "^8.3.0",
|
||||
"prom-client": "^13.1.0",
|
||||
"request-promise-native": "^1.0.7",
|
||||
"sqlite3": "^5.0.1",
|
||||
"sse-channel": "^3.1.1",
|
||||
"tslib": "1.11.2",
|
||||
"typeorm": "^0.2.30"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"main": "dist/index",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"oclif": {
|
||||
"commands": "./dist/commands",
|
||||
"bin": "n8n"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"",
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
|
||||
"start": "run-script-os",
|
||||
"start:default": "cd bin && ./n8n",
|
||||
"start:windows": "cd bin && n8n",
|
||||
"test": "jest",
|
||||
"tslint": "tslint -p tsconfig.json -c tslint.json",
|
||||
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
|
||||
"watch": "tsc --watch",
|
||||
"typeorm": "ts-node ./node_modules/typeorm/cli.js"
|
||||
},
|
||||
"bin": {
|
||||
"n8n": "./bin/n8n"
|
||||
},
|
||||
"keywords": [
|
||||
"automate",
|
||||
"automation",
|
||||
"IaaS",
|
||||
"iPaaS",
|
||||
"n8n",
|
||||
"workflow"
|
||||
"testURL": "http://localhost/",
|
||||
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||
"testPathIgnorePatterns": [
|
||||
"/dist/",
|
||||
"/node_modules/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"templates",
|
||||
"dist",
|
||||
"oclif.manifest.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@oclif/dev-cli": "^1.22.2",
|
||||
"@types/basic-auth": "^1.1.2",
|
||||
"@types/bcryptjs": "^2.4.1",
|
||||
"@types/bull": "^3.3.10",
|
||||
"@types/compression": "1.0.1",
|
||||
"@types/connect-history-api-fallback": "^1.3.1",
|
||||
"@types/convict": "^4.2.1",
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jest": "^26.0.13",
|
||||
"@types/localtunnel": "^1.9.0",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/node": "14.0.27",
|
||||
"@types/open": "^6.1.0",
|
||||
"@types/parseurl": "^1.3.1",
|
||||
"@types/request-promise-native": "~1.0.15",
|
||||
"concurrently": "^5.1.0",
|
||||
"jest": "^26.4.2",
|
||||
"nodemon": "^2.0.2",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"run-script-os": "^1.0.7",
|
||||
"ts-jest": "^26.3.0",
|
||||
"ts-node": "^8.9.1",
|
||||
"tslint": "^6.1.2",
|
||||
"typescript": "~3.9.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/command": "^1.5.18",
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@types/json-diff": "^0.5.1",
|
||||
"@types/jsonwebtoken": "^8.3.4",
|
||||
"basic-auth": "^2.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.18.3",
|
||||
"body-parser-xml": "^1.1.0",
|
||||
"bull": "^3.19.0",
|
||||
"client-oauth2": "^4.2.5",
|
||||
"compression": "^1.7.4",
|
||||
"connect-history-api-fallback": "^1.6.0",
|
||||
"convict": "^5.0.0",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^8.0.0",
|
||||
"express": "^4.16.4",
|
||||
"flatted": "^2.0.0",
|
||||
"glob-promise": "^3.4.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"inquirer": "^7.0.1",
|
||||
"json-diff": "^0.5.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "~1.9.0",
|
||||
"localtunnel": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"mysql2": "~2.1.0",
|
||||
"n8n-core": "~0.62.0",
|
||||
"n8n-editor-ui": "~0.77.0",
|
||||
"n8n-nodes-base": "~0.104.0",
|
||||
"n8n-workflow": "~0.51.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"open": "^7.0.0",
|
||||
"pg": "^8.3.0",
|
||||
"request-promise-native": "^1.0.7",
|
||||
"sqlite3": "^5.0.1",
|
||||
"sse-channel": "^3.1.1",
|
||||
"tslib": "1.11.2",
|
||||
"typeorm": "^0.2.30"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"testURL": "http://localhost/",
|
||||
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||
"testPathIgnorePatterns": [
|
||||
"/dist/",
|
||||
"/node_modules/"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"json"
|
||||
]
|
||||
}
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"json"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
NodeHelpers,
|
||||
WebhookHttpMethod,
|
||||
Workflow,
|
||||
WorkflowActivateMode,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
@ -66,7 +67,7 @@ export class ActiveWorkflowRunner {
|
|||
for (const workflowData of workflowsData) {
|
||||
console.log(` - ${workflowData.name}`);
|
||||
try {
|
||||
await this.add(workflowData.id.toString(), workflowData);
|
||||
await this.add(workflowData.id.toString(), 'init', workflowData);
|
||||
console.log(` => Started`);
|
||||
} catch (error) {
|
||||
console.log(` => ERROR: Workflow could not be activated:`);
|
||||
|
@ -273,7 +274,7 @@ export class ActiveWorkflowRunner {
|
|||
* @returns {Promise<void>}
|
||||
* @memberof ActiveWorkflowRunner
|
||||
*/
|
||||
async addWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): Promise<void> {
|
||||
async addWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise<void> {
|
||||
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData);
|
||||
let path = '' as string | undefined;
|
||||
|
||||
|
@ -319,10 +320,10 @@ export class ActiveWorkflowRunner {
|
|||
|
||||
await Db.collections.Webhook?.insert(webhook);
|
||||
|
||||
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, false);
|
||||
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, false);
|
||||
if (webhookExists !== true) {
|
||||
// If webhook does not exist yet create it
|
||||
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, false);
|
||||
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, activation, false);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
@ -378,7 +379,7 @@ export class ActiveWorkflowRunner {
|
|||
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData);
|
||||
|
||||
for (const webhookData of webhooks) {
|
||||
await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, false);
|
||||
await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, 'update', false);
|
||||
}
|
||||
|
||||
await WorkflowHelpers.saveStaticData(workflow);
|
||||
|
@ -446,9 +447,9 @@ export class ActiveWorkflowRunner {
|
|||
* @returns {IGetExecutePollFunctions}
|
||||
* @memberof ActiveWorkflowRunner
|
||||
*/
|
||||
getExecutePollFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): IGetExecutePollFunctions {
|
||||
getExecutePollFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecutePollFunctions {
|
||||
return ((workflow: Workflow, node: INode) => {
|
||||
const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode);
|
||||
const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode, activation);
|
||||
returnFunctions.__emit = (data: INodeExecutionData[][]): void => {
|
||||
this.runWorkflow(workflowData, node, data, additionalData, mode);
|
||||
};
|
||||
|
@ -467,9 +468,9 @@ export class ActiveWorkflowRunner {
|
|||
* @returns {IGetExecuteTriggerFunctions}
|
||||
* @memberof ActiveWorkflowRunner
|
||||
*/
|
||||
getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): IGetExecuteTriggerFunctions{
|
||||
getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecuteTriggerFunctions{
|
||||
return ((workflow: Workflow, node: INode) => {
|
||||
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode);
|
||||
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode, activation);
|
||||
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
|
||||
WorkflowHelpers.saveStaticData(workflow);
|
||||
this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) => console.error(err));
|
||||
|
@ -486,7 +487,7 @@ export class ActiveWorkflowRunner {
|
|||
* @returns {Promise<void>}
|
||||
* @memberof ActiveWorkflowRunner
|
||||
*/
|
||||
async add(workflowId: string, workflowData?: IWorkflowDb): Promise<void> {
|
||||
async add(workflowId: string, activation: WorkflowActivateMode, workflowData?: IWorkflowDb): Promise<void> {
|
||||
if (this.activeWorkflows === null) {
|
||||
throw new Error(`The "activeWorkflows" instance did not get initialized yet.`);
|
||||
}
|
||||
|
@ -511,15 +512,15 @@ export class ActiveWorkflowRunner {
|
|||
const mode = 'trigger';
|
||||
const credentials = await WorkflowCredentials(workflowData.nodes);
|
||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
|
||||
const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode);
|
||||
const getPollFunctions = this.getExecutePollFunctions(workflowData, additionalData, mode);
|
||||
const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode, activation);
|
||||
const getPollFunctions = this.getExecutePollFunctions(workflowData, additionalData, mode, activation);
|
||||
|
||||
// Add the workflows which have webhooks defined
|
||||
await this.addWorkflowWebhooks(workflowInstance, additionalData, mode);
|
||||
await this.addWorkflowWebhooks(workflowInstance, additionalData, mode, activation);
|
||||
|
||||
if (workflowInstance.getTriggerNodes().length !== 0
|
||||
|| workflowInstance.getPollNodes().length !== 0) {
|
||||
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, getTriggerFunctions, getPollFunctions);
|
||||
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions);
|
||||
}
|
||||
|
||||
if (this.activationErrors[workflowId] !== undefined) {
|
||||
|
|
|
@ -95,14 +95,15 @@ export async function getConfigValue(configKey: string): Promise<string | boolea
|
|||
|
||||
// Get the environment variable
|
||||
const configSchema = config.getSchema();
|
||||
let currentSchema = configSchema.properties as IDataObject;
|
||||
// @ts-ignore
|
||||
let currentSchema = configSchema._cvtProperties as IDataObject;
|
||||
for (const key of configKeyParts) {
|
||||
if (currentSchema[key] === undefined) {
|
||||
throw new Error(`Key "${key}" of ConfigKey "${configKey}" does not exist`);
|
||||
} else if ((currentSchema[key]! as IDataObject).properties === undefined) {
|
||||
} else if ((currentSchema[key]! as IDataObject)._cvtProperties === undefined) {
|
||||
currentSchema = currentSchema[key] as IDataObject;
|
||||
} else {
|
||||
currentSchema = (currentSchema[key] as IDataObject).properties as IDataObject;
|
||||
currentSchema = (currentSchema[key] as IDataObject)._cvtProperties as IDataObject;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -179,8 +179,7 @@ export interface IExecutionsStopData {
|
|||
}
|
||||
|
||||
export interface IExecutionsSummary {
|
||||
id?: string; // executionIdDb
|
||||
idActive?: string; // executionIdActive
|
||||
id: string;
|
||||
finished?: boolean;
|
||||
mode: WorkflowExecuteMode;
|
||||
retryOf?: string;
|
||||
|
@ -327,8 +326,7 @@ export type IPushDataType = 'executionFinished' | 'executionStarted' | 'nodeExec
|
|||
|
||||
export interface IPushDataExecutionFinished {
|
||||
data: IRun;
|
||||
executionIdActive: string;
|
||||
executionIdDb?: string;
|
||||
executionId: string;
|
||||
retryOf?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import * as csrf from 'csrf';
|
|||
import * as requestPromise from 'request-promise-native';
|
||||
import { createHmac } from 'crypto';
|
||||
import { compare } from 'bcryptjs';
|
||||
import * as promClient from 'prom-client';
|
||||
|
||||
import {
|
||||
ActiveExecutions,
|
||||
|
@ -91,9 +92,7 @@ import {
|
|||
import {
|
||||
FindManyOptions,
|
||||
FindOneOptions,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
MoreThanOrEqual,
|
||||
Not,
|
||||
} from 'typeorm';
|
||||
|
||||
|
@ -108,6 +107,7 @@ import * as parseUrl from 'parseurl';
|
|||
import * as querystring from 'querystring';
|
||||
import * as Queue from '../src/Queue';
|
||||
import { OptionsWithUrl } from 'request-promise-native';
|
||||
import { Registry } from 'prom-client';
|
||||
|
||||
class App {
|
||||
|
||||
|
@ -197,6 +197,16 @@ class App {
|
|||
|
||||
async config(): Promise<void> {
|
||||
|
||||
const enableMetrics = config.get('endpoints.metrics.enable') as boolean;
|
||||
let register: Registry;
|
||||
|
||||
if (enableMetrics === true) {
|
||||
const prefix = config.get('endpoints.metrics.prefix') as string;
|
||||
register = new promClient.Registry();
|
||||
register.setDefaultLabels({ prefix });
|
||||
promClient.collectDefaultMetrics({ register });
|
||||
}
|
||||
|
||||
this.versions = await GenericHelpers.getVersions();
|
||||
this.frontendSettings.versionCli = this.versions.cli;
|
||||
|
||||
|
@ -204,7 +214,7 @@ class App {
|
|||
|
||||
const excludeEndpoints = config.get('security.excludeEndpoints') as string;
|
||||
|
||||
const ignoredEndpoints = ['healthz', this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials];
|
||||
const ignoredEndpoints = ['healthz', 'metrics', this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials];
|
||||
ignoredEndpoints.push.apply(ignoredEndpoints, excludeEndpoints.split(':'));
|
||||
|
||||
const authIgnoreRegex = new RegExp(`^\/(${_(ignoredEndpoints).compact().join('|')})\/?.*$`);
|
||||
|
@ -386,7 +396,7 @@ class App {
|
|||
this.app.use(history({
|
||||
rewrites: [
|
||||
{
|
||||
from: new RegExp(`^\/(${this.restEndpoint}|healthz|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`),
|
||||
from: new RegExp(`^\/(${this.restEndpoint}|healthz|metrics|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`),
|
||||
to: (context) => {
|
||||
return context.parsedUrl!.pathname!.toString();
|
||||
},
|
||||
|
@ -395,7 +405,8 @@ class App {
|
|||
}));
|
||||
|
||||
//support application/x-www-form-urlencoded post data
|
||||
this.app.use(bodyParser.urlencoded({ extended: false,
|
||||
this.app.use(bodyParser.urlencoded({
|
||||
extended: false,
|
||||
verify: (req, res, buf) => {
|
||||
// @ts-ignore
|
||||
req.rawBody = buf;
|
||||
|
@ -453,7 +464,16 @@ class App {
|
|||
ResponseHelper.sendSuccessResponse(res, responseData, true, 200);
|
||||
});
|
||||
|
||||
|
||||
// ----------------------------------------
|
||||
// Metrics
|
||||
// ----------------------------------------
|
||||
if (enableMetrics === true) {
|
||||
this.app.get('/metrics', async (req: express.Request, res: express.Response) => {
|
||||
const response = await register.metrics();
|
||||
res.setHeader('Content-Type', register.contentType);
|
||||
ResponseHelper.sendSuccessResponse(res, response, true, 200);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Workflow
|
||||
|
@ -602,7 +622,7 @@ class App {
|
|||
try {
|
||||
await this.externalHooks.run('workflow.activate', [responseData]);
|
||||
|
||||
await this.activeWorkflowRunner.add(id);
|
||||
await this.activeWorkflowRunner.add(id, isActive ? 'update' : 'activate');
|
||||
} catch (error) {
|
||||
// If workflow could not be activated set it again to inactive
|
||||
newWorkflowData.active = false;
|
||||
|
@ -648,6 +668,7 @@ class App {
|
|||
const startNodes: string[] | undefined = req.body.startNodes;
|
||||
const destinationNode: string | undefined = req.body.destinationNode;
|
||||
const executionMode = 'manual';
|
||||
const activationMode = 'manual';
|
||||
|
||||
const sessionId = GenericHelpers.getSessionId(req);
|
||||
|
||||
|
@ -657,7 +678,7 @@ class App {
|
|||
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
|
||||
const nodeTypes = NodeTypes();
|
||||
const workflowInstance = new Workflow({ id: workflowData.id, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes, staticData: undefined, settings: workflowData.settings });
|
||||
const needsWebhook = await this.testWebhooks.needsWebhookData(workflowData, workflowInstance, additionalData, executionMode, sessionId, destinationNode);
|
||||
const needsWebhook = await this.testWebhooks.needsWebhookData(workflowData, workflowInstance, additionalData, executionMode, activationMode, sessionId, destinationNode);
|
||||
if (needsWebhook === true) {
|
||||
return {
|
||||
waitingForWebhook: true,
|
||||
|
@ -725,7 +746,7 @@ class App {
|
|||
// Make a copy of the object. If we don't do this, then when
|
||||
// The method below is called the properties are removed for good
|
||||
// This happens because nodes are returned as reference.
|
||||
const nodeInfo: INodeTypeDescription = {...nodeData.description};
|
||||
const nodeInfo: INodeTypeDescription = { ...nodeData.description };
|
||||
if (req.query.includeProperties !== 'true') {
|
||||
// @ts-ignore
|
||||
delete nodeInfo.properties;
|
||||
|
@ -1310,6 +1331,8 @@ class App {
|
|||
|
||||
// Verify and store app code. Generate access tokens and store for respective credential.
|
||||
this.app.get(`/${this.restEndpoint}/oauth2-credential/callback`, async (req: express.Request, res: express.Response) => {
|
||||
|
||||
// realmId it's currently just use for the quickbook OAuth2 flow
|
||||
const { code, state: stateEncoded } = req.query;
|
||||
|
||||
if (code === undefined || stateEncoded === undefined) {
|
||||
|
@ -1384,6 +1407,10 @@ class App {
|
|||
|
||||
const oauthToken = await oAuthObj.code.getToken(`${oAuth2Parameters.redirectUri}?${queryParameters}`, options);
|
||||
|
||||
if (Object.keys(req.query).length > 2) {
|
||||
_.set(oauthToken.data, 'callbackQueryString', _.omit(req.query, 'state', 'code'));
|
||||
}
|
||||
|
||||
if (oauthToken === undefined) {
|
||||
const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
|
@ -1430,14 +1457,14 @@ class App {
|
|||
limit = parseInt(req.query.limit as string, 10);
|
||||
}
|
||||
|
||||
let executingWorkflowIds;
|
||||
const executingWorkflowIds: string[] = [];
|
||||
|
||||
if (config.get('executions.mode') === 'queue') {
|
||||
const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']);
|
||||
executingWorkflowIds = currentJobs.map(job => job.data.executionId) as string[];
|
||||
} else {
|
||||
executingWorkflowIds = this.activeExecutionsInstance.getActiveExecutions().map(execution => execution.id.toString()) as string[];
|
||||
executingWorkflowIds.push(...currentJobs.map(job => job.data.executionId) as string[]);
|
||||
}
|
||||
// We may have manual executions even with queue so we must account for these.
|
||||
executingWorkflowIds.push(...this.activeExecutionsInstance.getActiveExecutions().map(execution => execution.id.toString()) as string[]);
|
||||
|
||||
const countFilter = JSON.parse(JSON.stringify(filter));
|
||||
countFilter.select = ['id'];
|
||||
|
@ -1510,7 +1537,7 @@ class App {
|
|||
}
|
||||
|
||||
if (req.query.unflattedResponse === 'true') {
|
||||
const fullExecutionData = ResponseHelper.unflattenExecutionData(result);
|
||||
const fullExecutionData = ResponseHelper.unflattenExecutionData(result);
|
||||
return fullExecutionData as IExecutionResponse;
|
||||
} else {
|
||||
// Convert to response format in which the id is a string
|
||||
|
@ -1638,7 +1665,16 @@ class App {
|
|||
if (config.get('executions.mode') === 'queue') {
|
||||
const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']);
|
||||
|
||||
const currentlyRunningExecutionIds = currentJobs.map(job => job.data.executionId);
|
||||
const currentlyRunningQueueIds = currentJobs.map(job => job.data.executionId);
|
||||
|
||||
const currentlyRunningManualExecutions = this.activeExecutionsInstance.getActiveExecutions();
|
||||
const manualExecutionIds = currentlyRunningManualExecutions.map(execution => execution.id);
|
||||
|
||||
const currentlyRunningExecutionIds = currentlyRunningQueueIds.concat(manualExecutionIds);
|
||||
|
||||
if (currentlyRunningExecutionIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const resultsQuery = await Db.collections.Execution!
|
||||
.createQueryBuilder("execution")
|
||||
|
@ -1663,7 +1699,7 @@ class App {
|
|||
|
||||
return results.map(result => {
|
||||
return {
|
||||
idActive: result.id,
|
||||
id: result.id,
|
||||
workflowId: result.workflowId,
|
||||
mode: result.mode,
|
||||
retryOf: result.retryOf !== null ? result.retryOf : undefined,
|
||||
|
@ -1686,8 +1722,8 @@ class App {
|
|||
}
|
||||
returnData.push(
|
||||
{
|
||||
idActive: data.id.toString(),
|
||||
workflowId: data.workflowId.toString(),
|
||||
id: data.id.toString(),
|
||||
workflowId: data.workflowId === undefined ? '' : data.workflowId.toString(),
|
||||
mode: data.mode,
|
||||
retryOf: data.retryOf,
|
||||
startedAt: new Date(data.startedAt),
|
||||
|
@ -1702,6 +1738,20 @@ class App {
|
|||
// Forces the execution to stop
|
||||
this.app.post(`/${this.restEndpoint}/executions-current/:id/stop`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IExecutionsStopData> => {
|
||||
if (config.get('executions.mode') === 'queue') {
|
||||
// Manual executions should still be stoppable, so
|
||||
// try notifying the `activeExecutions` to stop it.
|
||||
const result = await this.activeExecutionsInstance.stopExecution(req.params.id);
|
||||
if (result !== undefined) {
|
||||
const returnData: IExecutionsStopData = {
|
||||
mode: result.mode,
|
||||
startedAt: new Date(result.startedAt),
|
||||
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
|
||||
finished: result.finished,
|
||||
};
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']);
|
||||
|
||||
const job = currentJobs.find(job => job.data.executionId.toString() === req.params.id);
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
IWorkflowExecuteAdditionalData,
|
||||
WebhookHttpMethod,
|
||||
Workflow,
|
||||
WorkflowActivateMode,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
@ -161,7 +162,7 @@ export class TestWebhooks {
|
|||
* @returns {(Promise<IExecutionDb | undefined>)}
|
||||
* @memberof TestWebhooks
|
||||
*/
|
||||
async needsWebhookData(workflowData: IWorkflowDb, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, sessionId?: string, destinationNode?: string): Promise<boolean> {
|
||||
async needsWebhookData(workflowData: IWorkflowDb, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, sessionId?: string, destinationNode?: string): Promise<boolean> {
|
||||
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, destinationNode);
|
||||
|
||||
if (webhooks.length === 0) {
|
||||
|
@ -193,7 +194,7 @@ export class TestWebhooks {
|
|||
};
|
||||
|
||||
try {
|
||||
await this.activeWebhooks!.add(workflow, webhookData, mode);
|
||||
await this.activeWebhooks!.add(workflow, webhookData, mode, activation);
|
||||
} catch (error) {
|
||||
activatedKey.forEach(deleteKey => delete this.testWebhookData[deleteKey] );
|
||||
await this.activeWebhooks!.removeWorkflow(workflow);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
ActiveExecutions,
|
||||
CredentialsHelper,
|
||||
Db,
|
||||
ExternalHooks,
|
||||
|
@ -108,51 +109,15 @@ function pruneExecutionData(): void {
|
|||
|
||||
// throttle just on success to allow for self healing on failure
|
||||
Db.collections.Execution!.delete({ stoppedAt: LessThanOrEqual(date.toISOString()) })
|
||||
.then(data =>
|
||||
setTimeout(() => {
|
||||
throttling = false;
|
||||
}, timeout * 1000)
|
||||
).catch(err => throttling = false);
|
||||
.then(data =>
|
||||
setTimeout(() => {
|
||||
throttling = false;
|
||||
}, timeout * 1000)
|
||||
).catch(err => throttling = false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pushes the execution out to all connected clients
|
||||
*
|
||||
* @param {WorkflowExecuteMode} mode The mode in which the workflow got started in
|
||||
* @param {IRun} fullRunData The RunData of the finished execution
|
||||
* @param {string} executionIdActive The id of the finished execution
|
||||
* @param {string} [executionIdDb] The database id of finished execution
|
||||
*/
|
||||
export function pushExecutionFinished(mode: WorkflowExecuteMode, fullRunData: IRun, executionIdActive: string, executionIdDb?: string, retryOf?: string) {
|
||||
// Clone the object except the runData. That one is not supposed
|
||||
// to be send. Because that data got send piece by piece after
|
||||
// each node which finished executing
|
||||
const pushRunData = {
|
||||
...fullRunData,
|
||||
data: {
|
||||
...fullRunData.data,
|
||||
resultData: {
|
||||
...fullRunData.data.resultData,
|
||||
runData: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Push data to editor-ui once workflow finished
|
||||
const sendData: IPushDataExecutionFinished = {
|
||||
executionIdActive,
|
||||
executionIdDb,
|
||||
data: pushRunData,
|
||||
retryOf,
|
||||
};
|
||||
|
||||
const pushInstance = Push.getInstance();
|
||||
pushInstance.send('executionFinished', sendData);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns hook functions to push data to Editor-UI
|
||||
*
|
||||
|
@ -192,25 +157,52 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
|
|||
],
|
||||
workflowExecuteBefore: [
|
||||
async function (this: WorkflowHooks): Promise<void> {
|
||||
// Push data to editor-ui once workflow finished
|
||||
if (this.mode === 'manual') {
|
||||
const pushInstance = Push.getInstance();
|
||||
pushInstance.send('executionStarted', {
|
||||
executionId: this.executionId,
|
||||
mode: this.mode,
|
||||
startedAt: new Date(),
|
||||
retryOf: this.retryOf,
|
||||
workflowId: this.workflowData.id as string,
|
||||
workflowName: this.workflowData.name,
|
||||
});
|
||||
// Push data to session which started the workflow
|
||||
if (this.sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
const pushInstance = Push.getInstance();
|
||||
pushInstance.send('executionStarted', {
|
||||
executionId: this.executionId,
|
||||
mode: this.mode,
|
||||
startedAt: new Date(),
|
||||
retryOf: this.retryOf,
|
||||
workflowId: this.workflowData.id as string,
|
||||
workflowName: this.workflowData.name,
|
||||
}, this.sessionId);
|
||||
},
|
||||
],
|
||||
workflowExecuteAfter: [
|
||||
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> {
|
||||
if (this.mode === 'manual') {
|
||||
pushExecutionFinished(this.mode, fullRunData, this.executionId, undefined, this.retryOf);
|
||||
// Push data to session which started the workflow
|
||||
if (this.sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clone the object except the runData. That one is not supposed
|
||||
// to be send. Because that data got send piece by piece after
|
||||
// each node which finished executing
|
||||
const pushRunData = {
|
||||
...fullRunData,
|
||||
data: {
|
||||
...fullRunData.data,
|
||||
resultData: {
|
||||
...fullRunData.data.resultData,
|
||||
runData: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Push data to editor-ui once workflow finished
|
||||
// TODO: Look at this again
|
||||
const sendData: IPushDataExecutionFinished = {
|
||||
executionId: this.executionId,
|
||||
data: pushRunData,
|
||||
retryOf: this.retryOf,
|
||||
};
|
||||
|
||||
const pushInstance = Push.getInstance();
|
||||
pushInstance.send('executionFinished', sendData, this.sessionId);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -238,59 +230,70 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
|
|||
return;
|
||||
}
|
||||
|
||||
const execution = await Db.collections.Execution!.findOne(this.executionId);
|
||||
try {
|
||||
const execution = await Db.collections.Execution!.findOne(this.executionId);
|
||||
|
||||
if (execution === undefined) {
|
||||
// Something went badly wrong if this happens.
|
||||
// This check is here mostly to make typescript happy.
|
||||
return undefined;
|
||||
}
|
||||
const fullExecutionData: IExecutionResponse = ResponseHelper.unflattenExecutionData(execution);
|
||||
if (execution === undefined) {
|
||||
// Something went badly wrong if this happens.
|
||||
// This check is here mostly to make typescript happy.
|
||||
return undefined;
|
||||
}
|
||||
const fullExecutionData: IExecutionResponse = ResponseHelper.unflattenExecutionData(execution);
|
||||
|
||||
if (fullExecutionData.finished) {
|
||||
// We already received ´workflowExecuteAfter´ webhook, so this is just an async call
|
||||
// that was left behind. We skip saving because the other call should have saved everything
|
||||
// so this one is safe to ignore
|
||||
return;
|
||||
if (fullExecutionData.finished) {
|
||||
// We already received ´workflowExecuteAfter´ webhook, so this is just an async call
|
||||
// that was left behind. We skip saving because the other call should have saved everything
|
||||
// so this one is safe to ignore
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (fullExecutionData.data === undefined) {
|
||||
fullExecutionData.data = {
|
||||
startData: {
|
||||
},
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [],
|
||||
waitingExecution: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(fullExecutionData.data.resultData.runData[nodeName])) {
|
||||
// Append data if array exists
|
||||
fullExecutionData.data.resultData.runData[nodeName].push(data);
|
||||
} else {
|
||||
// Initialize array and save data
|
||||
fullExecutionData.data.resultData.runData[nodeName] = [data];
|
||||
}
|
||||
|
||||
fullExecutionData.data.executionData = executionData.executionData;
|
||||
|
||||
// Set last executed node so that it may resume on failure
|
||||
fullExecutionData.data.resultData.lastNodeExecuted = nodeName;
|
||||
|
||||
const flattenedExecutionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||
|
||||
await Db.collections.Execution!.update(this.executionId, flattenedExecutionData as IExecutionFlattedDb);
|
||||
} catch (err) {
|
||||
// TODO: Improve in the future!
|
||||
// Errors here might happen because of database access
|
||||
// For busy machines, we may get "Database is locked" errors.
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
if (fullExecutionData.data === undefined) {
|
||||
fullExecutionData.data = {
|
||||
startData: {
|
||||
},
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [],
|
||||
waitingExecution: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(fullExecutionData.data.resultData.runData[nodeName])) {
|
||||
// Append data if array exists
|
||||
fullExecutionData.data.resultData.runData[nodeName].push(data);
|
||||
} else {
|
||||
// Initialize array and save data
|
||||
fullExecutionData.data.resultData.runData[nodeName] = [data];
|
||||
}
|
||||
|
||||
fullExecutionData.data.executionData = executionData.executionData;
|
||||
|
||||
// Set last executed node so that it may resume on failure
|
||||
fullExecutionData.data.resultData.lastNodeExecuted = nodeName;
|
||||
|
||||
const flattenedExecutionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||
|
||||
await Db.collections.Execution!.update(this.executionId, flattenedExecutionData as IExecutionFlattedDb);
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns hook functions to save workflow execution and call error workflow
|
||||
*
|
||||
|
@ -330,7 +333,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
|
||||
if (isManualMode && saveManualExecutions === false) {
|
||||
// Data is always saved, so we remove from database
|
||||
Db.collections.Execution!.delete(this.executionId);
|
||||
await Db.collections.Execution!.delete(this.executionId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -350,7 +353,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf);
|
||||
}
|
||||
// Data is always saved, so we remove from database
|
||||
Db.collections.Execution!.delete(this.executionId);
|
||||
await Db.collections.Execution!.delete(this.executionId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -397,57 +400,79 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
|
||||
|
||||
/**
|
||||
* Executes the workflow with the given ID
|
||||
* Returns hook functions to save workflow execution and call error workflow
|
||||
* for running with queues. Manual executions should never run on queues as
|
||||
* they are always executed in the main process.
|
||||
*
|
||||
* @export
|
||||
* @param {string} workflowId The id of the workflow to execute
|
||||
* @param {IWorkflowExecuteAdditionalData} additionalData
|
||||
* @param {INodeExecutionData[]} [inputData]
|
||||
* @returns {(Promise<Array<INodeExecutionData[] | null>>)}
|
||||
* @returns {IWorkflowExecuteHooks}
|
||||
*/
|
||||
export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[]): Promise<Array<INodeExecutionData[] | null>> {
|
||||
function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||
return {
|
||||
nodeExecuteBefore: [],
|
||||
nodeExecuteAfter: [],
|
||||
workflowExecuteBefore: [],
|
||||
workflowExecuteAfter: [
|
||||
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> {
|
||||
try {
|
||||
if (WorkflowHelpers.isWorkflowIdValid(this.workflowData.id as string) === true && newStaticData) {
|
||||
// Workflow is saved so update in database
|
||||
try {
|
||||
await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData);
|
||||
} catch (e) {
|
||||
// TODO: Add proper logging!
|
||||
console.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check config to know if execution should be saved or not
|
||||
let saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
|
||||
if (this.workflowData.settings !== undefined) {
|
||||
saveDataErrorExecution = (this.workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution;
|
||||
}
|
||||
|
||||
const workflowDidSucceed = !fullRunData.data.resultData.error;
|
||||
if (workflowDidSucceed === false && saveDataErrorExecution === 'none') {
|
||||
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf);
|
||||
}
|
||||
|
||||
const fullExecutionData: IExecutionDb = {
|
||||
data: fullRunData.data,
|
||||
mode: fullRunData.mode,
|
||||
finished: fullRunData.finished ? fullRunData.finished : false,
|
||||
startedAt: fullRunData.startedAt,
|
||||
stoppedAt: fullRunData.stoppedAt,
|
||||
workflowData: this.workflowData,
|
||||
};
|
||||
|
||||
if (this.retryOf !== undefined) {
|
||||
fullExecutionData.retryOf = this.retryOf.toString();
|
||||
}
|
||||
|
||||
if (this.workflowData.id !== undefined && WorkflowHelpers.isWorkflowIdValid(this.workflowData.id.toString()) === true) {
|
||||
fullExecutionData.workflowId = this.workflowData.id.toString();
|
||||
}
|
||||
|
||||
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||
|
||||
// Save the Execution in DB
|
||||
await Db.collections.Execution!.update(this.executionId, executionData as IExecutionFlattedDb);
|
||||
|
||||
if (fullRunData.finished === true && this.retryOf !== undefined) {
|
||||
// If the retry was successful save the reference it on the original execution
|
||||
// await Db.collections.Execution!.save(executionData as IExecutionFlattedDb);
|
||||
await Db.collections.Execution!.update(this.retryOf, { retrySuccessId: this.executionId });
|
||||
}
|
||||
} catch (error) {
|
||||
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf);
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function getRunData(workflowData: IWorkflowBase, inputData?: INodeExecutionData[]): Promise<IWorkflowExecutionDataProcess> {
|
||||
const mode = 'integrated';
|
||||
|
||||
if (workflowInfo.id === undefined && workflowInfo.code === undefined) {
|
||||
throw new Error(`No information about the workflow to execute found. Please provide either the "id" or "code"!`);
|
||||
}
|
||||
|
||||
if (Db.collections!.Workflow === null) {
|
||||
// The first time executeWorkflow gets called the Database has
|
||||
// to get initialized first
|
||||
await Db.init();
|
||||
}
|
||||
|
||||
let workflowData: IWorkflowBase | undefined;
|
||||
if (workflowInfo.id !== undefined) {
|
||||
workflowData = await Db.collections!.Workflow!.findOne(workflowInfo.id);
|
||||
if (workflowData === undefined) {
|
||||
throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`);
|
||||
}
|
||||
} else {
|
||||
workflowData = workflowInfo.code;
|
||||
}
|
||||
|
||||
const externalHooks = ExternalHooks();
|
||||
await externalHooks.init();
|
||||
|
||||
const nodeTypes = NodeTypes();
|
||||
|
||||
const workflowName = workflowData ? workflowData.name : undefined;
|
||||
const workflow = new Workflow({ id: workflowInfo.id, name: workflowName, nodes: workflowData!.nodes, connections: workflowData!.connections, active: workflowData!.active, nodeTypes, staticData: workflowData!.staticData });
|
||||
|
||||
// Does not get used so set it simply to empty string
|
||||
const executionId = '';
|
||||
|
||||
// Get the needed credentials for the current workflow as they will differ to the ones of the
|
||||
// calling workflow.
|
||||
const credentials = await WorkflowCredentials(workflowData!.nodes);
|
||||
|
||||
// Create new additionalData to have different workflow loaded and to call
|
||||
// different webooks
|
||||
const additionalDataIntegrated = await getBase(credentials);
|
||||
additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(mode, executionId, workflowData!, { parentProcessMode: additionalData.hooks!.mode });
|
||||
|
||||
// Find Start-Node
|
||||
const requiredNodeTypes = ['n8n-nodes-base.start'];
|
||||
let startNode: INode | undefined;
|
||||
|
@ -494,17 +519,113 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
|
|||
},
|
||||
};
|
||||
|
||||
// Get the needed credentials for the current workflow as they will differ to the ones of the
|
||||
// calling workflow.
|
||||
const credentials = await WorkflowCredentials(workflowData!.nodes);
|
||||
|
||||
const runData: IWorkflowExecutionDataProcess = {
|
||||
credentials,
|
||||
executionMode: mode,
|
||||
executionData: runExecutionData,
|
||||
// @ts-ignore
|
||||
workflowData,
|
||||
};
|
||||
|
||||
return runData;
|
||||
}
|
||||
|
||||
|
||||
export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promise<IWorkflowBase> {
|
||||
if (workflowInfo.id === undefined && workflowInfo.code === undefined) {
|
||||
throw new Error(`No information about the workflow to execute found. Please provide either the "id" or "code"!`);
|
||||
}
|
||||
|
||||
if (Db.collections!.Workflow === null) {
|
||||
// The first time executeWorkflow gets called the Database has
|
||||
// to get initialized first
|
||||
await Db.init();
|
||||
}
|
||||
|
||||
let workflowData: IWorkflowBase | undefined;
|
||||
if (workflowInfo.id !== undefined) {
|
||||
workflowData = await Db.collections!.Workflow!.findOne(workflowInfo.id);
|
||||
if (workflowData === undefined) {
|
||||
throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`);
|
||||
}
|
||||
} else {
|
||||
workflowData = workflowInfo.code;
|
||||
}
|
||||
|
||||
return workflowData!;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the workflow with the given ID
|
||||
*
|
||||
* @export
|
||||
* @param {string} workflowId The id of the workflow to execute
|
||||
* @param {IWorkflowExecuteAdditionalData} additionalData
|
||||
* @param {INodeExecutionData[]} [inputData]
|
||||
* @returns {(Promise<Array<INodeExecutionData[] | null>>)}
|
||||
*/
|
||||
export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[], parentExecutionId?: string, loadedWorkflowData?: IWorkflowBase, loadedRunData?: IWorkflowExecutionDataProcess): Promise<Array<INodeExecutionData[] | null> | IRun> {
|
||||
const externalHooks = ExternalHooks();
|
||||
await externalHooks.init();
|
||||
|
||||
const nodeTypes = NodeTypes();
|
||||
|
||||
const workflowData = loadedWorkflowData !== undefined ? loadedWorkflowData : await getWorkflowData(workflowInfo);
|
||||
|
||||
const workflowName = workflowData ? workflowData.name : undefined;
|
||||
const workflow = new Workflow({ id: workflowInfo.id, name: workflowName, nodes: workflowData!.nodes, connections: workflowData!.connections, active: workflowData!.active, nodeTypes, staticData: workflowData!.staticData });
|
||||
|
||||
const runData = loadedRunData !== undefined ? loadedRunData : await getRunData(workflowData, inputData);
|
||||
|
||||
let executionId;
|
||||
|
||||
if (parentExecutionId !== undefined) {
|
||||
executionId = parentExecutionId;
|
||||
} else {
|
||||
executionId = parentExecutionId !== undefined ? parentExecutionId : await ActiveExecutions.getInstance().add(runData);
|
||||
}
|
||||
|
||||
const runExecutionData = runData.executionData as IRunExecutionData;
|
||||
|
||||
// Get the needed credentials for the current workflow as they will differ to the ones of the
|
||||
// calling workflow.
|
||||
const credentials = await WorkflowCredentials(workflowData!.nodes);
|
||||
|
||||
|
||||
// Create new additionalData to have different workflow loaded and to call
|
||||
// different webooks
|
||||
const additionalDataIntegrated = await getBase(credentials);
|
||||
additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(runData.executionMode, executionId, workflowData!, { parentProcessMode: additionalData.hooks!.mode });
|
||||
// Make sure we pass on the original executeWorkflow function we received
|
||||
// This one already contains changes to talk to parent process
|
||||
// and get executionID from `activeExecutions` running on main process
|
||||
additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow;
|
||||
|
||||
|
||||
// Execute the workflow
|
||||
const workflowExecute = new WorkflowExecute(additionalDataIntegrated, mode, runExecutionData);
|
||||
const workflowExecute = new WorkflowExecute(additionalDataIntegrated, runData.executionMode, runExecutionData);
|
||||
const data = await workflowExecute.processRunExecutionData(workflow);
|
||||
|
||||
await externalHooks.run('workflow.postExecute', [data, workflowData]);
|
||||
|
||||
if (data.finished === true) {
|
||||
// Workflow did finish successfully
|
||||
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
|
||||
return returnData!.data!.main;
|
||||
|
||||
if (parentExecutionId !== undefined) {
|
||||
return data;
|
||||
} else {
|
||||
await ActiveExecutions.getInstance().remove(executionId, data);
|
||||
|
||||
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
|
||||
return returnData!.data!.main;
|
||||
}
|
||||
} else {
|
||||
await ActiveExecutions.getInstance().remove(executionId, data);
|
||||
// Workflow did fail
|
||||
const error = new Error(data.data.resultData.error!.message);
|
||||
error.stack = data.data.resultData.error!.stack;
|
||||
|
@ -564,6 +685,22 @@ export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionI
|
|||
return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns WorkflowHooks instance for running integrated workflows
|
||||
* (Workflows which get started inside of another workflow)
|
||||
*/
|
||||
export function getWorkflowHooksWorkerExecuter(mode: WorkflowExecuteMode, executionId: string, workflowData: IWorkflowBase, optionalParameters?: IWorkflowHooksOptionalParameters): WorkflowHooks {
|
||||
optionalParameters = optionalParameters || {};
|
||||
const hookFunctions = hookFunctionsSaveWorker();
|
||||
const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode);
|
||||
for (const key of Object.keys(preExecuteFunctions)) {
|
||||
if (hookFunctions[key] === undefined) {
|
||||
hookFunctions[key] = [];
|
||||
}
|
||||
hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]);
|
||||
}
|
||||
return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns WorkflowHooks instance for main process if workflow runs via worker
|
||||
|
@ -616,6 +753,6 @@ export function getWorkflowHooksMain(data: IWorkflowExecutionDataProcess, execut
|
|||
}
|
||||
}
|
||||
|
||||
return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData, { sessionId: data.sessionId, retryOf: data.retryOf as string});
|
||||
return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData, { sessionId: data.sessionId, retryOf: data.retryOf as string });
|
||||
}
|
||||
|
||||
|
|
|
@ -101,9 +101,6 @@ export class WorkflowRunner {
|
|||
// Remove from active execution with empty data. That will
|
||||
// set the execution to failed.
|
||||
this.activeExecutions.remove(executionId, fullRunData);
|
||||
|
||||
// Also send to Editor UI
|
||||
WorkflowExecuteAdditionalData.pushExecutionFinished(executionMode, fullRunData, executionId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -349,6 +346,26 @@ export class WorkflowRunner {
|
|||
// Normally also static data should be supplied here but as it only used for sending
|
||||
// data to editor-UI is not needed.
|
||||
hooks.executeHookFunctions('workflowExecuteAfter', [runData]);
|
||||
try {
|
||||
// Check if this execution data has to be removed from database
|
||||
// based on workflow settings.
|
||||
let saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
|
||||
let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
|
||||
if (data.workflowData.settings !== undefined) {
|
||||
saveDataErrorExecution = (data.workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution;
|
||||
saveDataSuccessExecution = (data.workflowData.settings.saveDataSuccessExecution as string) || saveDataSuccessExecution;
|
||||
}
|
||||
|
||||
const workflowDidSucceed = !runData.data.resultData.error;
|
||||
if (workflowDidSucceed === true && saveDataSuccessExecution === 'none' ||
|
||||
workflowDidSucceed === false && saveDataErrorExecution === 'none'
|
||||
) {
|
||||
await Db.collections.Execution!.delete(executionId);
|
||||
}
|
||||
} catch (err) {
|
||||
// We don't want errors here to crash n8n. Just log and proceed.
|
||||
console.log('Error removing saved execution from database. More details: ', err);
|
||||
}
|
||||
|
||||
resolve(runData);
|
||||
});
|
||||
|
@ -440,7 +457,7 @@ export class WorkflowRunner {
|
|||
|
||||
|
||||
// Listen to data from the subprocess
|
||||
subprocess.on('message', (message: IProcessMessage) => {
|
||||
subprocess.on('message', async (message: IProcessMessage) => {
|
||||
if (message.type === 'end') {
|
||||
clearTimeout(executionTimeout);
|
||||
this.activeExecutions.remove(executionId!, message.data.runData);
|
||||
|
@ -457,6 +474,11 @@ export class WorkflowRunner {
|
|||
const timeoutError = { message: 'Workflow execution timed out!' } as IExecutionError;
|
||||
|
||||
this.processError(timeoutError, startedAt, data.executionMode, executionId);
|
||||
} else if (message.type === 'startExecution') {
|
||||
const executionId = await this.activeExecutions.add(message.data.runData);
|
||||
subprocess.send({ type: 'executionId', data: {executionId} } as IProcessMessage);
|
||||
} else if (message.type === 'finishExecution') {
|
||||
await this.activeExecutions.remove(message.data.executionId, message.data.result);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
IWorkflowExecutionDataProcessWithExecution,
|
||||
NodeTypes,
|
||||
WorkflowExecuteAdditionalData,
|
||||
WorkflowHelpers,
|
||||
} from './';
|
||||
|
||||
import {
|
||||
|
@ -17,12 +18,15 @@ import {
|
|||
import {
|
||||
IDataObject,
|
||||
IExecuteData,
|
||||
IExecuteWorkflowInfo,
|
||||
IExecutionError,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeData,
|
||||
IRun,
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
IWorkflowExecuteHooks,
|
||||
Workflow,
|
||||
WorkflowHooks,
|
||||
|
@ -35,9 +39,20 @@ export class WorkflowRunnerProcess {
|
|||
startedAt = new Date();
|
||||
workflow: Workflow | undefined;
|
||||
workflowExecute: WorkflowExecute | undefined;
|
||||
executionIdCallback: (executionId: string) => void | undefined;
|
||||
|
||||
static async stopProcess() {
|
||||
setTimeout(() => {
|
||||
// Attempt a graceful shutdown, giving executions 30 seconds to finish
|
||||
process.exit(0);
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
|
||||
async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise<IRun> {
|
||||
process.on('SIGTERM', WorkflowRunnerProcess.stopProcess);
|
||||
process.on('SIGINT', WorkflowRunnerProcess.stopProcess);
|
||||
|
||||
this.data = inputData;
|
||||
let className: string;
|
||||
let tempNode: INodeType;
|
||||
|
@ -92,10 +107,35 @@ export class WorkflowRunnerProcess {
|
|||
await Db.init();
|
||||
}
|
||||
|
||||
this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings});
|
||||
this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings });
|
||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials);
|
||||
additionalData.hooks = this.getProcessForwardHooks();
|
||||
|
||||
const executeWorkflowFunction = additionalData.executeWorkflow;
|
||||
additionalData.executeWorkflow = async (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[] | undefined): Promise<Array<INodeExecutionData[] | null> | IRun> => {
|
||||
const workflowData = await WorkflowExecuteAdditionalData.getWorkflowData(workflowInfo);
|
||||
const runData = await WorkflowExecuteAdditionalData.getRunData(workflowData, inputData);
|
||||
await sendToParentProcess('startExecution', { runData });
|
||||
const executionId: string = await new Promise((resolve) => {
|
||||
this.executionIdCallback = (executionId: string) => {
|
||||
resolve(executionId);
|
||||
};
|
||||
});
|
||||
let result: IRun;
|
||||
try {
|
||||
result = await executeWorkflowFunction(workflowInfo, additionalData, inputData, executionId, workflowData, runData);
|
||||
} catch (e) {
|
||||
await sendToParentProcess('finishExecution', { executionId });
|
||||
// Throw same error we had
|
||||
throw e;
|
||||
}
|
||||
|
||||
await sendToParentProcess('finishExecution', { executionId, result });
|
||||
|
||||
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(result);
|
||||
return returnData!.data!.main;
|
||||
};
|
||||
|
||||
if (this.data.executionData !== undefined) {
|
||||
this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode, this.data.executionData);
|
||||
return this.workflowExecute.processRunExecutionData(this.workflow);
|
||||
|
@ -152,8 +192,8 @@ export class WorkflowRunnerProcess {
|
|||
},
|
||||
],
|
||||
nodeExecuteAfter: [
|
||||
async (nodeName: string, data: ITaskData, executionData: IRunExecutionData): Promise<void> => {
|
||||
this.sendHookToParentProcess('nodeExecuteAfter', [nodeName, data, executionData]);
|
||||
async (nodeName: string, data: ITaskData): Promise<void> => {
|
||||
this.sendHookToParentProcess('nodeExecuteAfter', [nodeName, data]);
|
||||
},
|
||||
],
|
||||
workflowExecuteBefore: [
|
||||
|
@ -257,6 +297,8 @@ process.on('message', async (message: IProcessMessage) => {
|
|||
|
||||
// Stop process
|
||||
process.exit();
|
||||
} else if (message.type === 'executionId') {
|
||||
workflowRunner.executionIdCallback(message.data.executionId);
|
||||
}
|
||||
} catch (error) {
|
||||
// Catch all uncaught errors and forward them to parent process
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import * as config from '../../../../config';
|
||||
|
||||
export class ChangeDataSize1615306975123 implements MigrationInterface {
|
||||
name = 'ChangeDataSize1615306975123';
|
||||
|
||||
async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'execution_entity` MODIFY COLUMN `data` MEDIUMTEXT NOT NULL');
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'execution_entity` MODIFY COLUMN `data` TEXT NOT NULL');
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import { WebhookModel1592447867632 } from './1592447867632-WebhookModel';
|
|||
import { CreateIndexStoppedAt1594902918301 } from './1594902918301-CreateIndexStoppedAt';
|
||||
import { AddWebhookId1611149998770 } from './1611149998770-AddWebhookId';
|
||||
import { MakeStoppedAtNullable1607431743767 } from './1607431743767-MakeStoppedAtNullable';
|
||||
import { ChangeDataSize1615306975123 } from './1615306975123-ChangeDataSize';
|
||||
|
||||
export const mysqlMigrations = [
|
||||
InitialMigration1588157391238,
|
||||
|
@ -10,4 +11,5 @@ export const mysqlMigrations = [
|
|||
CreateIndexStoppedAt1594902918301,
|
||||
AddWebhookId1611149998770,
|
||||
MakeStoppedAtNullable1607431743767,
|
||||
ChangeDataSize1615306975123,
|
||||
];
|
||||
|
|
|
@ -1,74 +1,74 @@
|
|||
{
|
||||
"name": "n8n-core",
|
||||
"version": "0.62.0",
|
||||
"description": "Core functionality of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
"name": "n8n-core",
|
||||
"version": "0.67.0",
|
||||
"description": "Core functionality of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"main": "dist/src/index",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "npm run watch",
|
||||
"tslint": "tslint -p tsconfig.json -c tslint.json",
|
||||
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
|
||||
"watch": "tsc --watch",
|
||||
"test": "jest"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/cron": "^1.7.1",
|
||||
"@types/crypto-js": "^4.0.1",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jest": "^26.0.13",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/node": "14.0.27",
|
||||
"@types/request-promise-native": "~1.0.15",
|
||||
"jest": "^26.4.2",
|
||||
"source-map-support": "^0.5.9",
|
||||
"ts-jest": "^26.3.0",
|
||||
"tslint": "^6.1.2",
|
||||
"typescript": "~3.9.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"client-oauth2": "^4.2.5",
|
||||
"cron": "^1.7.2",
|
||||
"crypto-js": "4.0.0",
|
||||
"file-type": "^14.6.2",
|
||||
"lodash.get": "^4.4.2",
|
||||
"mime-types": "^2.1.27",
|
||||
"n8n-workflow": "~0.55.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.7"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"main": "dist/src/index",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "npm run watch",
|
||||
"tslint": "tslint -p tsconfig.json -c tslint.json",
|
||||
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
|
||||
"watch": "tsc --watch",
|
||||
"test": "jest"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"testURL": "http://localhost/",
|
||||
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||
"testPathIgnorePatterns": [
|
||||
"/dist/",
|
||||
"/node_modules/"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/cron": "^1.7.1",
|
||||
"@types/crypto-js": "^4.0.1",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jest": "^26.0.13",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/node": "14.0.27",
|
||||
"@types/request-promise-native": "~1.0.15",
|
||||
"jest": "^26.4.2",
|
||||
"source-map-support": "^0.5.9",
|
||||
"ts-jest": "^26.3.0",
|
||||
"tslint": "^6.1.2",
|
||||
"typescript": "~3.9.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"client-oauth2": "^4.2.5",
|
||||
"cron": "^1.7.2",
|
||||
"crypto-js": "4.0.0",
|
||||
"file-type": "^14.6.2",
|
||||
"lodash.get": "^4.4.2",
|
||||
"mime-types": "^2.1.27",
|
||||
"n8n-workflow": "~0.51.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.7"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"testURL": "http://localhost/",
|
||||
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||
"testPathIgnorePatterns": [
|
||||
"/dist/",
|
||||
"/node_modules/"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"json",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"json",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
IWebhookData,
|
||||
WebhookHttpMethod,
|
||||
Workflow,
|
||||
WorkflowActivateMode,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
@ -30,7 +31,7 @@ export class ActiveWebhooks {
|
|||
* @returns {Promise<void>}
|
||||
* @memberof ActiveWebhooks
|
||||
*/
|
||||
async add(workflow: Workflow, webhookData: IWebhookData, mode: WorkflowExecuteMode): Promise<void> {
|
||||
async add(workflow: Workflow, webhookData: IWebhookData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise<void> {
|
||||
if (workflow.id === undefined) {
|
||||
throw new Error('Webhooks can only be added for saved workflows as an id is needed!');
|
||||
}
|
||||
|
@ -57,10 +58,10 @@ export class ActiveWebhooks {
|
|||
this.webhookUrls[webhookKey].push(webhookData);
|
||||
|
||||
try {
|
||||
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
|
||||
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, this.testWebhooks);
|
||||
if (webhookExists !== true) {
|
||||
// If webhook does not exist yet create it
|
||||
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
|
||||
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, activation, this.testWebhooks);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -183,7 +184,7 @@ export class ActiveWebhooks {
|
|||
|
||||
// Go through all the registered webhooks of the workflow and remove them
|
||||
for (const webhookData of webhooks) {
|
||||
await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
|
||||
await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, 'update', this.testWebhooks);
|
||||
|
||||
delete this.webhookUrls[this.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId)];
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import {
|
|||
ITriggerResponse,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
Workflow,
|
||||
WorkflowActivateMode,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
|
@ -66,14 +68,14 @@ export class ActiveWorkflows {
|
|||
* @returns {Promise<void>}
|
||||
* @memberof ActiveWorkflows
|
||||
*/
|
||||
async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise<void> {
|
||||
async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise<void> {
|
||||
this.workflowData[id] = {};
|
||||
const triggerNodes = workflow.getTriggerNodes();
|
||||
|
||||
let triggerResponse: ITriggerResponse | undefined;
|
||||
this.workflowData[id].triggerResponses = [];
|
||||
for (const triggerNode of triggerNodes) {
|
||||
triggerResponse = await workflow.runTrigger(triggerNode, getTriggerFunctions, additionalData, 'trigger');
|
||||
triggerResponse = await workflow.runTrigger(triggerNode, getTriggerFunctions, additionalData, mode, activation);
|
||||
if (triggerResponse !== undefined) {
|
||||
// If a response was given save it
|
||||
this.workflowData[id].triggerResponses!.push(triggerResponse);
|
||||
|
@ -84,7 +86,7 @@ export class ActiveWorkflows {
|
|||
if (pollNodes.length) {
|
||||
this.workflowData[id].pollResponses = [];
|
||||
for (const pollNode of pollNodes) {
|
||||
this.workflowData[id].pollResponses!.push(await this.activatePolling(pollNode, workflow, additionalData, getPollFunctions));
|
||||
this.workflowData[id].pollResponses!.push(await this.activatePolling(pollNode, workflow, additionalData, getPollFunctions, mode, activation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,10 +102,8 @@ export class ActiveWorkflows {
|
|||
* @returns {Promise<IPollResponse>}
|
||||
* @memberof ActiveWorkflows
|
||||
*/
|
||||
async activatePolling(node: INode, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getPollFunctions: IGetExecutePollFunctions): Promise<IPollResponse> {
|
||||
const mode = 'trigger';
|
||||
|
||||
const pollFunctions = getPollFunctions(workflow, node, additionalData, mode);
|
||||
async activatePolling(node: INode, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getPollFunctions: IGetExecutePollFunctions, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise<IPollResponse> {
|
||||
const pollFunctions = getPollFunctions(workflow, node, additionalData, mode, activation);
|
||||
|
||||
const pollTimes = pollFunctions.getNodeParameter('pollTimes') as unknown as {
|
||||
item: ITriggerTime[];
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
NodeHelpers,
|
||||
NodeParameterValue,
|
||||
Workflow,
|
||||
WorkflowActivateMode,
|
||||
WorkflowDataProxy,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
@ -103,6 +104,9 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m
|
|||
|
||||
const filePathParts = path.parse(filePath as string);
|
||||
|
||||
if (filePathParts.dir !== '') {
|
||||
returnData.directory = filePathParts.dir;
|
||||
}
|
||||
returnData.fileName = filePathParts.base;
|
||||
|
||||
// Remove the dot
|
||||
|
@ -161,8 +165,9 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin
|
|||
|
||||
return this.helpers.request!(newRequestOptions)
|
||||
.catch(async (error: IResponseError) => {
|
||||
// TODO: Check if also other codes are possible
|
||||
if (error.statusCode === 401) {
|
||||
const statusCodeReturned = oAuth2Options?.tokenExpiredStatusCode === undefined ? 401 : oAuth2Options?.tokenExpiredStatusCode;
|
||||
|
||||
if (error.statusCode === statusCodeReturned) {
|
||||
// Token is probably not valid anymore. So try refresh it.
|
||||
|
||||
const tokenRefreshOptions: IDataObject = {};
|
||||
|
@ -531,7 +536,7 @@ export function getWorkflowMetadata(workflow: Workflow): IWorkflowMetadata {
|
|||
* @returns {ITriggerFunctions}
|
||||
*/
|
||||
// TODO: Check if I can get rid of: additionalData, and so then maybe also at ActiveWorkflowRunner.add
|
||||
export function getExecutePollFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IPollFunctions {
|
||||
export function getExecutePollFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IPollFunctions {
|
||||
return ((workflow: Workflow, node: INode) => {
|
||||
return {
|
||||
__emit: (data: INodeExecutionData[][]): void => {
|
||||
|
@ -543,6 +548,9 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio
|
|||
getMode: (): WorkflowExecuteMode => {
|
||||
return mode;
|
||||
},
|
||||
getActivationMode: (): WorkflowActivateMode => {
|
||||
return activation;
|
||||
},
|
||||
getNode: () => {
|
||||
return getNode(node);
|
||||
},
|
||||
|
@ -594,7 +602,7 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio
|
|||
* @returns {ITriggerFunctions}
|
||||
*/
|
||||
// TODO: Check if I can get rid of: additionalData, and so then maybe also at ActiveWorkflowRunner.add
|
||||
export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): ITriggerFunctions {
|
||||
export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): ITriggerFunctions {
|
||||
return ((workflow: Workflow, node: INode) => {
|
||||
return {
|
||||
emit: (data: INodeExecutionData[][]): void => {
|
||||
|
@ -609,6 +617,9 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi
|
|||
getMode: (): WorkflowExecuteMode => {
|
||||
return mode;
|
||||
},
|
||||
getActivationMode: (): WorkflowActivateMode => {
|
||||
return activation;
|
||||
},
|
||||
getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any
|
||||
const runExecutionData: IRunExecutionData | null = null;
|
||||
const itemIndex = 0;
|
||||
|
@ -906,7 +917,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio
|
|||
* @param {WorkflowExecuteMode} mode
|
||||
* @returns {IHookFunctions}
|
||||
*/
|
||||
export function getExecuteHookFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, isTest?: boolean, webhookData?: IWebhookData): IHookFunctions {
|
||||
export function getExecuteHookFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, isTest?: boolean, webhookData?: IWebhookData): IHookFunctions {
|
||||
return ((workflow: Workflow, node: INode) => {
|
||||
const that = {
|
||||
getCredentials(type: string): ICredentialDataDecryptedObject | undefined {
|
||||
|
@ -915,6 +926,9 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio
|
|||
getMode: (): WorkflowExecuteMode => {
|
||||
return mode;
|
||||
},
|
||||
getActivationMode: (): WorkflowActivateMode => {
|
||||
return activation;
|
||||
},
|
||||
getNode: () => {
|
||||
return getNode(node);
|
||||
},
|
||||
|
|
|
@ -22,6 +22,8 @@ import {
|
|||
NodeExecuteFunctions,
|
||||
} from './';
|
||||
|
||||
import { get } from 'lodash';
|
||||
|
||||
export class WorkflowExecute {
|
||||
runExecutionData: IRunExecutionData;
|
||||
private additionalData: IWorkflowExecuteAdditionalData;
|
||||
|
@ -234,6 +236,21 @@ export class WorkflowExecute {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks the incoming connection does not receive any data
|
||||
*/
|
||||
incomingConnectionIsEmpty(runData: IRunData, inputConnections: IConnection[], runIndex: number): boolean {
|
||||
// for (const inputConnection of workflow.connectionsByDestinationNode[nodeToAdd].main[0]) {
|
||||
for (const inputConnection of inputConnections) {
|
||||
const nodeIncomingData = get(runData, `[${inputConnection.node}][${runIndex}].data.main[${inputConnection.index}]`);
|
||||
if (nodeIncomingData !== undefined && (nodeIncomingData as object[]).length !== 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
addNodeToBeExecuted(workflow: Workflow, connectionData: IConnection, outputIndex: number, parentNodeName: string, nodeSuccessData: INodeExecutionData[][], runIndex: number): void {
|
||||
let stillDataMissing = false;
|
||||
|
||||
|
@ -299,7 +316,7 @@ export class WorkflowExecute {
|
|||
|
||||
if (nodeWasWaiting === false) {
|
||||
|
||||
// Get a list of all the output nodes that we can check for siblings eaiser
|
||||
// Get a list of all the output nodes that we can check for siblings easier
|
||||
const checkOutputNodes = [];
|
||||
for (const outputIndexParent in workflow.connectionsBySourceNode[parentNodeName].main) {
|
||||
if (!workflow.connectionsBySourceNode[parentNodeName].main.hasOwnProperty(outputIndexParent)) {
|
||||
|
@ -327,8 +344,12 @@ export class WorkflowExecute {
|
|||
// previously processed one
|
||||
if (inputData.node !== parentNodeName && checkOutputNodes.includes(inputData.node)) {
|
||||
// So the parent node will be added anyway which
|
||||
// will then process this node next. So nothing to do.
|
||||
continue;
|
||||
// will then process this node next. So nothing to do
|
||||
// unless the incoming data of the node is empty
|
||||
// because then it would not be executed
|
||||
if (!this.incomingConnectionIsEmpty(this.runExecutionData.resultData.runData, workflow.connectionsByDestinationNode[inputData.node].main[0], runIndex)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it is already in the execution stack
|
||||
|
@ -384,7 +405,19 @@ export class WorkflowExecute {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) {
|
||||
let addEmptyItem = false;
|
||||
|
||||
if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) {
|
||||
// Add empty item if the node does not have any input connections
|
||||
addEmptyItem = true;
|
||||
} else {
|
||||
if (this.incomingConnectionIsEmpty(this.runExecutionData.resultData.runData, workflow.connectionsByDestinationNode[nodeToAdd].main[0], runIndex)) {
|
||||
// Add empty item also if the input data is empty
|
||||
addEmptyItem = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (addEmptyItem === true) {
|
||||
// Add only node if it does not have any inputs because else it will
|
||||
// be added by its input node later anyway.
|
||||
this.runExecutionData.executionData!.nodeExecutionStack.push(
|
||||
|
@ -524,7 +557,7 @@ export class WorkflowExecute {
|
|||
executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
|
||||
executionNode = executionData.node;
|
||||
|
||||
this.executeHook('nodeExecuteBefore', [executionNode.name]);
|
||||
await this.executeHook('nodeExecuteBefore', [executionNode.name]);
|
||||
|
||||
// Get the index of the current run
|
||||
runIndex = 0;
|
||||
|
@ -689,7 +722,7 @@ export class WorkflowExecute {
|
|||
// Add the execution data again so that it can get restarted
|
||||
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
|
||||
|
||||
this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]);
|
||||
await this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -596,7 +596,7 @@ class NodeTypesClass implements INodeTypes {
|
|||
let item: INodeExecutionData;
|
||||
let keepOnlySet: boolean;
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, []) as boolean;
|
||||
keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, false) as boolean;
|
||||
item = items[itemIndex];
|
||||
const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject;
|
||||
|
||||
|
|
|
@ -986,6 +986,172 @@ describe('WorkflowExecute', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should use empty data if input of sibling does not receive any data from parent',
|
||||
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": {
|
||||
"conditions": {
|
||||
"number": [
|
||||
{
|
||||
"value1": "={{$json[\"value1\"]}}",
|
||||
"operation": "equal",
|
||||
"value2": 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"name": "IF",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
650,
|
||||
300,
|
||||
],
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [],
|
||||
"number": [
|
||||
{
|
||||
"name": "value2",
|
||||
"value": 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
"options": {},
|
||||
},
|
||||
"name": "Set2",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
850,
|
||||
450,
|
||||
],
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"number": [
|
||||
{
|
||||
"name": "value1",
|
||||
"value": 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
"options": {},
|
||||
},
|
||||
"name": "Set1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
450,
|
||||
300,
|
||||
],
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"name": "Merge",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1050,
|
||||
300,
|
||||
],
|
||||
},
|
||||
],
|
||||
"connections": {
|
||||
"Start": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set1",
|
||||
"type": "main",
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
"IF": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Merge",
|
||||
"type": "main",
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Set2",
|
||||
"type": "main",
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
"Set2": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Merge",
|
||||
"type": "main",
|
||||
"index": 1,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
"Set1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "IF",
|
||||
"type": "main",
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeExecutionOrder: [
|
||||
'Start',
|
||||
'Set1',
|
||||
'IF',
|
||||
'Set2',
|
||||
'Merge',
|
||||
],
|
||||
nodeData: {
|
||||
Merge: [
|
||||
[
|
||||
{
|
||||
value1: 1,
|
||||
},
|
||||
{
|
||||
value2: 2,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
|
|
@ -6,3 +6,10 @@ indent_style = tab
|
|||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[package.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-editor-ui",
|
||||
"version": "0.77.0",
|
||||
"version": "0.84.0",
|
||||
"description": "Workflow Editor UI for n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -65,7 +65,7 @@
|
|||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"n8n-workflow": "~0.51.0",
|
||||
"n8n-workflow": "~0.55.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"prismjs": "^1.17.1",
|
||||
|
|
|
@ -314,8 +314,7 @@ export interface IExecutionsListResponse {
|
|||
}
|
||||
|
||||
export interface IExecutionsCurrentSummaryExtended {
|
||||
id?: string;
|
||||
idActive: string;
|
||||
id: string;
|
||||
finished?: boolean;
|
||||
mode: WorkflowExecuteMode;
|
||||
retryOf?: string;
|
||||
|
@ -334,8 +333,7 @@ export interface IExecutionsStopData {
|
|||
}
|
||||
|
||||
export interface IExecutionsSummary {
|
||||
id?: string; // executionIdDb
|
||||
idActive?: string; // executionIdActive
|
||||
id: string;
|
||||
mode: WorkflowExecuteMode;
|
||||
finished?: boolean;
|
||||
retryOf?: string;
|
||||
|
@ -370,8 +368,7 @@ export interface IPushDataExecutionStarted {
|
|||
|
||||
export interface IPushDataExecutionFinished {
|
||||
data: IRun;
|
||||
executionIdActive: string;
|
||||
executionIdDb?: string;
|
||||
executionId: string;
|
||||
retryOf?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
{{parameter.displayName}}:
|
||||
</div>
|
||||
<div class="text-editor" @keydown.stop>
|
||||
<prism-editor :lineNumbers="true" :code="value" @change="valueChanged" language="js"></prism-editor>
|
||||
<prism-editor :lineNumbers="true" :code="value" :readonly="isReadOnly" @change="valueChanged" language="js"></prism-editor>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
@ -14,42 +14,43 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
// @ts-ignore
|
||||
import PrismEditor from 'vue-prism-editor';
|
||||
|
||||
import {
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CodeEdit',
|
||||
props: [
|
||||
'dialogVisible',
|
||||
'parameter',
|
||||
'value',
|
||||
],
|
||||
components: {
|
||||
PrismEditor,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
valueChanged (value: string) {
|
||||
this.$emit('valueChanged', value);
|
||||
},
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
closeDialog () {
|
||||
// Handle the close externally as the visible parameter is an external prop
|
||||
// and is so not allowed to be changed here.
|
||||
this.$emit('closeDialog');
|
||||
return false;
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
)
|
||||
.extend({
|
||||
name: 'CodeEdit',
|
||||
props: [
|
||||
'dialogVisible',
|
||||
'parameter',
|
||||
'value',
|
||||
],
|
||||
components: {
|
||||
PrismEditor,
|
||||
},
|
||||
},
|
||||
});
|
||||
data () {
|
||||
return {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
valueChanged (value: string) {
|
||||
this.$emit('valueChanged', value);
|
||||
},
|
||||
|
||||
closeDialog () {
|
||||
// Handle the close externally as the visible parameter is an external prop
|
||||
// and is so not allowed to be changed here.
|
||||
this.$emit('closeDialog');
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
<template slot-scope="scope">
|
||||
{{convertToDisplayDate(scope.row.startedAt)}}<br />
|
||||
<small v-if="scope.row.id">ID: {{scope.row.id}}</small>
|
||||
<small v-if="scope.row.idActive && scope.row.id === undefined && scope.row.stoppedAt === undefined">Active-ID: {{scope.row.idActive}}</small>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="workflowName" label="Name">
|
||||
|
@ -89,14 +88,17 @@
|
|||
<span class="status-badge success" v-else-if="scope.row.finished">
|
||||
Success
|
||||
</span>
|
||||
<span class="status-badge error" v-else>
|
||||
<span class="status-badge error" v-else-if="scope.row.stoppedAt !== null">
|
||||
Error
|
||||
</span>
|
||||
<span class="status-badge warning" v-else>
|
||||
Unknown
|
||||
</span>
|
||||
</el-tooltip>
|
||||
|
||||
<el-dropdown trigger="click" @command="handleRetryClick">
|
||||
<span class="el-dropdown-link">
|
||||
<el-button class="retry-button" circle v-if="scope.row.stoppedAt !== undefined && !scope.row.finished && scope.row.retryOf === undefined && scope.row.retrySuccessId === undefined" type="text" size="small" title="Retry execution">
|
||||
<el-button class="retry-button" v-bind:class="{ warning: scope.row.stoppedAt === null }" circle v-if="scope.row.stoppedAt !== undefined && !scope.row.finished && scope.row.retryOf === undefined && scope.row.retrySuccessId === undefined" type="text" size="small" title="Retry execution">
|
||||
<font-awesome-icon icon="redo" />
|
||||
</el-button>
|
||||
</span>
|
||||
|
@ -126,8 +128,8 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.stoppedAt === undefined && scope.row.idActive">
|
||||
<el-button circle title="Stop Execution" @click.stop="stopExecution(scope.row.idActive)" :loading="stoppingExecutions.includes(scope.row.idActive)" size="mini">
|
||||
<span v-if="scope.row.stoppedAt === undefined">
|
||||
<el-button circle title="Stop Execution" @click.stop="stopExecution(scope.row.id)" :loading="stoppingExecutions.includes(scope.row.id)" size="mini">
|
||||
<font-awesome-icon icon="stop" />
|
||||
</el-button>
|
||||
</span>
|
||||
|
@ -173,6 +175,10 @@ import {
|
|||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
range as _range,
|
||||
} from 'lodash';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(
|
||||
|
@ -411,14 +417,16 @@ export default mixins(
|
|||
this.$store.commit('setActiveExecutions', activeExecutions);
|
||||
},
|
||||
async loadAutoRefresh () : Promise<void> {
|
||||
let firstId: string | number | undefined = 0;
|
||||
if (this.finishedExecutions.length !== 0) {
|
||||
firstId = this.finishedExecutions[0].id;
|
||||
}
|
||||
const activeExecutionsPromise: Promise<IExecutionsListResponse> = this.restApi().getPastExecutions({}, 100, undefined, firstId);
|
||||
const filter = this.workflowFilterPast;
|
||||
// We cannot use firstId here as some executions finish out of order. Let's say
|
||||
// You have execution ids 500 to 505 running.
|
||||
// Suppose 504 finishes before 500, 501, 502 and 503.
|
||||
// iF you use firstId, filtering id >= 504 you won't
|
||||
// ever get ids 500, 501, 502 and 503 when they finish
|
||||
const pastExecutionsPromise: Promise<IExecutionsListResponse> = this.restApi().getPastExecutions(filter, 30);
|
||||
const currentExecutionsPromise: Promise<IExecutionsCurrentSummaryExtended[]> = this.restApi().getCurrentExecutions({});
|
||||
|
||||
const results = await Promise.all([activeExecutionsPromise, currentExecutionsPromise]);
|
||||
const results = await Promise.all([pastExecutionsPromise, currentExecutionsPromise]);
|
||||
|
||||
for (const activeExecution of results[1]) {
|
||||
if (activeExecution.workflowId !== undefined && activeExecution.workflowName === undefined) {
|
||||
|
@ -428,7 +436,55 @@ export default mixins(
|
|||
|
||||
this.$store.commit('setActiveExecutions', results[1]);
|
||||
|
||||
this.finishedExecutions.unshift.apply(this.finishedExecutions, results[0].results);
|
||||
const alreadyPresentExecutionIds = this.finishedExecutions.map(exec => exec.id);
|
||||
let lastId = 0;
|
||||
const gaps = [] as number[];
|
||||
for(let i = results[0].results.length - 1; i >= 0; i--) {
|
||||
const currentItem = results[0].results[i];
|
||||
const currentId = parseInt(currentItem.id, 10);
|
||||
if (lastId !== 0 && isNaN(currentId) === false) {
|
||||
// We are doing this iteration to detect possible gaps.
|
||||
// The gaps are used to remove executions that finished
|
||||
// and were deleted from database but were displaying
|
||||
// in this list while running.
|
||||
if (currentId - lastId > 1) {
|
||||
// We have some gaps.
|
||||
const range = _range(lastId + 1, currentId);
|
||||
gaps.push(...range);
|
||||
}
|
||||
}
|
||||
lastId = parseInt(currentItem.id, 10) || 0;
|
||||
|
||||
// Check new results from end to start
|
||||
// Add new items accordingly.
|
||||
const executionIndex = alreadyPresentExecutionIds.indexOf(currentItem.id);
|
||||
if (executionIndex !== -1) {
|
||||
// Execution that we received is already present.
|
||||
|
||||
if (this.finishedExecutions[executionIndex].finished === false && currentItem.finished === true) {
|
||||
// Concurrency stuff. This might happen if the execution finishes
|
||||
// prior to saving all information to database. Somewhat rare but
|
||||
// With auto refresh and several executions, it happens sometimes.
|
||||
// So we replace the execution data so it displays correctly.
|
||||
this.finishedExecutions[executionIndex] = currentItem;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the correct position to place this newcomer
|
||||
let j;
|
||||
for (j = this.finishedExecutions.length - 1; j >= 0; j--) {
|
||||
if (currentItem.id < this.finishedExecutions[j].id) {
|
||||
this.finishedExecutions.splice(j + 1, 0, currentItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j === -1) {
|
||||
this.finishedExecutions.unshift(currentItem);
|
||||
}
|
||||
}
|
||||
this.finishedExecutions = this.finishedExecutions.filter(execution => !gaps.includes(parseInt(execution.id, 10)) && lastId >= parseInt(execution.id, 10));
|
||||
this.finishedExecutionsCount = results[0].count;
|
||||
},
|
||||
async loadFinishedExecutions (): Promise<void> {
|
||||
|
@ -554,6 +610,8 @@ export default mixins(
|
|||
return `The workflow execution was a retry of "${entry.retryOf}" and failed.<br />New retries have to be started from the original execution.`;
|
||||
} else if (entry.retrySuccessId !== undefined) {
|
||||
return `The workflow execution failed but the retry "${entry.retrySuccessId}" was successful.`;
|
||||
} else if (entry.stoppedAt === null) {
|
||||
return 'The workflow execution is probably still running but it may have crashed and n8n cannot safely tell. ';
|
||||
} else {
|
||||
return 'The workflow execution failed.';
|
||||
}
|
||||
|
@ -610,6 +668,10 @@ export default mixins(
|
|||
color: $--custom-error-text;
|
||||
background-color: $--custom-error-background;
|
||||
margin-left: 5px;
|
||||
&.warning {
|
||||
background-color: $--custom-warning-background;
|
||||
color: $--custom-warning-text;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-options {
|
||||
|
@ -640,6 +702,11 @@ export default mixins(
|
|||
background-color: $--custom-success-background;
|
||||
color: $--custom-success-text;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: $--custom-warning-background;
|
||||
color: $--custom-warning-text;
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-name {
|
||||
|
|
|
@ -12,23 +12,22 @@ import 'quill/dist/quill.core.css';
|
|||
|
||||
import Quill, { DeltaOperation } from 'quill';
|
||||
// @ts-ignore
|
||||
import AutoFormat, { AutoformatHelperAttribute } from 'quill-autoformat';
|
||||
import AutoFormat from 'quill-autoformat';
|
||||
import {
|
||||
NodeParameterValue,
|
||||
Workflow,
|
||||
WorkflowDataProxy,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
IExecutionResponse,
|
||||
IVariableItemSelected,
|
||||
IVariableSelectorOption,
|
||||
} from '@/Interface';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
workflowHelpers,
|
||||
)
|
||||
.extend({
|
||||
|
@ -119,7 +118,7 @@ export default mixins(
|
|||
};
|
||||
|
||||
this.editor = new Quill(this.$refs['expression-editor'] as Element, {
|
||||
readOnly: !!this.resolvedValue,
|
||||
readOnly: !!this.resolvedValue || this.isReadOnly,
|
||||
modules: {
|
||||
autoformat: {},
|
||||
keyboard: {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<prism-editor v-if="!codeEditDialogVisible" :lineNumbers="true" :readonly="true" :code="displayValue" language="js"></prism-editor>
|
||||
</div>
|
||||
|
||||
<el-input v-else v-model="tempValue" ref="inputField" size="small" :type="getStringInputType" :rows="getArgument('rows')" :value="displayValue" :disabled="isReadOnly" @change="valueChanged" @keydown.stop @focus="setFocus" :title="displayTitle" :placeholder="isValueExpression?'':parameter.placeholder">
|
||||
<el-input v-else v-model="tempValue" ref="inputField" size="small" :type="getStringInputType" :rows="getArgument('rows')" :value="displayValue" :disabled="!isValueExpression && isReadOnly" @change="valueChanged" @keydown.stop @focus="setFocus" :title="displayTitle" :placeholder="isValueExpression?'':parameter.placeholder">
|
||||
<font-awesome-icon v-if="!isValueExpression && !isReadOnly" slot="suffix" icon="external-link-alt" class="edit-window-button clickable" title="Open Edit Window" @click="displayEditDialog()" />
|
||||
</el-input>
|
||||
</div>
|
||||
|
@ -523,9 +523,6 @@ export default mixins(
|
|||
this.valueChanged(value);
|
||||
},
|
||||
setFocus () {
|
||||
if (this.isReadOnly === true) {
|
||||
return;
|
||||
}
|
||||
if (this.isValueExpression) {
|
||||
this.expressionEditDialogVisible = true;
|
||||
return;
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
<div class="header">
|
||||
<div class="title-text">
|
||||
<strong v-if="dataCount < maxDisplayItems">
|
||||
Results: {{ dataCount }}
|
||||
Items: {{ dataCount }}
|
||||
</strong>
|
||||
<strong v-else>Results:
|
||||
<strong v-else>Items:
|
||||
<el-select v-model="maxDisplayItems" @click.stop>
|
||||
<el-option v-for="option in maxDisplayItemsOptions" :label="option" :value="option" :key="option" />
|
||||
</el-select> /
|
||||
|
@ -157,6 +157,10 @@
|
|||
<div class="label">File Name: </div>
|
||||
<div class="value">{{binaryData.fileName}}</div>
|
||||
</div>
|
||||
<div v-if="binaryData.directory">
|
||||
<div class="label">Directory: </div>
|
||||
<div class="value">{{binaryData.directory}}</div>
|
||||
</div>
|
||||
<div v-if="binaryData.fileExtension">
|
||||
<div class="label">File Extension:</div>
|
||||
<div class="value">{{binaryData.fileExtension}}</div>
|
||||
|
|
|
@ -114,6 +114,9 @@ export default mixins(
|
|||
// Has still options left so return
|
||||
inputData.options = this.sortOptions(newOptions);
|
||||
return inputData;
|
||||
} else if (Array.isArray(newOptions) && newOptions.length === 0) {
|
||||
delete inputData.options;
|
||||
return inputData;
|
||||
}
|
||||
// Has no options left so remove
|
||||
return null;
|
||||
|
|
|
@ -23,14 +23,15 @@ export const mouseSelect = mixins(
|
|||
this.selectBox.id = 'select-box';
|
||||
this.selectBox.style.margin = '0px auto';
|
||||
this.selectBox.style.border = '2px dotted #FF0000';
|
||||
this.selectBox.style.position = 'fixed';
|
||||
// Positioned absolutely within #node-view. This is consistent with how nodes are positioned.
|
||||
this.selectBox.style.position = 'absolute';
|
||||
this.selectBox.style.zIndex = '100';
|
||||
this.selectBox.style.visibility = 'hidden';
|
||||
|
||||
this.selectBox.addEventListener('mouseup', this.mouseUpMouseSelect);
|
||||
|
||||
// document.body.appendChild(this.selectBox);
|
||||
this.$el.appendChild(this.selectBox);
|
||||
const nodeViewEl = this.$el.querySelector('#node-view') as HTMLDivElement;
|
||||
nodeViewEl.appendChild(this.selectBox);
|
||||
},
|
||||
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
|
||||
if (this.isTouchDevice === true) {
|
||||
|
@ -41,14 +42,28 @@ export const mouseSelect = mixins(
|
|||
}
|
||||
return e.ctrlKey;
|
||||
},
|
||||
/**
|
||||
* Gets mouse position within the node view. Both node view offset and scale (zoom) are considered when
|
||||
* calculating position.
|
||||
*
|
||||
* @param event - mouse event within node view
|
||||
*/
|
||||
getMousePositionWithinNodeView (event: MouseEvent) {
|
||||
// @ts-ignore
|
||||
const nodeViewScale = this.nodeViewScale;
|
||||
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
|
||||
return {
|
||||
x: (event.pageX - offsetPosition[0]) / nodeViewScale,
|
||||
y: (event.pageY - offsetPosition[1]) / nodeViewScale,
|
||||
};
|
||||
},
|
||||
showSelectBox (event: MouseEvent) {
|
||||
// @ts-ignore
|
||||
this.selectBox.x = event.pageX;
|
||||
// @ts-ignore
|
||||
this.selectBox.y = event.pageY;
|
||||
this.selectBox = Object.assign(this.selectBox, this.getMousePositionWithinNodeView(event));
|
||||
|
||||
this.selectBox.style.left = event.pageX + 'px';
|
||||
this.selectBox.style.top = event.pageY + 'px';
|
||||
// @ts-ignore
|
||||
this.selectBox.style.left = this.selectBox.x + 'px';
|
||||
// @ts-ignore
|
||||
this.selectBox.style.top = this.selectBox.y + 'px';
|
||||
this.selectBox.style.visibility = 'visible';
|
||||
|
||||
this.selectActive = true;
|
||||
|
@ -75,25 +90,21 @@ export const mouseSelect = mixins(
|
|||
this.selectActive = false;
|
||||
},
|
||||
getSelectionBox (event: MouseEvent) {
|
||||
const {x, y} = this.getMousePositionWithinNodeView(event);
|
||||
return {
|
||||
// @ts-ignore
|
||||
x: Math.min(event.pageX, this.selectBox.x),
|
||||
x: Math.min(x, this.selectBox.x),
|
||||
// @ts-ignore
|
||||
y: Math.min(event.pageY, this.selectBox.y),
|
||||
y: Math.min(y, this.selectBox.y),
|
||||
// @ts-ignore
|
||||
width: Math.abs(event.pageX - this.selectBox.x),
|
||||
width: Math.abs(x - this.selectBox.x),
|
||||
// @ts-ignore
|
||||
height: Math.abs(event.pageY - this.selectBox.y),
|
||||
height: Math.abs(y - this.selectBox.y),
|
||||
};
|
||||
},
|
||||
getNodesInSelection (event: MouseEvent): INodeUi[] {
|
||||
const returnNodes: INodeUi[] = [];
|
||||
const selectionBox = this.getSelectionBox(event);
|
||||
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
|
||||
|
||||
// Consider the offset of the workflow when it got moved
|
||||
selectionBox.x -= offsetPosition[0];
|
||||
selectionBox.y -= offsetPosition[1];
|
||||
|
||||
// Go through all nodes and check if they are selected
|
||||
this.$store.getters.allNodes.forEach((node: INodeUi) => {
|
||||
|
|
|
@ -191,7 +191,7 @@ export const pushConnection = mixins(
|
|||
return false;
|
||||
}
|
||||
|
||||
if (this.$store.getters.activeExecutionId !== pushData.executionIdActive) {
|
||||
if (this.$store.getters.activeExecutionId !== pushData.executionId) {
|
||||
// The workflow which did finish execution did either not get started
|
||||
// by this session or we do not have the execution id yet.
|
||||
if (isRetry !== true) {
|
||||
|
@ -242,7 +242,7 @@ export const pushConnection = mixins(
|
|||
const pushData = receivedData.data as IPushDataExecutionStarted;
|
||||
|
||||
const executionData: IExecutionsCurrentSummaryExtended = {
|
||||
idActive: pushData.executionId,
|
||||
id: pushData.executionId,
|
||||
finished: false,
|
||||
mode: pushData.mode,
|
||||
startedAt: pushData.startedAt,
|
||||
|
|
|
@ -22,6 +22,8 @@ $--custom-running-background : #ffffe5;
|
|||
$--custom-running-text : #eb9422;
|
||||
$--custom-success-background : #e3f0e4;
|
||||
$--custom-success-text : #40c351;
|
||||
$--custom-warning-background : #ffffe5;
|
||||
$--custom-warning-text : #eb9422;
|
||||
|
||||
$--custom-node-view-background : #faf9fe;
|
||||
|
||||
|
|
|
@ -99,7 +99,7 @@ export const store = new Vuex.Store({
|
|||
addActiveExecution (state, newActiveExecution: IExecutionsCurrentSummaryExtended) {
|
||||
// Check if the execution exists already
|
||||
const activeExecution = state.activeExecutions.find(execution => {
|
||||
return execution.idActive === newActiveExecution.idActive;
|
||||
return execution.id === newActiveExecution.id;
|
||||
});
|
||||
|
||||
if (activeExecution !== undefined) {
|
||||
|
@ -115,7 +115,7 @@ export const store = new Vuex.Store({
|
|||
finishActiveExecution (state, finishedActiveExecution: IPushDataExecutionFinished) {
|
||||
// Find the execution to set to finished
|
||||
const activeExecution = state.activeExecutions.find(execution => {
|
||||
return execution.idActive === finishedActiveExecution.executionIdActive;
|
||||
return execution.id === finishedActiveExecution.executionId;
|
||||
});
|
||||
|
||||
if (activeExecution === undefined) {
|
||||
|
@ -123,8 +123,8 @@ export const store = new Vuex.Store({
|
|||
return;
|
||||
}
|
||||
|
||||
if (finishedActiveExecution.executionIdDb !== undefined) {
|
||||
Vue.set(activeExecution, 'id', finishedActiveExecution.executionIdDb);
|
||||
if (finishedActiveExecution.executionId !== undefined) {
|
||||
Vue.set(activeExecution, 'id', finishedActiveExecution.executionId);
|
||||
}
|
||||
|
||||
Vue.set(activeExecution, 'finished', finishedActiveExecution.data.finished);
|
||||
|
|
|
@ -742,8 +742,7 @@ export default mixins(
|
|||
} as IRun;
|
||||
const pushData = {
|
||||
data: executedData,
|
||||
executionIdActive: executionId,
|
||||
executionIdDb: executionId,
|
||||
executionId,
|
||||
retryOf: execution.retryOf,
|
||||
} as IPushDataExecutionFinished;
|
||||
this.$store.commit('finishActiveExecution', pushData);
|
||||
|
@ -759,8 +758,6 @@ export default mixins(
|
|||
} else {
|
||||
this.$showError(error, 'Problem stopping execution', 'There was a problem stopping the execuction:');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
this.stopExecutionInProgress = false;
|
||||
},
|
||||
|
@ -964,7 +961,7 @@ export default mixins(
|
|||
return originalName;
|
||||
}
|
||||
|
||||
const nameMatch = originalName.match(/(.*[a-zA-Z])(\d*)/);
|
||||
const nameMatch = originalName.match(/(.*\D+)(\d*)/);
|
||||
let ignore, baseName, nameIndex, uniqueName;
|
||||
let index = 1;
|
||||
|
||||
|
@ -2142,6 +2139,7 @@ export default mixins(
|
|||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.node-view-background {
|
||||
|
|
|
@ -1,70 +1,70 @@
|
|||
{
|
||||
"name": "n8n-node-dev",
|
||||
"version": "0.11.0",
|
||||
"description": "CLI to simplify n8n credentials/node development",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"main": "dist/src/index",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"oclif": {
|
||||
"commands": "./dist/commands",
|
||||
"bin": "n8n-node-dev"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run watch",
|
||||
"build": "tsc",
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
|
||||
"tslint": "tslint -p tsconfig.json -c tslint.json",
|
||||
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"bin": {
|
||||
"n8n-node-dev": "./bin/n8n-node-dev"
|
||||
},
|
||||
"keywords": [
|
||||
"development",
|
||||
"node",
|
||||
"helper",
|
||||
"n8n"
|
||||
],
|
||||
"files": [
|
||||
"bin",
|
||||
"dist",
|
||||
"templates",
|
||||
"oclif.manifest.json",
|
||||
"src/tsconfig-build.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@oclif/dev-cli": "^1.22.2",
|
||||
"@types/copyfiles": "^2.1.1",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/tmp": "^0.1.0",
|
||||
"@types/vorpal": "^1.11.0",
|
||||
"tslint": "^6.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/command": "^1.5.18",
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/node": "14.0.27",
|
||||
"change-case": "^4.1.1",
|
||||
"copyfiles": "^2.1.1",
|
||||
"inquirer": "^7.0.1",
|
||||
"n8n-core": "^0.48.0",
|
||||
"n8n-workflow": "^0.42.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"replace-in-file": "^6.0.0",
|
||||
"request": "^2.88.2",
|
||||
"tmp-promise": "^2.0.2",
|
||||
"typescript": "~3.9.7"
|
||||
}
|
||||
"name": "n8n-node-dev",
|
||||
"version": "0.11.0",
|
||||
"description": "CLI to simplify n8n credentials/node development",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"main": "dist/src/index",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"oclif": {
|
||||
"commands": "./dist/commands",
|
||||
"bin": "n8n-node-dev"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run watch",
|
||||
"build": "tsc",
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
|
||||
"tslint": "tslint -p tsconfig.json -c tslint.json",
|
||||
"tslintfix": "tslint --fix -p tsconfig.json -c tslint.json",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"bin": {
|
||||
"n8n-node-dev": "./bin/n8n-node-dev"
|
||||
},
|
||||
"keywords": [
|
||||
"development",
|
||||
"node",
|
||||
"helper",
|
||||
"n8n"
|
||||
],
|
||||
"files": [
|
||||
"bin",
|
||||
"dist",
|
||||
"templates",
|
||||
"oclif.manifest.json",
|
||||
"src/tsconfig-build.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@oclif/dev-cli": "^1.22.2",
|
||||
"@types/copyfiles": "^2.1.1",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/tmp": "^0.1.0",
|
||||
"@types/vorpal": "^1.11.0",
|
||||
"tslint": "^6.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/command": "^1.5.18",
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/node": "14.0.27",
|
||||
"change-case": "^4.1.1",
|
||||
"copyfiles": "^2.1.1",
|
||||
"inquirer": "^7.0.1",
|
||||
"n8n-core": "^0.48.0",
|
||||
"n8n-workflow": "^0.42.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"replace-in-file": "^6.0.0",
|
||||
"request": "^2.88.2",
|
||||
"tmp-promise": "^2.0.2",
|
||||
"typescript": "~3.9.7"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class ApiTemplateIoApi implements ICredentialType {
|
||||
name = 'apiTemplateIoApi';
|
||||
displayName = 'APITemplate.io API';
|
||||
documentationUrl = 'apiTemplateIo';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
18
packages/nodes-base/credentials/AutopilotApi.credentials.ts
Normal file
18
packages/nodes-base/credentials/AutopilotApi.credentials.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class AutopilotApi implements ICredentialType {
|
||||
name = 'autopilotApi';
|
||||
displayName = 'Autopilot API';
|
||||
documentationUrl = 'autopilot';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -96,6 +96,21 @@ export class Aws implements ICredentialType {
|
|||
default: '',
|
||||
placeholder: 'https://email.{region}.amazonaws.com',
|
||||
},
|
||||
{
|
||||
displayName: 'SQS Endpoint',
|
||||
name: 'sqsEndpoint',
|
||||
description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and SQS using a VPC endpoint. Leave blank to use the default endpoint.',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
displayOptions: {
|
||||
show: {
|
||||
customEndpoints: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'https://sqs.{region}.amazonaws.com',
|
||||
},
|
||||
{
|
||||
displayName: 'S3 Endpoint',
|
||||
name: 's3Endpoint',
|
||||
|
|
56
packages/nodes-base/credentials/BitwardenApi.credentials.ts
Normal file
56
packages/nodes-base/credentials/BitwardenApi.credentials.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
// https://bitwarden.com/help/article/public-api/#authentication
|
||||
|
||||
export class BitwardenApi implements ICredentialType {
|
||||
name = 'bitwardenApi';
|
||||
displayName = 'Bitwarden API';
|
||||
documentationUrl = 'bitwarden';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Client ID',
|
||||
name: 'clientId',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Client Secret',
|
||||
name: 'clientSecret',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Environment',
|
||||
name: 'environment',
|
||||
type: 'options' as NodePropertyTypes,
|
||||
default: 'cloudHosted',
|
||||
options: [
|
||||
{
|
||||
name: 'Cloud-hosted',
|
||||
value: 'cloudHosted',
|
||||
},
|
||||
{
|
||||
name: 'Self-hosted',
|
||||
value: 'selfHosted',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Self-hosted domain',
|
||||
name: 'domain',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
placeholder: 'https://www.mydomain.com',
|
||||
displayOptions: {
|
||||
show: {
|
||||
environment: [
|
||||
'selfHosted',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
70
packages/nodes-base/credentials/BubbleApi.credentials.ts
Normal file
70
packages/nodes-base/credentials/BubbleApi.credentials.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class BubbleApi implements ICredentialType {
|
||||
name = 'bubbleApi';
|
||||
displayName = 'Bubble API';
|
||||
documentationUrl = 'bubble';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Token',
|
||||
name: 'apiToken',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'App Name',
|
||||
name: 'appName',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Environment',
|
||||
name: 'environment',
|
||||
type: 'options' as NodePropertyTypes,
|
||||
default: 'live',
|
||||
options: [
|
||||
{
|
||||
name: 'Development',
|
||||
value: 'development',
|
||||
},
|
||||
{
|
||||
name: 'Live',
|
||||
value: 'live',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Hosting',
|
||||
name: 'hosting',
|
||||
type: 'options' as NodePropertyTypes,
|
||||
default: 'bubbleHosted',
|
||||
options: [
|
||||
{
|
||||
name: 'Bubble-hosted',
|
||||
value: 'bubbleHosted',
|
||||
},
|
||||
{
|
||||
name: 'Self-hosted',
|
||||
value: 'selfHosted',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Domain',
|
||||
name: 'domain',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
placeholder: 'mydomain.com',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
hosting: [
|
||||
'selfHosted',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
15
packages/nodes-base/credentials/DeepLApi.credentials.ts
Normal file
15
packages/nodes-base/credentials/DeepLApi.credentials.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { ICredentialType, NodePropertyTypes } from 'n8n-workflow';
|
||||
|
||||
export class DeepLApi implements ICredentialType {
|
||||
name = 'deepLApi';
|
||||
displayName = 'DeepL API';
|
||||
documentationUrl = 'deepL';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
24
packages/nodes-base/credentials/DemioApi.credentials.ts
Normal file
24
packages/nodes-base/credentials/DemioApi.credentials.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class DemioApi implements ICredentialType {
|
||||
name = 'demioApi';
|
||||
displayName = 'Demio API';
|
||||
documentationUrl = 'demio';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'API Secret',
|
||||
name: 'apiSecret',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -14,5 +14,21 @@ export class DropboxApi implements ICredentialType {
|
|||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'APP Access Type',
|
||||
name: 'accessType',
|
||||
type: 'options' as NodePropertyTypes,
|
||||
options: [
|
||||
{
|
||||
name: 'App Folder',
|
||||
value: 'folder',
|
||||
},
|
||||
{
|
||||
name: 'Full Dropbox',
|
||||
value: 'full',
|
||||
},
|
||||
],
|
||||
default: 'full',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ const scopes = [
|
|||
'files.content.write',
|
||||
'files.content.read',
|
||||
'sharing.read',
|
||||
'account_info.read',
|
||||
];
|
||||
|
||||
export class DropboxOAuth2Api implements ICredentialType {
|
||||
|
@ -41,7 +42,7 @@ export class DropboxOAuth2Api implements ICredentialType {
|
|||
displayName: 'Auth URI Query Parameters',
|
||||
name: 'authQueryParameters',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'token_access_type=offline',
|
||||
default: 'token_access_type=offline&force_reapprove=true',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
|
@ -49,5 +50,21 @@ export class DropboxOAuth2Api implements ICredentialType {
|
|||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'header',
|
||||
},
|
||||
{
|
||||
displayName: 'APP Access Type',
|
||||
name: 'accessType',
|
||||
type: 'options' as NodePropertyTypes,
|
||||
options: [
|
||||
{
|
||||
name: 'App Folder',
|
||||
value: 'folder',
|
||||
},
|
||||
{
|
||||
name: 'Full Dropbox',
|
||||
value: 'full',
|
||||
},
|
||||
],
|
||||
default: 'full',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
32
packages/nodes-base/credentials/ERPNextApi.credentials.ts
Normal file
32
packages/nodes-base/credentials/ERPNextApi.credentials.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class ERPNextApi implements ICredentialType {
|
||||
name = 'erpNextApi';
|
||||
displayName = 'ERPNext API';
|
||||
documentationUrl = 'erpnext';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'API Secret',
|
||||
name: 'apiSecret',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Subdomain',
|
||||
name: 'subdomain',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
placeholder: 'n8n',
|
||||
description: 'ERPNext subdomain. For instance, entering n8n will make the url look like: https://n8n.erpnext.com/.',
|
||||
},
|
||||
];
|
||||
}
|
18
packages/nodes-base/credentials/EmeliaApi.credentials.ts
Normal file
18
packages/nodes-base/credentials/EmeliaApi.credentials.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class EmeliaApi implements ICredentialType {
|
||||
name = 'emeliaApi';
|
||||
displayName = 'Emelia API';
|
||||
documentationUrl = 'emelia';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class GoToWebinarOAuth2Api implements ICredentialType {
|
||||
name = 'goToWebinarOAuth2Api';
|
||||
extends = [
|
||||
'oAuth2Api',
|
||||
];
|
||||
displayName = 'GoToWebinar OAuth2 API';
|
||||
documentationUrl = 'goToWebinar';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Authorization URL',
|
||||
name: 'authUrl',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'https://api.getgo.com/oauth/v2/authorize',
|
||||
},
|
||||
{
|
||||
displayName: 'Access Token URL',
|
||||
name: 'accessTokenUrl',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'https://api.getgo.com/oauth/v2/token',
|
||||
},
|
||||
{
|
||||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: '', // set when creating the OAuth client
|
||||
},
|
||||
{
|
||||
displayName: 'Auth URI Query Parameters',
|
||||
name: 'authQueryParameters',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'header',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
const scopes = [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/presentations',
|
||||
];
|
||||
|
||||
export class GoogleSlidesOAuth2Api implements ICredentialType {
|
||||
name = 'googleSlidesOAuth2Api';
|
||||
extends = [
|
||||
'googleOAuth2Api',
|
||||
];
|
||||
displayName = 'Google Slides OAuth2 API';
|
||||
documentationUrl = 'google';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: scopes.join(' '),
|
||||
},
|
||||
];
|
||||
}
|
|
@ -28,10 +28,23 @@ export class Kafka implements ICredentialType {
|
|||
type: 'boolean' as NodePropertyTypes,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'boolean' as NodePropertyTypes,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
displayName: 'Username',
|
||||
name: 'username',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Optional username if authenticated is required.',
|
||||
},
|
||||
|
@ -39,11 +52,46 @@ export class Kafka implements ICredentialType {
|
|||
displayName: 'Password',
|
||||
name: 'password',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Optional password if authenticated is required.',
|
||||
},
|
||||
{
|
||||
displayName: 'SASL mechanism',
|
||||
name: 'saslMechanism',
|
||||
type: 'options' as NodePropertyTypes,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'plain',
|
||||
value: 'plain',
|
||||
},
|
||||
{
|
||||
name: 'scram-sha-256',
|
||||
value: 'scram-sha-256',
|
||||
},
|
||||
{
|
||||
name: 'scram-sha-512',
|
||||
value: 'scram-sha-512',
|
||||
},
|
||||
],
|
||||
default: 'plain',
|
||||
description: 'The SASL mechanism.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
18
packages/nodes-base/credentials/LemlistApi.credentials.ts
Normal file
18
packages/nodes-base/credentials/LemlistApi.credentials.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class LemlistApi implements ICredentialType {
|
||||
name = 'lemlistApi';
|
||||
displayName = 'Lemlist API';
|
||||
documentationUrl = 'lemlist';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -50,5 +50,12 @@ export class MicrosoftSql implements ICredentialType {
|
|||
type: 'boolean' as NodePropertyTypes,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Connect Timeout',
|
||||
name: 'connectTimeout',
|
||||
type: 'number' as NodePropertyTypes,
|
||||
default: 15000,
|
||||
description: 'Connection timeout in ms.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
18
packages/nodes-base/credentials/OuraApi.credentials.ts
Normal file
18
packages/nodes-base/credentials/OuraApi.credentials.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class OuraApi implements ICredentialType {
|
||||
name = 'ouraApi';
|
||||
displayName = 'Oura API';
|
||||
documentationUrl = 'oura';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Personal Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
24
packages/nodes-base/credentials/PlivoApi.credentials.ts
Normal file
24
packages/nodes-base/credentials/PlivoApi.credentials.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class PlivoApi implements ICredentialType {
|
||||
name = 'plivoApi';
|
||||
displayName = 'Plivo API';
|
||||
documentationUrl = 'plivo';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Auth ID',
|
||||
name: 'authId',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Auth Token',
|
||||
name: 'authToken',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
23
packages/nodes-base/credentials/PostHogApi.credentials.ts
Normal file
23
packages/nodes-base/credentials/PostHogApi.credentials.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class PostHogApi implements ICredentialType {
|
||||
name = 'postHogApi';
|
||||
displayName = 'PostHog API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'URL',
|
||||
name: 'url',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: 'https://app.posthog.com',
|
||||
},
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
const scopes = [
|
||||
'com.intuit.quickbooks.accounting',
|
||||
'com.intuit.quickbooks.payment',
|
||||
];
|
||||
|
||||
// https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization
|
||||
|
||||
export class QuickBooksOAuth2Api implements ICredentialType {
|
||||
name = 'quickBooksOAuth2Api';
|
||||
extends = [
|
||||
'oAuth2Api',
|
||||
];
|
||||
displayName = 'QuickBooks OAuth2 API';
|
||||
documentationUrl = 'quickbooks';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Authorization URL',
|
||||
name: 'authUrl',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'https://appcenter.intuit.com/connect/oauth2',
|
||||
},
|
||||
{
|
||||
displayName: 'Access Token URL',
|
||||
name: 'accessTokenUrl',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer',
|
||||
},
|
||||
{
|
||||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: scopes.join(' '),
|
||||
},
|
||||
{
|
||||
displayName: 'Auth URI Query Parameters',
|
||||
name: 'authQueryParameters',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'header',
|
||||
},
|
||||
{
|
||||
displayName: 'Environment',
|
||||
name: 'environment',
|
||||
type: 'options' as NodePropertyTypes,
|
||||
default: 'production',
|
||||
options: [
|
||||
{
|
||||
name: 'Production',
|
||||
value: 'production',
|
||||
},
|
||||
{
|
||||
name: 'Sandbox',
|
||||
value: 'sandbox',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
// https://developer.raindrop.io/v1/authentication
|
||||
|
||||
export class RaindropOAuth2Api implements ICredentialType {
|
||||
name = 'raindropOAuth2Api';
|
||||
extends = [
|
||||
'oAuth2Api',
|
||||
];
|
||||
displayName = 'Raindrop OAuth2 API';
|
||||
documentationUrl = 'raindrop';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Authorization URL',
|
||||
name: 'authUrl',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'https://raindrop.io/oauth/authorize',
|
||||
},
|
||||
{
|
||||
displayName: 'Access Token URL',
|
||||
name: 'accessTokenUrl',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'https://raindrop.io/oauth/access_token',
|
||||
},
|
||||
{
|
||||
displayName: 'Auth URI Query Parameters',
|
||||
name: 'authQueryParameters',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'body',
|
||||
},
|
||||
{
|
||||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -36,7 +36,7 @@ export class SpotifyOAuth2Api implements ICredentialType {
|
|||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'user-read-playback-state playlist-read-collaborative user-modify-playback-state playlist-modify-public user-read-currently-playing playlist-read-private user-read-recently-played playlist-modify-private',
|
||||
default: 'user-read-playback-state playlist-read-collaborative user-modify-playback-state playlist-modify-public user-read-currently-playing playlist-read-private user-read-recently-played playlist-modify-private user-library-read',
|
||||
},
|
||||
{
|
||||
displayName: 'Auth URI Query Parameters',
|
||||
|
|
|
@ -41,5 +41,11 @@ export class TheHiveApi implements ICredentialType {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Ignore SSL Issues',
|
||||
name: 'allowUnauthorizedCerts',
|
||||
type: 'boolean' as NodePropertyTypes,
|
||||
default: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
34
packages/nodes-base/credentials/WiseApi.credentials.ts
Normal file
34
packages/nodes-base/credentials/WiseApi.credentials.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class WiseApi implements ICredentialType {
|
||||
name = 'wiseApi';
|
||||
displayName = 'Wise API';
|
||||
documentationUrl = 'wise';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Token',
|
||||
name: 'apiToken',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Environment',
|
||||
name: 'environment',
|
||||
type: 'options' as NodePropertyTypes,
|
||||
default: 'live',
|
||||
options: [
|
||||
{
|
||||
name: 'Live',
|
||||
value: 'live',
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
value: 'test',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
73
packages/nodes-base/nodes/ActivationTrigger.node.ts
Normal file
73
packages/nodes-base/nodes/ActivationTrigger.node.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { ITriggerFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
ITriggerResponse,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class ActivationTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Activation Trigger',
|
||||
name: 'activationTrigger',
|
||||
icon: 'fa:play-circle',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Executes whenever the workflow becomes active.',
|
||||
defaults: {
|
||||
name: 'Activation Trigger',
|
||||
color: '#00e000',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Events',
|
||||
name: 'events',
|
||||
type: 'multiOptions',
|
||||
required: true,
|
||||
default: [],
|
||||
description: 'Specifies under which conditions an execution should happen:<br />' +
|
||||
'- <b>Activation</b>: Workflow gets activated<br />' +
|
||||
'- <b>Update</b>: Workflow gets saved while active<br>' +
|
||||
'- <b>Start</b>: n8n starts or restarts',
|
||||
options: [
|
||||
{
|
||||
name: 'Activation',
|
||||
value: 'activate',
|
||||
description: 'Run when workflow gets activated',
|
||||
},
|
||||
{
|
||||
name: 'Start',
|
||||
value: 'init',
|
||||
description: 'Run when n8n starts or restarts',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Run when workflow gets saved while it is active',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||
const events = this.getNodeParameter('events', []) as string[];
|
||||
|
||||
const activationMode = this.getActivationMode();
|
||||
|
||||
if (events.includes(activationMode)) {
|
||||
this.emit([this.helpers.returnJsonArray([{ activation: activationMode }])]);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
async function manualTriggerFunction() {
|
||||
self.emit([self.helpers.returnJsonArray([{ activation: 'manual' }])]);
|
||||
}
|
||||
|
||||
return {
|
||||
manualTriggerFunction,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -137,7 +137,7 @@ export class Airtable implements INodeType {
|
|||
// delete
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Id',
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
|
@ -317,7 +317,7 @@ export class Airtable implements INodeType {
|
|||
// read
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Id',
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
|
@ -336,7 +336,7 @@ export class Airtable implements INodeType {
|
|||
// update
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Id',
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
|
@ -499,7 +499,7 @@ export class Airtable implements INodeType {
|
|||
for (let i = 0; i < items.length; i++) {
|
||||
id = this.getNodeParameter('id', i) as string;
|
||||
|
||||
endpoint = `${application}/${table}/${id}`;
|
||||
endpoint = `${application}/${table}`;
|
||||
|
||||
// Make one request after another. This is slower but makes
|
||||
// sure that we do not run into the rate limit they have in
|
||||
|
@ -507,9 +507,11 @@ export class Airtable implements INodeType {
|
|||
// functionality in core should make it easy to make requests
|
||||
// according to specific rules like not more than 5 requests
|
||||
// per seconds.
|
||||
qs.records = [id];
|
||||
|
||||
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
returnData.push(responseData);
|
||||
returnData.push(...responseData.records);
|
||||
}
|
||||
|
||||
} else if (operation === 'list') {
|
||||
|
@ -586,7 +588,6 @@ export class Airtable implements INodeType {
|
|||
let updateAllFields: boolean;
|
||||
let fields: string[];
|
||||
let options: IDataObject;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
updateAllFields = this.getNodeParameter('updateAllFields', i) as boolean;
|
||||
options = this.getNodeParameter('options', i, {}) as IDataObject;
|
||||
|
@ -616,13 +617,9 @@ export class Airtable implements INodeType {
|
|||
}
|
||||
}
|
||||
|
||||
if (options.typecast === true) {
|
||||
body['typecast'] = true;
|
||||
}
|
||||
|
||||
id = this.getNodeParameter('id', i) as string;
|
||||
|
||||
endpoint = `${application}/${table}/${id}`;
|
||||
endpoint = `${application}/${table}`;
|
||||
|
||||
// Make one request after another. This is slower but makes
|
||||
// sure that we do not run into the rate limit they have in
|
||||
|
@ -631,9 +628,11 @@ export class Airtable implements INodeType {
|
|||
// according to specific rules like not more than 5 requests
|
||||
// per seconds.
|
||||
|
||||
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
const data = { records: [{ id, fields: body.fields }], typecast: (options.typecast) ? true : false };
|
||||
|
||||
returnData.push(responseData);
|
||||
responseData = await apiRequest.call(this, requestMethod, endpoint, data, qs);
|
||||
|
||||
returnData.push(...responseData.records);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
|
|
@ -58,6 +58,7 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa
|
|||
body,
|
||||
qs: query,
|
||||
uri: uri || `https://api.airtable.com/v0/${endpoint}`,
|
||||
useQuerystring: false,
|
||||
json: true,
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"node": "n8n-nodes-base.apiTemplateIo",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": [
|
||||
"Marketing & Content"
|
||||
],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/credentials/apiTemplateIo"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.apiTemplateIo/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
560
packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts
Normal file
560
packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts
Normal file
|
@ -0,0 +1,560 @@
|
|||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
apiTemplateIoApiRequest,
|
||||
downloadImage,
|
||||
loadResource,
|
||||
validateJSON,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class ApiTemplateIo implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'APITemplate.io',
|
||||
name: 'apiTemplateIo',
|
||||
icon: 'file:apiTemplateIo.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Consume the APITemplate.io API',
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
defaults: {
|
||||
name: 'APITemplate.io',
|
||||
color: '#c0c0c0',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'apiTemplateIoApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Account',
|
||||
value: 'account',
|
||||
},
|
||||
{
|
||||
name: 'Image',
|
||||
value: 'image',
|
||||
},
|
||||
{
|
||||
name: 'PDF',
|
||||
value: 'pdf',
|
||||
},
|
||||
],
|
||||
default: 'image',
|
||||
description: 'Resource to consume',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
default: 'create',
|
||||
required: true,
|
||||
description: 'Operation to perform',
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'image',
|
||||
'pdf',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
default: 'get',
|
||||
required: true,
|
||||
description: 'Operation to perform',
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'account',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Template ID',
|
||||
name: 'imageTemplateId',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the image template to use.',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getImageTemplates',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'image',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Template ID',
|
||||
name: 'pdfTemplateId',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the PDF template to use.',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getPdfTemplates',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'pdf',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'JSON Parameters',
|
||||
name: 'jsonParameters',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'pdf',
|
||||
'image',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Download',
|
||||
name: 'download',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'pdf',
|
||||
'image',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Name of the binary property to which to<br />write the data of the read file.',
|
||||
},
|
||||
{
|
||||
displayName: 'Binary Property',
|
||||
name: 'binaryProperty',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: 'data',
|
||||
description: 'Name of the binary property to which to write to.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'pdf',
|
||||
'image',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
download: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Overrides (JSON)',
|
||||
name: 'overridesJson',
|
||||
type: 'json',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'image',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
jsonParameters: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: `[ {"name": "text_1", "text": "hello world", "textBackgroundColor": "rgba(246, 243, 243, 0)" } ]`,
|
||||
},
|
||||
{
|
||||
displayName: 'Properties (JSON)',
|
||||
name: 'propertiesJson',
|
||||
type: 'json',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'pdf',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
jsonParameters: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: `{ "name": "text_1" }`,
|
||||
},
|
||||
{
|
||||
displayName: 'Overrides',
|
||||
name: 'overridesUi',
|
||||
placeholder: 'Add Override',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'image',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
jsonParameters: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'overrideValues',
|
||||
displayName: 'Override',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Properties',
|
||||
name: 'propertiesUi',
|
||||
placeholder: 'Add Property',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'propertyValues',
|
||||
displayName: 'Property',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the property',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value to the property.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Properties',
|
||||
name: 'propertiesUi',
|
||||
placeholder: 'Add Property',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'pdf',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
jsonParameters: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'propertyValues',
|
||||
displayName: 'Property',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the property',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value to the property.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
resource: [
|
||||
'pdf',
|
||||
'image',
|
||||
],
|
||||
'download': [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'File Name',
|
||||
name: 'fileName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The name of the downloaded image/pdf. It has to include the extension. For example: report.pdf',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
async getImageTemplates(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await loadResource.call(this, 'image');
|
||||
},
|
||||
|
||||
async getPdfTemplates(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await loadResource.call(this, 'pdf');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
const length = items.length;
|
||||
|
||||
let responseData;
|
||||
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
if (resource === 'account') {
|
||||
|
||||
// *********************************************************************
|
||||
// account
|
||||
// *********************************************************************
|
||||
|
||||
if (operation === 'get') {
|
||||
|
||||
// ----------------------------------
|
||||
// account: get
|
||||
// ----------------------------------
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
|
||||
responseData = await apiTemplateIoApiRequest.call(this, 'GET', '/account-information');
|
||||
|
||||
returnData.push(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (resource === 'image') {
|
||||
|
||||
// *********************************************************************
|
||||
// image
|
||||
// *********************************************************************
|
||||
|
||||
if (operation === 'create') {
|
||||
// ----------------------------------
|
||||
// image: create
|
||||
// ----------------------------------
|
||||
|
||||
const download = this.getNodeParameter('download', 0) as boolean;
|
||||
|
||||
// https://docs.apitemplate.io/reference/api-reference.html#create-an-image-jpeg-and-png
|
||||
for (let i = 0; i < length; i++) {
|
||||
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
|
||||
|
||||
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||
|
||||
const qs = {
|
||||
template_id: this.getNodeParameter('imageTemplateId', i),
|
||||
};
|
||||
|
||||
const body = { overrides: [] } as IDataObject;
|
||||
|
||||
if (jsonParameters === false) {
|
||||
const overrides = (this.getNodeParameter('overridesUi', i) as IDataObject || {}).overrideValues as IDataObject[] || [];
|
||||
if (overrides.length !== 0) {
|
||||
const data: IDataObject[] = [];
|
||||
for (const override of overrides) {
|
||||
const properties = (override.propertiesUi as IDataObject || {}).propertyValues as IDataObject[] || [];
|
||||
data.push(properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}));
|
||||
}
|
||||
body.overrides = data;
|
||||
}
|
||||
} else {
|
||||
const overrideJson = this.getNodeParameter('overridesJson', i) as string;
|
||||
if (overrideJson !== '') {
|
||||
const data = validateJSON(overrideJson);
|
||||
if (data === undefined) {
|
||||
throw new Error('A valid JSON must be provided.');
|
||||
}
|
||||
body.overrides = data;
|
||||
}
|
||||
}
|
||||
|
||||
responseData = await apiTemplateIoApiRequest.call(this, 'POST', '/create', qs, body);
|
||||
|
||||
if (download === true) {
|
||||
const binaryProperty = this.getNodeParameter('binaryProperty', i) as string;
|
||||
const data = await downloadImage.call(this, responseData.download_url);
|
||||
const fileName = responseData.download_url.split('/').pop();
|
||||
const binaryData = await this.helpers.prepareBinaryData(data, options.fileName || fileName);
|
||||
responseData = {
|
||||
json: responseData,
|
||||
binary: {
|
||||
[binaryProperty]: binaryData,
|
||||
},
|
||||
};
|
||||
}
|
||||
returnData.push(responseData);
|
||||
}
|
||||
|
||||
if (download === true) {
|
||||
return this.prepareOutputData(returnData as unknown as INodeExecutionData[]);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (resource === 'pdf') {
|
||||
|
||||
// *********************************************************************
|
||||
// pdf
|
||||
// *********************************************************************
|
||||
|
||||
if (operation === 'create') {
|
||||
|
||||
// ----------------------------------
|
||||
// pdf: create
|
||||
// ----------------------------------
|
||||
|
||||
// https://docs.apitemplate.io/reference/api-reference.html#create-a-pdf
|
||||
const download = this.getNodeParameter('download', 0) as boolean;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
|
||||
|
||||
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||
|
||||
const qs = {
|
||||
template_id: this.getNodeParameter('pdfTemplateId', i),
|
||||
};
|
||||
|
||||
let data;
|
||||
|
||||
if (jsonParameters === false) {
|
||||
const properties = (this.getNodeParameter('propertiesUi', i) as IDataObject || {}).propertyValues as IDataObject[] || [];
|
||||
if (properties.length === 0) {
|
||||
throw new Error('The parameter properties cannot be empty');
|
||||
}
|
||||
data = properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {});
|
||||
} else {
|
||||
const propertiesJson = this.getNodeParameter('propertiesJson', i) as string;
|
||||
data = validateJSON(propertiesJson);
|
||||
if (data === undefined) {
|
||||
throw new Error('A valid JSON must be provided.');
|
||||
}
|
||||
}
|
||||
|
||||
responseData = await apiTemplateIoApiRequest.call(this, 'POST', '/create', qs, data);
|
||||
|
||||
if (download === true) {
|
||||
const binaryProperty = this.getNodeParameter('binaryProperty', i) as string;
|
||||
const data = await downloadImage.call(this, responseData.download_url);
|
||||
const fileName = responseData.download_url.split('/').pop();
|
||||
const binaryData = await this.helpers.prepareBinaryData(data, options.fileName || fileName);
|
||||
responseData = {
|
||||
json: responseData,
|
||||
binary: {
|
||||
[binaryProperty]: binaryData,
|
||||
},
|
||||
};
|
||||
}
|
||||
returnData.push(responseData);
|
||||
}
|
||||
if (download === true) {
|
||||
return this.prepareOutputData(returnData as unknown as INodeExecutionData[]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
91
packages/nodes-base/nodes/ApiTemplateIo/GenericFunctions.ts
Normal file
91
packages/nodes-base/nodes/ApiTemplateIo/GenericFunctions.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
export async function apiTemplateIoApiRequest(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: string,
|
||||
endpoint: string,
|
||||
qs = {},
|
||||
body = {},
|
||||
) {
|
||||
const { apiKey } = this.getCredentials('apiTemplateIoApi') as { apiKey: string };
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
'user-agent': 'n8n',
|
||||
Accept: 'application/json',
|
||||
'X-API-KEY': `${apiKey}`,
|
||||
},
|
||||
uri: `https://api.apitemplate.io/v1${endpoint}`,
|
||||
method,
|
||||
qs,
|
||||
body,
|
||||
followRedirect: true,
|
||||
followAllRedirects: true,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (!Object.keys(body).length) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
if (!Object.keys(qs).length) {
|
||||
delete options.qs;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.helpers.request!(options);
|
||||
if (response.status === 'error') {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error?.response?.body?.message) {
|
||||
throw new Error(`APITemplate.io error response [${error.statusCode}]: ${error.response.body.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadResource(
|
||||
this: ILoadOptionsFunctions,
|
||||
resource: 'image' | 'pdf',
|
||||
) {
|
||||
const target = resource === 'image' ? ['JPEG', 'PNG'] : ['PDF'];
|
||||
const templates = await apiTemplateIoApiRequest.call(this, 'GET', '/list-templates');
|
||||
const filtered = templates.filter(({ format }: { format: 'PDF' | 'JPEG' | 'PNG' }) => target.includes(format));
|
||||
|
||||
return filtered.map(({ format, name, id }: { format: string, name: string, id: string }) => ({
|
||||
name: `${name} (${format})`,
|
||||
value: id,
|
||||
}));
|
||||
}
|
||||
|
||||
export function validateJSON(json: string | object | undefined): any { // tslint:disable-line:no-any
|
||||
let result;
|
||||
if (typeof json === 'object') {
|
||||
return json;
|
||||
}
|
||||
try {
|
||||
result = JSON.parse(json!);
|
||||
} catch (exception) {
|
||||
result = undefined;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export function downloadImage(this: IExecuteFunctions, url: string) {
|
||||
return this.helpers.request({
|
||||
uri: url,
|
||||
method: 'GET',
|
||||
json: false,
|
||||
encoding: null,
|
||||
});
|
||||
}
|
102
packages/nodes-base/nodes/ApiTemplateIo/apiTemplateIo.svg
Executable file
102
packages/nodes-base/nodes/ApiTemplateIo/apiTemplateIo.svg
Executable file
|
@ -0,0 +1,102 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 23.166659 21.166671"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.0.1 (1.0.1+r75)"
|
||||
sodipodi:docname="favicon.svg"
|
||||
inkscape:export-filename="/mnt/shared/bktan81@gmail.com/Companies/AlphaCloud/APITemplate.io/export/favicon-color.png"
|
||||
inkscape:export-xdpi="614.40002"
|
||||
inkscape:export-ydpi="614.40002">
|
||||
<defs
|
||||
id="defs2">
|
||||
<rect
|
||||
x="5.5194178"
|
||||
y="6.7918406"
|
||||
width="82.495544"
|
||||
height="30.566183"
|
||||
id="rect835" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="3.6962788"
|
||||
inkscape:cx="84.04094"
|
||||
inkscape:cy="31.651389"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1014"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="36"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
style="fill:#000000"
|
||||
id="g944"
|
||||
transform="matrix(0.26072249,-0.0450346,0.0450346,0.26072249,-4.4090493,0.80238047)">
|
||||
<path
|
||||
fill="#91bce5"
|
||||
d="m 85.926965,13.698648 -63.285045,30.243807 15.697805,11.196666 1.472218,15.148608 13.242212,-6.250661 9.822444,5.345054 z"
|
||||
id="path918" />
|
||||
<path
|
||||
fill="#1f212b"
|
||||
d="m 39.778008,71.288154 c -0.158903,-0.0056 -0.31648,-0.04907 -0.45874,-0.130089 -0.283589,-0.159995 -0.470626,-0.448698 -0.501329,-0.772967 L 37.38898,55.688275 22.06215,44.756717 c -0.288015,-0.205176 -0.446151,-0.547904 -0.415808,-0.901061 0.03035,-0.353155 0.245283,-0.662843 0.564816,-0.815784 L 85.495203,12.796029 c 0.373477,-0.179075 0.817321,-0.109542 1.119534,0.176179 0.301247,0.284686 0.393931,0.72519 0.235443,1.108892 L 63.800813,69.764609 c -0.108224,0.262384 -0.32346,0.465996 -0.589929,0.560754 -0.268434,0.09369 -0.562801,0.0704 -0.811249,-0.06435 l -9.376677,-5.101349 -12.782391,6.034577 c -0.147425,0.0669 -0.305655,0.09939 -0.462559,0.09391 z M 24.604162,44.11304 38.920495,54.324893 c 0.235269,0.168314 0.386237,0.429742 0.414198,0.717894 l 1.33547,13.734974 11.957261,-5.644136 c 0.287923,-0.136035 0.624788,-0.126272 0.904658,0.02559 l 8.84643,4.812668 21.627569,-52.247035 z"
|
||||
id="path920" />
|
||||
<path
|
||||
fill="#3a84c1"
|
||||
d="M 40.73124,69.46431 46.129963,59.222484 85.897126,14.553126 39.059778,54.433821 Z"
|
||||
id="path922" />
|
||||
<path
|
||||
fill="#1f212b"
|
||||
d="m 40.71279,69.963971 c -0.07296,-0.0026 -0.146351,-0.02112 -0.215122,-0.05754 -0.244661,-0.128616 -0.338162,-0.431064 -0.208546,-0.675689 l 5.398723,-10.241827 c 0.01925,-0.03535 0.04242,-0.06856 0.06852,-0.09967 L 80.940068,19.368061 38.684781,54.856992 c -0.211297,0.176733 -0.526545,0.149715 -0.705278,-0.06165 -0.177698,-0.212332 -0.149715,-0.526547 0.06165,-0.705279 L 85.574314,14.169627 c 0.201987,-0.168053 0.497667,-0.153725 0.680201,0.03476 0.182533,0.188486 0.189157,0.485899 0.01525,0.680947 L 46.544181,59.510112 41.173361,69.69788 c -0.09216,0.17489 -0.27468,0.272579 -0.460568,0.266088 z"
|
||||
id="path924" />
|
||||
<path
|
||||
fill="#1f212b"
|
||||
d="m 28.066921,71.78593 c -4.37019,-0.64391 -8.339996,-2.867809 -10.598292,-6.766997 -1.432549,-2.473502 -1.528128,-6.298167 2.122716,-6.172679 3.274075,0.112327 4.26409,2.596776 4.740567,5.406801 1.694711,8.06304 -1.290339,11.045322 -9.002137,14.829352 -0.594821,0.242389 -0.134492,1.113471 0.457295,0.871978 5.181377,-2.944492 7.754842,-4.282847 9.399081,-7.76557 1.370916,-4.981188 0.489139,-11.773963 -2.947089,-13.689368 -3.921594,-2.143168 -8.01567,0.59562 -6.601411,4.939623 1.751362,5.378399 7.074496,8.562112 12.394405,9.345251 0.635099,0.09422 0.665001,-0.905343 0.03486,-0.998391 z"
|
||||
id="path926"
|
||||
sodipodi:nodetypes="cccccccccccc" />
|
||||
<path
|
||||
fill="#1f212b"
|
||||
d="m 32.211855,72.060754 c -0.960938,-0.01857 -1.924304,-0.08221 -2.882497,-0.15069 -0.64277,-0.04646 -0.676671,0.952965 -0.0349,0.999392 0.958193,0.06848 1.921524,0.133135 2.882497,0.150689 0.642958,0.01242 0.679856,-0.986875 0.0349,-0.999391 z m 4.771615,-1.109149 c -1.135251,0.302564 -2.280575,0.549744 -3.442445,0.726303 -0.635768,0.09687 -0.401574,1.069637 0.232195,0.972696 1.16187,-0.176559 2.307194,-0.423738 3.442444,-0.726302 0.620118,-0.164459 0.391991,-1.139015 -0.232194,-0.972697 z"
|
||||
id="path928" />
|
||||
<path
|
||||
fill="#1f212b"
|
||||
d="m 54.069217,64.636113 c -0.08695,-0.003 -0.175091,-0.02913 -0.253366,-0.07989 l -7.481853,-4.834056 c -0.231899,-0.150184 -0.298138,-0.459685 -0.148952,-0.69162 0.150185,-0.231899 0.460683,-0.298103 0.691619,-0.148953 l 7.481852,4.834058 c 0.231899,0.150184 0.298139,0.459686 0.148953,0.69162 -0.09943,0.153622 -0.268357,0.234778 -0.438253,0.228845 z"
|
||||
id="path926-0"
|
||||
sodipodi:nodetypes="sccccccs"
|
||||
style="fill:#000000" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.7 KiB |
|
@ -28,7 +28,7 @@ export class Asana implements INodeType {
|
|||
description: INodeTypeDescription = {
|
||||
displayName: 'Asana',
|
||||
name: 'asana',
|
||||
icon: 'file:asana.png',
|
||||
icon: 'file:asana.svg',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
|
|
|
@ -25,7 +25,7 @@ export class AsanaTrigger implements INodeType {
|
|||
description: INodeTypeDescription = {
|
||||
displayName: 'Asana Trigger',
|
||||
name: 'asanaTrigger',
|
||||
icon: 'file:asana.png',
|
||||
icon: 'file:asana.svg',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow when Asana events occure.',
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB |
18
packages/nodes-base/nodes/Asana/asana.svg
Normal file
18
packages/nodes-base/nodes/Asana/asana.svg
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="60px" height="60px" viewBox="0 0 60 60" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>asana_node_icon</title>
|
||||
<defs>
|
||||
<radialGradient cx="50%" cy="55%" fx="50%" fy="55%" r="72.5074481%" gradientTransform="translate(0.500000,0.550000),scale(0.924043,1.000000),translate(-0.500000,-0.550000)" id="radialGradient-1">
|
||||
<stop stop-color="#FFB900" offset="0%"></stop>
|
||||
<stop stop-color="#F95D8F" offset="60%"></stop>
|
||||
<stop stop-color="#F95353" offset="99.91%"></stop>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g id="asana_node_icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="asana" transform="translate(0.000000, 2.000000)" fill="url(#radialGradient-1)">
|
||||
<g id="Group" transform="translate(0.731707, 0.731707)">
|
||||
<path d="M45.5941463,28.5 C38.5995187,28.50323 32.9300593,34.1726894 32.9268293,41.1673171 C32.9300593,48.1619448 38.5995187,53.8314041 45.5941463,53.8346341 C52.588774,53.8314041 58.2582334,48.1619448 58.2614634,41.1673171 C58.2582334,34.1726894 52.588774,28.50323 45.5941463,28.5 L45.5941463,28.5 Z M12.6673171,28.5014634 C5.67268939,28.5046934 0.00323002946,34.1741528 -1.40700459e-15,41.1687805 C0.00323002946,48.1634082 5.67268939,53.8328675 12.6673171,53.8360976 C19.6619448,53.8328675 25.3314041,48.1634082 25.3346341,41.1687805 C25.3314041,34.1741528 19.6619448,28.5046934 12.6673171,28.5014634 L12.6673171,28.5014634 Z M41.7892683,12.6673171 C41.7868464,19.6619451 36.118042,25.3320595 29.1234146,25.3360976 C22.1282158,25.3328669 16.4585201,19.6625162 16.4560976,12.6673171 C16.4593276,5.67268939 22.128787,0.00323002946 29.1234146,-1.40700459e-15 C36.1174708,0.0040373938 41.7860389,5.67326048 41.7892683,12.6673171 L41.7892683,12.6673171 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
378
packages/nodes-base/nodes/Autopilot/Autopilot.node.ts
Normal file
378
packages/nodes-base/nodes/Autopilot/Autopilot.node.ts
Normal file
|
@ -0,0 +1,378 @@
|
|||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
autopilotApiRequest,
|
||||
autopilotApiRequestAllItems,
|
||||
} from './GenericFunctions';
|
||||
|
||||
import {
|
||||
contactFields,
|
||||
contactOperations,
|
||||
} from './ContactDescription';
|
||||
|
||||
import {
|
||||
contactJourneyFields,
|
||||
contactJourneyOperations,
|
||||
} from './ContactJourneyDescription';
|
||||
|
||||
import {
|
||||
contactListFields,
|
||||
contactListOperations,
|
||||
} from './ContactListDescription';
|
||||
|
||||
import {
|
||||
listFields,
|
||||
listOperations,
|
||||
} from './ListDescription';
|
||||
|
||||
export class Autopilot implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Autopilot',
|
||||
name: 'autopilot',
|
||||
icon: 'file:autopilot.svg',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume Autopilot API',
|
||||
defaults: {
|
||||
name: 'Autopilot',
|
||||
color: '#6ad7b9',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'autopilotApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Contact',
|
||||
value: 'contact',
|
||||
},
|
||||
{
|
||||
name: 'Contact Journey',
|
||||
value: 'contactJourney',
|
||||
},
|
||||
{
|
||||
name: 'Contact List',
|
||||
value: 'contactList',
|
||||
},
|
||||
{
|
||||
name: 'List',
|
||||
value: 'list',
|
||||
},
|
||||
],
|
||||
default: 'contact',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
|
||||
...contactOperations,
|
||||
...contactFields,
|
||||
...contactJourneyOperations,
|
||||
...contactJourneyFields,
|
||||
...contactListOperations,
|
||||
...contactListFields,
|
||||
...listOperations,
|
||||
...listFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
async getCustomFields(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const customFields = await autopilotApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/contacts/custom_fields',
|
||||
);
|
||||
for (const customField of customFields) {
|
||||
returnData.push({
|
||||
name: customField.name,
|
||||
value: `${customField.name}-${customField.fieldType}`,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
async getLists(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { lists } = await autopilotApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/lists',
|
||||
);
|
||||
for (const list of lists) {
|
||||
returnData.push({
|
||||
name: list.title,
|
||||
value: list.list_id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
async getTriggers(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { triggers } = await autopilotApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/triggers',
|
||||
);
|
||||
for (const trigger of triggers) {
|
||||
returnData.push({
|
||||
name: trigger.journey,
|
||||
value: trigger.trigger_id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
const length = (items.length as unknown) as number;
|
||||
const qs: IDataObject = {};
|
||||
let responseData;
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
if (resource === 'contact') {
|
||||
if (operation === 'upsert') {
|
||||
const email = this.getNodeParameter('email', i) as string;
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
|
||||
const body: IDataObject = {
|
||||
Email: email,
|
||||
};
|
||||
|
||||
Object.assign(body, additionalFields);
|
||||
|
||||
if (body.customFieldsUi) {
|
||||
const customFieldsValues = (body.customFieldsUi as IDataObject).customFieldsValues as IDataObject[];
|
||||
|
||||
body.custom = {};
|
||||
|
||||
for (const customField of customFieldsValues) {
|
||||
const [name, fieldType] = (customField.key as string).split('-');
|
||||
|
||||
const fieldName = name.replace(/\s/g, '--');
|
||||
|
||||
//@ts-ignore
|
||||
body.custom[`${fieldType}--${fieldName}`] = customField.value;
|
||||
}
|
||||
delete body.customFieldsUi;
|
||||
}
|
||||
|
||||
if (body.autopilotList) {
|
||||
body._autopilot_list = body.autopilotList;
|
||||
delete body.autopilotList;
|
||||
}
|
||||
|
||||
if (body.autopilotSessionId) {
|
||||
body._autopilot_session_id = body.autopilotSessionId;
|
||||
delete body.autopilotSessionId;
|
||||
}
|
||||
|
||||
if (body.newEmail) {
|
||||
body._NewEmail = body.newEmail;
|
||||
delete body.newEmail;
|
||||
}
|
||||
|
||||
responseData = await autopilotApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/contact`,
|
||||
{ contact: body },
|
||||
);
|
||||
}
|
||||
|
||||
if (operation === 'delete') {
|
||||
const contactId = this.getNodeParameter('contactId', i) as string;
|
||||
|
||||
responseData = await autopilotApiRequest.call(
|
||||
this,
|
||||
'DELETE',
|
||||
`/contact/${contactId}`,
|
||||
);
|
||||
|
||||
responseData = { success: true };
|
||||
}
|
||||
|
||||
if (operation === 'get') {
|
||||
const contactId = this.getNodeParameter('contactId', i) as string;
|
||||
|
||||
responseData = await autopilotApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/contact/${contactId}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (operation === 'getAll') {
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
if (returnAll === false) {
|
||||
qs.limit = this.getNodeParameter('limit', i) as number;
|
||||
}
|
||||
responseData = await autopilotApiRequestAllItems.call(
|
||||
this,
|
||||
'contacts',
|
||||
'GET',
|
||||
`/contacts`,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
|
||||
if (returnAll === false) {
|
||||
responseData = responseData.splice(0, qs.limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resource === 'contactJourney') {
|
||||
if (operation === 'add') {
|
||||
|
||||
const triggerId = this.getNodeParameter('triggerId', i) as string;
|
||||
|
||||
const contactId = this.getNodeParameter('contactId', i) as string;
|
||||
|
||||
responseData = await autopilotApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/trigger/${triggerId}/contact/${contactId}`,
|
||||
);
|
||||
|
||||
responseData = { success: true };
|
||||
}
|
||||
}
|
||||
if (resource === 'contactList') {
|
||||
if (['add', 'remove', 'exist'].includes(operation)) {
|
||||
|
||||
const listId = this.getNodeParameter('listId', i) as string;
|
||||
|
||||
const contactId = this.getNodeParameter('contactId', i) as string;
|
||||
|
||||
const method: { [key: string]: string } = {
|
||||
'add': 'POST',
|
||||
'remove': 'DELETE',
|
||||
'exist': 'GET',
|
||||
};
|
||||
|
||||
const endpoint = `/list/${listId}/contact/${contactId}`;
|
||||
|
||||
if (operation === 'exist') {
|
||||
try {
|
||||
await autopilotApiRequest.call(this, method[operation], endpoint);
|
||||
responseData = { exist: true };
|
||||
} catch (error) {
|
||||
responseData = { exist: false };
|
||||
}
|
||||
} else if (operation === 'add' || operation === 'remove') {
|
||||
responseData = await autopilotApiRequest.call(this, method[operation], endpoint);
|
||||
responseData['success'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'getAll') {
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
const listId = this.getNodeParameter('listId', i) as string;
|
||||
|
||||
if (returnAll === false) {
|
||||
qs.limit = this.getNodeParameter('limit', i) as number;
|
||||
}
|
||||
responseData = await autopilotApiRequestAllItems.call(
|
||||
this,
|
||||
'contacts',
|
||||
'GET',
|
||||
`/list/${listId}/contacts`,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
|
||||
if (returnAll === false) {
|
||||
responseData = responseData.splice(0, qs.limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resource === 'list') {
|
||||
if (operation === 'create') {
|
||||
|
||||
const name = this.getNodeParameter('name', i) as string;
|
||||
|
||||
const body: IDataObject = {
|
||||
name,
|
||||
};
|
||||
|
||||
responseData = await autopilotApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/list`,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
if (operation === 'getAll') {
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
if (returnAll === false) {
|
||||
qs.limit = this.getNodeParameter('limit', i) as number;
|
||||
}
|
||||
responseData = await autopilotApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/lists',
|
||||
);
|
||||
|
||||
responseData = responseData.lists;
|
||||
|
||||
if (returnAll === false) {
|
||||
responseData = responseData.splice(0, qs.limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else if (responseData !== undefined) {
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ error: error.toString() });
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
140
packages/nodes-base/nodes/Autopilot/AutopilotTrigger.node.ts
Normal file
140
packages/nodes-base/nodes/Autopilot/AutopilotTrigger.node.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
import {
|
||||
IHookFunctions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IWebhookResponseData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
autopilotApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
import {
|
||||
snakeCase,
|
||||
} from 'change-case';
|
||||
|
||||
export class AutopilotTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Autopilot Trigger',
|
||||
name: 'autopilotTrigger',
|
||||
icon: 'file:autopilot.svg',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["event"]}}',
|
||||
description: 'Handle Autopilot events via webhooks',
|
||||
defaults: {
|
||||
name: 'Autopilot Trigger',
|
||||
color: '#6ad7b9',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'autopilotApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Event',
|
||||
name: 'event',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: '',
|
||||
options: [
|
||||
{
|
||||
name: 'Contact Added',
|
||||
value: 'contactAdded',
|
||||
},
|
||||
{
|
||||
name: 'Contact Added To List',
|
||||
value: 'contactAddedToList',
|
||||
},
|
||||
{
|
||||
name: 'Contact Entered Segment',
|
||||
value: 'contactEnteredSegment',
|
||||
},
|
||||
{
|
||||
name: 'Contact Left Segment',
|
||||
value: 'contactLeftSegment',
|
||||
},
|
||||
{
|
||||
name: 'Contact Removed From List',
|
||||
value: 'contactRemovedFromList',
|
||||
},
|
||||
{
|
||||
name: 'Contact Unsubscribed',
|
||||
value: 'contactUnsubscribed',
|
||||
},
|
||||
{
|
||||
name: 'Contact Updated',
|
||||
value: 'contactUpdated',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
const event = this.getNodeParameter('event') as string;
|
||||
const { hooks: webhooks } = await autopilotApiRequest.call(this, 'GET', '/hooks');
|
||||
for (const webhook of webhooks) {
|
||||
if (webhook.target_url === webhookUrl && webhook.event === snakeCase(event)) {
|
||||
webhookData.webhookId = webhook.hook_id;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const event = this.getNodeParameter('event') as string;
|
||||
const body: IDataObject = {
|
||||
event: snakeCase(event),
|
||||
target_url: webhookUrl,
|
||||
};
|
||||
const webhook = await autopilotApiRequest.call(this, 'POST', '/hook', body);
|
||||
webhookData.webhookId = webhook.hook_id;
|
||||
return true;
|
||||
},
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
try {
|
||||
await autopilotApiRequest.call(this, 'DELETE', `/hook/${webhookData.webhookId}`);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
delete webhookData.webhookId;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const req = this.getRequestObject();
|
||||
return {
|
||||
workflowData: [
|
||||
this.helpers.returnJsonArray(req.body),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
369
packages/nodes-base/nodes/Autopilot/ContactDescription.ts
Normal file
369
packages/nodes-base/nodes/Autopilot/ContactDescription.ts
Normal file
|
@ -0,0 +1,369 @@
|
|||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const contactOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create/Update',
|
||||
value: 'upsert',
|
||||
description: 'Create/Update a contact',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a contact',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a contact',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all contacts',
|
||||
},
|
||||
],
|
||||
default: 'upsert',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const contactFields = [
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:upsert */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
required: true,
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'upsert',
|
||||
],
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Email address of the contact.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'upsert',
|
||||
],
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
placeholder: 'Add Field',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Company',
|
||||
name: 'Company',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Fields',
|
||||
name: 'customFieldsUi',
|
||||
type: 'fixedCollection',
|
||||
default: '',
|
||||
placeholder: 'Add Custom Field',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
loadOptionsMethod: 'getCustomFields',
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'customFieldsValues',
|
||||
displayName: 'Custom Field',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getCustomFields',
|
||||
},
|
||||
description: 'User-specified key of user-defined data.',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
description: 'User-specified value of user-defined data.',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Fax',
|
||||
name: 'Fax',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'FirstName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Industry',
|
||||
name: 'Industry',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'LastName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Lead Source',
|
||||
name: 'LeadSource',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'LinkedIn URL',
|
||||
name: 'LinkedIn',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'List ID',
|
||||
name: 'autopilotList',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLists',
|
||||
},
|
||||
default: '',
|
||||
description: 'List to which this contact will be added on creation.',
|
||||
},
|
||||
{
|
||||
displayName: 'Mailing Country',
|
||||
name: 'MailingCountry',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Mailing Postal Code',
|
||||
name: 'MailingPostalCode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Mailing State',
|
||||
name: 'MailingState',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Mailing Street',
|
||||
name: 'MailingStreet',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Mailing City',
|
||||
name: 'MailingCity',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Mobile Phone',
|
||||
name: 'MobilePhone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'New Email',
|
||||
name: 'newEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'If provided, will change the email address of the contact identified by the Email field.',
|
||||
},
|
||||
{
|
||||
displayName: 'Notify',
|
||||
name: 'notify',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: `By default Autopilot notifies registered REST hook endpoints for contact_added/contact_updated events when</br> a new contact is added or an existing contact is updated via API. Disable to skip notifications.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Number of Employees',
|
||||
name: 'NumberOfEmployees',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name',
|
||||
name: 'owner_name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'Phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Salutation',
|
||||
name: 'Salutation',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'autopilotSessionId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Used to associate a contact with a session.',
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'Status',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'Title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Subscribe',
|
||||
name: 'unsubscribed',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to subscribe or un-subscribe a contact.',
|
||||
},
|
||||
{
|
||||
displayName: 'Website URL',
|
||||
name: 'Website',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:delete */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'delete',
|
||||
],
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Can be ID or email.',
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:get */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'get',
|
||||
],
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Can be ID or email.',
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
},
|
||||
},
|
||||
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: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 500,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
] as INodeProperties[];
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const contactJourneyOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contactJourney',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add',
|
||||
value: 'add',
|
||||
description: 'Add contact to list',
|
||||
},
|
||||
],
|
||||
default: 'add',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const contactJourneyFields = [
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contactJourney:add */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Trigger ID',
|
||||
name: 'triggerId',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTriggers',
|
||||
},
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'add',
|
||||
],
|
||||
resource: [
|
||||
'contactJourney',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'List ID.',
|
||||
},
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'add',
|
||||
],
|
||||
resource: [
|
||||
'contactJourney',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Can be ID or email.',
|
||||
},
|
||||
] as INodeProperties[];
|
138
packages/nodes-base/nodes/Autopilot/ContactListDescription.ts
Normal file
138
packages/nodes-base/nodes/Autopilot/ContactListDescription.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const contactListOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contactList',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add',
|
||||
value: 'add',
|
||||
description: 'Add contact to list.',
|
||||
},
|
||||
{
|
||||
name: 'Exist',
|
||||
value: 'exist',
|
||||
description: 'Check if contact is on list.',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all contacts on list.',
|
||||
},
|
||||
{
|
||||
name: 'Remove',
|
||||
value: 'remove',
|
||||
description: 'Remove a contact from a list.',
|
||||
},
|
||||
],
|
||||
default: 'add',
|
||||
description: 'Operation to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const contactListFields = [
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contactList:add */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'List ID',
|
||||
name: 'listId',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLists',
|
||||
},
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'add',
|
||||
'remove',
|
||||
'exist',
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'contactList',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'ID of the list to operate on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'add',
|
||||
'remove',
|
||||
'exist',
|
||||
],
|
||||
resource: [
|
||||
'contactList',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Can be ID or email.',
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contactList:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'contactList',
|
||||
],
|
||||
},
|
||||
},
|
||||
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: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'contactList',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 500,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
] as INodeProperties[];
|
72
packages/nodes-base/nodes/Autopilot/GenericFunctions.ts
Normal file
72
packages/nodes-base/nodes/Autopilot/GenericFunctions.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
IHookFunctions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function autopilotApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const credentials = this.getCredentials('autopilotApi') as IDataObject;
|
||||
|
||||
const apiKey = `${credentials.apiKey}`;
|
||||
|
||||
const endpoint = 'https://api2.autopilothq.com/v1';
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
autopilotapikey: apiKey,
|
||||
},
|
||||
method,
|
||||
body,
|
||||
qs: query,
|
||||
uri: uri || `${endpoint}${resource}`,
|
||||
json: true,
|
||||
};
|
||||
if (!Object.keys(body).length) {
|
||||
delete options.body;
|
||||
}
|
||||
if (!Object.keys(query).length) {
|
||||
delete options.qs;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
const errorMessage = error.response.body.message || error.response.body.description || error.message;
|
||||
throw new Error(`Autopilot error response [${error.statusCode}]: ${errorMessage}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function autopilotApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const returnData: IDataObject[] = [];
|
||||
const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean;
|
||||
|
||||
const base = endpoint;
|
||||
|
||||
let responseData;
|
||||
do {
|
||||
responseData = await autopilotApiRequest.call(this, method, endpoint, body, query);
|
||||
endpoint = `${base}/${responseData.bookmark}`;
|
||||
returnData.push.apply(returnData, responseData[propertyName]);
|
||||
if (query.limit && returnData.length >= query.limit && returnAll === false) {
|
||||
return returnData;
|
||||
}
|
||||
} while (
|
||||
responseData.bookmark !== undefined
|
||||
);
|
||||
return returnData;
|
||||
}
|
102
packages/nodes-base/nodes/Autopilot/ListDescription.ts
Normal file
102
packages/nodes-base/nodes/Autopilot/ListDescription.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const listOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'list',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a list.',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all lists.',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const listFields = [
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* list:create */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
required: true,
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
resource: [
|
||||
'list',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Name of the list to create.',
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* list:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'list',
|
||||
],
|
||||
},
|
||||
},
|
||||
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: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'list',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 500,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
] as INodeProperties[];
|
8
packages/nodes-base/nodes/Autopilot/autopilot.svg
Normal file
8
packages/nodes-base/nodes/Autopilot/autopilot.svg
Normal file
|
@ -0,0 +1,8 @@
|
|||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="38 26 35 35">
|
||||
<circle cx="50" cy="50" r="40" stroke="#18d4b2" stroke-width="3" fill="#18d4b2" />
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#ffffff" d="M45.4,42.6h19.9l3.4-4.8H42L45.4,42.6z M48.5,50.9h13.1l3.4-4.8H45.4L48.5,50.9z M102.5,50.2"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 378 B |
20
packages/nodes-base/nodes/Aws/AwsComprehend.node.json
Normal file
20
packages/nodes-base/nodes/Aws/AwsComprehend.node.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"node": "n8n-nodes-base.awsComprehend",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": [
|
||||
"Development"
|
||||
],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/credentials/aws"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.awsComprehend/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@ function getEndpointForService(service: string, credentials: ICredentialDataDecr
|
|||
endpoint = credentials.lambdaEndpoint;
|
||||
} else if (service === 'sns' && credentials.snsEndpoint) {
|
||||
endpoint = credentials.snsEndpoint;
|
||||
} else if (service === 'sqs' && credentials.sqsEndpoint) {
|
||||
endpoint = credentials.sqsEndpoint;
|
||||
} else {
|
||||
endpoint = `https://${service}.${credentials.region}.amazonaws.com`;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
|
||||
import {
|
||||
awsApiRequestREST,
|
||||
keysTPascalCase,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class AwsRekognition implements INodeType {
|
||||
|
@ -66,6 +67,16 @@ export class AwsRekognition implements INodeType {
|
|||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'analyze',
|
||||
],
|
||||
resource: [
|
||||
'image',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Detect Faces',
|
||||
|
@ -79,6 +90,10 @@ export class AwsRekognition implements INodeType {
|
|||
name: 'Detect Moderation Labels',
|
||||
value: 'detectModerationLabels',
|
||||
},
|
||||
{
|
||||
name: 'Detect Text',
|
||||
value: 'detectText',
|
||||
},
|
||||
{
|
||||
name: 'Recognize Celebrity',
|
||||
value: 'recognizeCelebrity',
|
||||
|
@ -185,6 +200,59 @@ export class AwsRekognition implements INodeType {
|
|||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Regions of Interest',
|
||||
name: 'regionsOfInterestUi',
|
||||
type: 'fixedCollection',
|
||||
default: '',
|
||||
placeholder: 'Add Region of Interest',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/type': [
|
||||
'detectText',
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'regionsOfInterestValues',
|
||||
displayName: 'Region of Interest',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Height',
|
||||
name: 'height',
|
||||
type: 'number',
|
||||
description: 'Height of the bounding box as a ratio of the overall image height.',
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
displayName: 'Left',
|
||||
name: 'left',
|
||||
type: 'number',
|
||||
description: 'Left coordinate of the bounding box as a ratio of overall image width.',
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
displayName: 'Top',
|
||||
name: 'top',
|
||||
type: 'number',
|
||||
description: 'Top coordinate of the bounding box as a ratio of overall image height.',
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
displayName: 'Width',
|
||||
name: 'Width',
|
||||
type: 'number',
|
||||
description: 'Width of the bounding box as a ratio of the overall image width.',
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Version',
|
||||
name: 'version',
|
||||
|
@ -199,6 +267,46 @@ export class AwsRekognition implements INodeType {
|
|||
default: '',
|
||||
description: 'If the bucket is versioning enabled, you can specify the object version',
|
||||
},
|
||||
{
|
||||
displayName: 'Word Filter',
|
||||
name: 'wordFilterUi',
|
||||
type: 'collection',
|
||||
default: '',
|
||||
placeholder: 'Add Word Filter',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/type': [
|
||||
'detectText',
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Min Bounding Box Height',
|
||||
name: 'MinBoundingBoxHeight',
|
||||
type: 'number',
|
||||
description: 'Sets the minimum height of the word bounding box. Words with bounding box heights lesser than this value will be excluded from the result. Value is relative to the video frame height.',
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
displayName: 'Min Bounding Box Width',
|
||||
name: 'MinBoundingBoxWidth',
|
||||
type: 'number',
|
||||
description: 'Sets the minimum width of the word bounding box. Words with bounding boxes widths lesser than this value will be excluded from the result. Value is relative to the video frame width.',
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
displayName: 'Min Confidence',
|
||||
name: 'MinConfidence',
|
||||
type: 'number',
|
||||
description: 'Sets the confidence of word detection. Words with detection confidence below this will be excluded from the result. Values should be between 50 and 100 as Text in Video will not return any result below 50.',
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Max Labels',
|
||||
name: 'maxLabels',
|
||||
|
@ -280,7 +388,7 @@ export class AwsRekognition implements INodeType {
|
|||
|
||||
let action = undefined;
|
||||
|
||||
let body: IDataObject = {};
|
||||
const body: IDataObject = {};
|
||||
|
||||
const type = this.getNodeParameter('type', 0) as string;
|
||||
|
||||
|
@ -324,57 +432,72 @@ export class AwsRekognition implements INodeType {
|
|||
action = 'RekognitionService.RecognizeCelebrities';
|
||||
}
|
||||
|
||||
const binaryData = this.getNodeParameter('binaryData', 0) as boolean;
|
||||
if (type === 'detectText') {
|
||||
action = 'RekognitionService.DetectText';
|
||||
|
||||
if (binaryData) {
|
||||
body.Filters = {};
|
||||
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string;
|
||||
const box = (additionalFields.regionsOfInterestUi as IDataObject || {}).regionsOfInterestValues as IDataObject[] || [];
|
||||
|
||||
if (items[i].binary === undefined) {
|
||||
throw new Error('No binary data exists on item!');
|
||||
}
|
||||
|
||||
if ((items[i].binary as IBinaryKeyData)[binaryPropertyName] === undefined) {
|
||||
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
|
||||
}
|
||||
|
||||
const binaryPropertyData = (items[i].binary as IBinaryKeyData)[binaryPropertyName];
|
||||
|
||||
body = {
|
||||
Image: {
|
||||
Bytes: binaryPropertyData.data,
|
||||
},
|
||||
};
|
||||
|
||||
} else {
|
||||
|
||||
const bucket = this.getNodeParameter('bucket', i) as string;
|
||||
|
||||
const name = this.getNodeParameter('name', i) as string;
|
||||
|
||||
body = {
|
||||
Image: {
|
||||
S3Object: {
|
||||
Bucket: bucket,
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (additionalFields.version) {
|
||||
if (box.length !== 0) {
|
||||
//@ts-ignore
|
||||
body.Image.S3Object.Version = additionalFields.version as string;
|
||||
body.Filters.RegionsOfInterest = box.map((box: IDataObject) => {
|
||||
return { BoundingBox: keysTPascalCase(box) };
|
||||
});
|
||||
}
|
||||
|
||||
const wordFilter = additionalFields.wordFilterUi as IDataObject || {};
|
||||
if (Object.keys(wordFilter).length !== 0) {
|
||||
//@ts-ignore
|
||||
body.Filters.WordFilter = keysTPascalCase(wordFilter);
|
||||
}
|
||||
|
||||
const binaryData = this.getNodeParameter('binaryData', 0) as boolean;
|
||||
|
||||
if (binaryData) {
|
||||
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string;
|
||||
|
||||
if (items[i].binary === undefined) {
|
||||
throw new Error('No binary data exists on item!');
|
||||
}
|
||||
|
||||
if ((items[i].binary as IBinaryKeyData)[binaryPropertyName] === undefined) {
|
||||
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
|
||||
}
|
||||
|
||||
const binaryPropertyData = (items[i].binary as IBinaryKeyData)[binaryPropertyName];
|
||||
|
||||
Object.assign(body, {
|
||||
Image: {
|
||||
Bytes: binaryPropertyData.data,
|
||||
},
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
const bucket = this.getNodeParameter('bucket', i) as string;
|
||||
|
||||
const name = this.getNodeParameter('name', i) as string;
|
||||
|
||||
Object.assign(body, {
|
||||
Image: {
|
||||
S3Object: {
|
||||
Bucket: bucket,
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (additionalFields.version) {
|
||||
//@ts-ignore
|
||||
body.Image.S3Object.Version = additionalFields.version as string;
|
||||
}
|
||||
}
|
||||
responseData = await awsApiRequestREST.call(this, 'rekognition', 'POST', '', JSON.stringify(body), {}, { 'X-Amz-Target': action, 'Content-Type': 'application/x-amz-json-1.1' });
|
||||
}
|
||||
|
||||
responseData = await awsApiRequestREST.call(this, 'rekognition', 'POST', '', JSON.stringify(body), {}, { 'X-Amz-Target': action, 'Content-Type': 'application/x-amz-json-1.1' });
|
||||
|
||||
// if (property !== undefined) {
|
||||
// responseData = responseData[property as string];
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else {
|
||||
|
|
|
@ -29,6 +29,10 @@ import {
|
|||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
pascalCase,
|
||||
} from 'change-case';
|
||||
|
||||
export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string | Buffer | IDataObject, query: IDataObject = {}, headers?: object, option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('aws');
|
||||
if (credentials === undefined) {
|
||||
|
@ -128,3 +132,11 @@ export async function awsApiRequestSOAPAllItems(this: IHookFunctions | IExecuteF
|
|||
function queryToString(params: IDataObject) {
|
||||
return Object.keys(params).map(key => key + '=' + params[key]).join('&');
|
||||
}
|
||||
|
||||
export function keysTPascalCase(object: IDataObject) {
|
||||
const data: IDataObject = {};
|
||||
for (const key of Object.keys(object)) {
|
||||
data[pascalCase(key as string)] = object[key];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -1125,15 +1125,9 @@ export class AwsSes implements INodeType {
|
|||
setParameter(params, 'Destination.BccAddresses.member', additionalFields.bccAddresses as string[]);
|
||||
}
|
||||
|
||||
if (additionalFields.ccAddressesUi) {
|
||||
let ccAddresses = (additionalFields.ccAddressesUi as IDataObject).ccAddressesValues as string[];
|
||||
//@ts-ignore
|
||||
ccAddresses = ccAddresses.map(o => o.address);
|
||||
if (ccAddresses) {
|
||||
setParameter(params, 'Destination.CcAddresses.member', ccAddresses);
|
||||
}
|
||||
if (additionalFields.ccAddresses) {
|
||||
setParameter(params, 'Destination.CcAddresses.member', additionalFields.ccAddresses as string[]);
|
||||
}
|
||||
|
||||
responseData = await awsApiRequestSOAP.call(this, 'email', 'POST', '/?Action=SendEmail&' + params.join('&'));
|
||||
}
|
||||
|
||||
|
@ -1184,13 +1178,8 @@ export class AwsSes implements INodeType {
|
|||
setParameter(params, 'Destination.BccAddresses.member', additionalFields.bccAddresses as string[]);
|
||||
}
|
||||
|
||||
if (additionalFields.ccAddressesUi) {
|
||||
let ccAddresses = (additionalFields.ccAddressesUi as IDataObject).ccAddressesValues as string[];
|
||||
//@ts-ignore
|
||||
ccAddresses = ccAddresses.map(o => o.address);
|
||||
if (ccAddresses) {
|
||||
setParameter(params, 'Destination.CcAddresses.member', ccAddresses);
|
||||
}
|
||||
if (additionalFields.ccAddresses) {
|
||||
setParameter(params, 'Destination.CcAddresses.member', additionalFields.ccAddresses as string[]);
|
||||
}
|
||||
|
||||
if (templateDataUi) {
|
||||
|
|
21
packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.json
Normal file
21
packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"node": "n8n-nodes-base.awsSqs",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": [
|
||||
"Development",
|
||||
"Communication"
|
||||
],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/credentials/aws"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.awsSqs/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
387
packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.ts
Normal file
387
packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.ts
Normal file
|
@ -0,0 +1,387 @@
|
|||
import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
URL,
|
||||
} from 'url';
|
||||
|
||||
import {
|
||||
awsApiRequestSOAP,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
export class AwsSqs implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'AWS SQS',
|
||||
name: 'awsSqs',
|
||||
icon: 'file:sqs.svg',
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
subtitle: `={{$parameter["operation"]}}`,
|
||||
description: 'Sends messages to AWS SQS',
|
||||
defaults: {
|
||||
name: 'AWS SQS',
|
||||
color: '#FF9900',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'aws',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Send message',
|
||||
value: 'sendMessage',
|
||||
description: 'Send a message to a queue.',
|
||||
},
|
||||
],
|
||||
default: 'sendMessage',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Queue',
|
||||
name: 'queue',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getQueues',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'sendMessage',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [],
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Queue to send a message to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Queue Type',
|
||||
name: 'queueType',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'FIFO',
|
||||
value: 'fifo',
|
||||
description: 'FIFO SQS queue.',
|
||||
},
|
||||
{
|
||||
name: 'Standard',
|
||||
value: 'standard',
|
||||
description: 'Standard SQS queue.',
|
||||
},
|
||||
],
|
||||
default: 'standard',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Send Input Data',
|
||||
name: 'sendInputData',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Send the data the node receives as JSON to SQS.',
|
||||
},
|
||||
{
|
||||
displayName: 'Message',
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'sendMessage',
|
||||
],
|
||||
sendInputData: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Message to send to the queue.',
|
||||
},
|
||||
{
|
||||
displayName: 'Message Group ID',
|
||||
name: 'messageGroupId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Tag that specifies that a message belongs to a specific message group. Applies only to FIFO (first-in-first-out) queues.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
queueType: [
|
||||
'fifo',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'sendMessage',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
placeholder: 'Add Option',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Delay Seconds',
|
||||
name: 'delaySeconds',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/queueType': [
|
||||
'standard',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'How long, in seconds, to delay a message for.',
|
||||
default: 0,
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 900,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Message Attributes',
|
||||
name: 'messageAttributes',
|
||||
placeholder: 'Add Attribute',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
description: 'Attributes to set.',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'binary',
|
||||
displayName: 'Binary',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the attribute.',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'dataPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
description: 'Name of the binary property which contains the data for the message attribute.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
displayName: 'Number',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the attribute.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'Number value of the attribute.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the attribute.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'String value of attribute.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Message Deduplication ID',
|
||||
name: 'messageDeduplicationId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Token used for deduplication of sent messages. Applies only to FIFO (first-in-first-out) queues.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/queueType': [
|
||||
'fifo',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
// Get all the available queues to display them to user so that it can be selected easily
|
||||
async getQueues(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
let data;
|
||||
try {
|
||||
// loads first 1000 queues from SQS
|
||||
data = await awsApiRequestSOAP.call(this, 'sqs', 'GET', `?Action=ListQueues`);
|
||||
} catch (err) {
|
||||
throw new Error(`AWS Error: ${err}`);
|
||||
}
|
||||
|
||||
let queues = data.ListQueuesResponse.ListQueuesResult.QueueUrl;
|
||||
if (!queues) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(queues)) {
|
||||
// If user has only a single queue no array get returned so we make
|
||||
// one manually to be able to process everything identically
|
||||
queues = [queues];
|
||||
}
|
||||
|
||||
return queues.map((queueUrl: string) => {
|
||||
const urlParts = queueUrl.split('/');
|
||||
const name = urlParts[urlParts.length - 1];
|
||||
|
||||
return {
|
||||
name,
|
||||
value: queueUrl,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const queueUrl = this.getNodeParameter('queue', i) as string;
|
||||
const queuePath = new URL(queueUrl).pathname;
|
||||
const params = [];
|
||||
|
||||
const options = this.getNodeParameter('options', i, {}) as IDataObject;
|
||||
const sendInputData = this.getNodeParameter('sendInputData', i) as boolean;
|
||||
|
||||
const message = sendInputData ? JSON.stringify(items[i].json) : this.getNodeParameter('message', i) as string;
|
||||
params.push(`MessageBody=${message}`);
|
||||
|
||||
if (options.delaySeconds) {
|
||||
params.push(`DelaySeconds=${options.delaySeconds}`);
|
||||
}
|
||||
|
||||
const queueType = this.getNodeParameter('queueType', i, {}) as string;
|
||||
if (queueType === 'fifo') {
|
||||
const messageDeduplicationId = this.getNodeParameter('options.messageDeduplicationId', i, '') as string;
|
||||
if (messageDeduplicationId) {
|
||||
params.push(`MessageDeduplicationId=${messageDeduplicationId}`);
|
||||
}
|
||||
|
||||
const messageGroupId = this.getNodeParameter('messageGroupId', i) as string;
|
||||
if (messageGroupId) {
|
||||
params.push(`MessageGroupId=${messageGroupId}`);
|
||||
}
|
||||
}
|
||||
|
||||
let attributeCount = 0;
|
||||
// Add string values
|
||||
(this.getNodeParameter('options.messageAttributes.string', i, []) as INodeParameters[]).forEach((attribute) => {
|
||||
attributeCount++;
|
||||
params.push(`MessageAttribute.${attributeCount}.Name=${attribute.name}`);
|
||||
params.push(`MessageAttribute.${attributeCount}.Value.StringValue=${attribute.value}`);
|
||||
params.push(`MessageAttribute.${attributeCount}.Value.DataType=String`);
|
||||
});
|
||||
|
||||
// Add binary values
|
||||
(this.getNodeParameter('options.messageAttributes.binary', i, []) as INodeParameters[]).forEach((attribute) => {
|
||||
attributeCount++;
|
||||
const dataPropertyName = attribute.dataPropertyName as string;
|
||||
const item = items[i];
|
||||
|
||||
if (item.binary === undefined) {
|
||||
throw new Error('No binary data set. So message attribute cannot be added!');
|
||||
}
|
||||
|
||||
if (item.binary[dataPropertyName] === undefined) {
|
||||
throw new Error(`The binary property "${dataPropertyName}" does not exist. So message attribute cannot be added!`);
|
||||
}
|
||||
|
||||
const binaryData = item.binary[dataPropertyName].data;
|
||||
|
||||
params.push(`MessageAttribute.${attributeCount}.Name=${attribute.name}`);
|
||||
params.push(`MessageAttribute.${attributeCount}.Value.BinaryValue=${binaryData}`);
|
||||
params.push(`MessageAttribute.${attributeCount}.Value.DataType=Binary`);
|
||||
});
|
||||
|
||||
// Add number values
|
||||
(this.getNodeParameter('options.messageAttributes.number', i, []) as INodeParameters[]).forEach((attribute) => {
|
||||
attributeCount++;
|
||||
params.push(`MessageAttribute.${attributeCount}.Name=${attribute.name}`);
|
||||
params.push(`MessageAttribute.${attributeCount}.Value.StringValue=${attribute.value}`);
|
||||
params.push(`MessageAttribute.${attributeCount}.Value.DataType=Number`);
|
||||
});
|
||||
|
||||
let responseData;
|
||||
try {
|
||||
responseData = await awsApiRequestSOAP.call(this, 'sqs', 'GET', `${queuePath}/?Action=${operation}&` + params.join('&'));
|
||||
} catch (err) {
|
||||
throw new Error(`AWS Error: ${err}`);
|
||||
}
|
||||
|
||||
const result = responseData.SendMessageResponse.SendMessageResult;
|
||||
returnData.push(result as IDataObject);
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
1
packages/nodes-base/nodes/Aws/SQS/sqs.svg
Normal file
1
packages/nodes-base/nodes/Aws/SQS/sqs.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-5 0 85 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x="2.188" y="2.5"/><symbol id="A" overflow="visible"><g fill="#876929" stroke="none"><path d="M0 25.938L.021 63.44 35 80l34.98-16.56v-9.368l-29.745-3.508.02-21.127L70 25.948V16.57L35 0 0 16.56v9.378z"/><path d="M.021 54.062l34.98 9.942V80L.021 63.44v-9.378z"/><path d="M4.465 65.549L0 63.431V16.56l4.475-2.109-.01 51.098z"/></g><path d="M40.255 50.564l-35.79 4.762.01-30.661 35.78 4.772v21.127zM70 25.948l-35-9.951V0l35 16.57v9.378zm-.02 28.124L35 64.004V80l34.98-16.56v-9.368z" stroke="none" fill="#d9a741"/><path d="M22.109 48.581l12.892 1.526V29.815L22.109 31.36v17.221zM9.125 47.065l8.365.982V31.924l-8.365 1.001v14.14z" stroke="none" fill="#876929"/><path d="M4.475 24.665L35 15.996l35 9.951-29.745 3.489-35.78-4.772z" fill="#624a1e" stroke="none"/><path d="M4.465 55.326L35 64.004l34.98-9.932-29.724-3.508-35.79 4.762z" fill="#fad791" stroke="none"/><path d="M69.98 45.918L35 50.107V29.815l34.98 4.218v11.885z" fill="#d9a741" stroke="none"/></symbol></svg>
|
After Width: | Height: | Size: 1.2 KiB |
509
packages/nodes-base/nodes/Bitwarden/Bitwarden.node.ts
Normal file
509
packages/nodes-base/nodes/Bitwarden/Bitwarden.node.ts
Normal file
|
@ -0,0 +1,509 @@
|
|||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
bitwardenApiRequest as tokenlessBitwardenApiRequest,
|
||||
getAccessToken,
|
||||
handleGetAll as tokenlessHandleGetAll,
|
||||
loadResource,
|
||||
} from './GenericFunctions';
|
||||
|
||||
import {
|
||||
collectionFields,
|
||||
collectionOperations,
|
||||
CollectionUpdateFields,
|
||||
} from './descriptions/CollectionDescription';
|
||||
|
||||
import {
|
||||
eventFields,
|
||||
eventOperations,
|
||||
} from './descriptions/EventDescription';
|
||||
|
||||
import {
|
||||
GroupCreationAdditionalFields,
|
||||
groupFields,
|
||||
groupOperations,
|
||||
GroupUpdateFields,
|
||||
} from './descriptions/GroupDescription';
|
||||
|
||||
import {
|
||||
MemberCreationAdditionalFields,
|
||||
memberFields,
|
||||
memberOperations,
|
||||
MemberUpdateFields,
|
||||
} from './descriptions/MemberDescription';
|
||||
|
||||
import {
|
||||
isEmpty,
|
||||
partialRight,
|
||||
} from 'lodash';
|
||||
|
||||
export class Bitwarden implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Bitwarden',
|
||||
name: 'bitwarden',
|
||||
icon: 'file:bitwarden.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume the Bitwarden API',
|
||||
defaults: {
|
||||
name: 'Bitwarden',
|
||||
color: '#175DDC',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'bitwardenApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Collection',
|
||||
value: 'collection',
|
||||
},
|
||||
{
|
||||
name: 'Event',
|
||||
value: 'event',
|
||||
},
|
||||
{
|
||||
name: 'Group',
|
||||
value: 'group',
|
||||
},
|
||||
{
|
||||
name: 'Member',
|
||||
value: 'member',
|
||||
},
|
||||
],
|
||||
default: 'collection',
|
||||
description: 'Resource to consume',
|
||||
},
|
||||
...collectionOperations,
|
||||
...collectionFields,
|
||||
...eventOperations,
|
||||
...eventFields,
|
||||
...groupOperations,
|
||||
...groupFields,
|
||||
...memberOperations,
|
||||
...memberFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
async getGroups(this: ILoadOptionsFunctions) {
|
||||
return await loadResource.call(this, 'groups');
|
||||
},
|
||||
|
||||
async getCollections(this: ILoadOptionsFunctions) {
|
||||
return await loadResource.call(this, 'collections');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
let responseData;
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const token = await getAccessToken.call(this);
|
||||
const bitwardenApiRequest = partialRight(tokenlessBitwardenApiRequest, token);
|
||||
const handleGetAll = partialRight(tokenlessHandleGetAll, token);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
if (resource === 'collection') {
|
||||
|
||||
// *********************************************************************
|
||||
// collection
|
||||
// *********************************************************************
|
||||
|
||||
if (operation === 'delete') {
|
||||
|
||||
// ----------------------------------
|
||||
// collection: delete
|
||||
// ----------------------------------
|
||||
|
||||
const id = this.getNodeParameter('collectionId', i);
|
||||
const endpoint = `/public/collections/${id}`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'DELETE', endpoint, {}, {});
|
||||
responseData = { success: true };
|
||||
|
||||
} else if (operation === 'get') {
|
||||
|
||||
// ----------------------------------
|
||||
// collection: get
|
||||
// ----------------------------------
|
||||
|
||||
const id = this.getNodeParameter('collectionId', i);
|
||||
const endpoint = `/public/collections/${id}`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'GET', endpoint, {}, {});
|
||||
|
||||
} else if (operation === 'getAll') {
|
||||
|
||||
// ----------------------------------
|
||||
// collection: getAll
|
||||
// ----------------------------------
|
||||
|
||||
const endpoint = '/public/collections';
|
||||
responseData = await handleGetAll.call(this, i, 'GET', endpoint, {}, {});
|
||||
|
||||
} else if (operation === 'update') {
|
||||
|
||||
// ----------------------------------
|
||||
// collection: update
|
||||
// ----------------------------------
|
||||
|
||||
const updateFields = this.getNodeParameter('updateFields', i) as CollectionUpdateFields;
|
||||
|
||||
if (isEmpty(updateFields)) {
|
||||
throw new Error(`Please enter at least one field to update for the ${resource}.`);
|
||||
}
|
||||
|
||||
const { groups, externalId } = updateFields;
|
||||
|
||||
const body = {} as IDataObject;
|
||||
|
||||
if (groups) {
|
||||
body.groups = groups.map((groupId) => ({
|
||||
id: groupId,
|
||||
ReadOnly: false,
|
||||
}));
|
||||
}
|
||||
|
||||
if (externalId) {
|
||||
body.externalId = externalId;
|
||||
}
|
||||
|
||||
const id = this.getNodeParameter('collectionId', i);
|
||||
const endpoint = `/public/collections/${id}`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'PUT', endpoint, {}, body);
|
||||
|
||||
}
|
||||
|
||||
} else if (resource === 'event') {
|
||||
|
||||
// *********************************************************************
|
||||
// event
|
||||
// *********************************************************************
|
||||
|
||||
if (operation === 'getAll') {
|
||||
|
||||
// ----------------------------------
|
||||
// event: getAll
|
||||
// ----------------------------------
|
||||
|
||||
const filters = this.getNodeParameter('filters', i) as IDataObject;
|
||||
const qs = isEmpty(filters) ? {} : filters;
|
||||
const endpoint = '/public/events';
|
||||
responseData = await handleGetAll.call(this, i, 'GET', endpoint, qs, {});
|
||||
|
||||
}
|
||||
|
||||
} else if (resource === 'group') {
|
||||
|
||||
// *********************************************************************
|
||||
// group
|
||||
// *********************************************************************
|
||||
|
||||
if (operation === 'create') {
|
||||
|
||||
// ----------------------------------
|
||||
// group: create
|
||||
// ----------------------------------
|
||||
|
||||
const body = {
|
||||
name: this.getNodeParameter('name', i),
|
||||
AccessAll: this.getNodeParameter('accessAll', i),
|
||||
} as IDataObject;
|
||||
|
||||
const {
|
||||
collections,
|
||||
externalId,
|
||||
} = this.getNodeParameter('additionalFields', i) as GroupCreationAdditionalFields;
|
||||
|
||||
if (collections) {
|
||||
body.collections = collections.map((collectionId) => ({
|
||||
id: collectionId,
|
||||
ReadOnly: false,
|
||||
}));
|
||||
}
|
||||
|
||||
if (externalId) {
|
||||
body.externalId = externalId;
|
||||
}
|
||||
|
||||
const endpoint = '/public/groups';
|
||||
responseData = await bitwardenApiRequest.call(this, 'POST', endpoint, {}, body);
|
||||
|
||||
} else if (operation === 'delete') {
|
||||
|
||||
// ----------------------------------
|
||||
// group: delete
|
||||
// ----------------------------------
|
||||
|
||||
const id = this.getNodeParameter('groupId', i);
|
||||
const endpoint = `/public/groups/${id}`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'DELETE', endpoint, {}, {});
|
||||
responseData = { success: true };
|
||||
|
||||
} else if (operation === 'get') {
|
||||
|
||||
// ----------------------------------
|
||||
// group: get
|
||||
// ----------------------------------
|
||||
|
||||
const id = this.getNodeParameter('groupId', i);
|
||||
const endpoint = `/public/groups/${id}`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'GET', endpoint, {}, {});
|
||||
|
||||
} else if (operation === 'getAll') {
|
||||
|
||||
// ----------------------------------
|
||||
// group: getAll
|
||||
// ----------------------------------
|
||||
|
||||
const endpoint = '/public/groups';
|
||||
responseData = await handleGetAll.call(this, i, 'GET', endpoint, {}, {});
|
||||
|
||||
} else if (operation === 'getMembers') {
|
||||
|
||||
// ----------------------------------
|
||||
// group: getMembers
|
||||
// ----------------------------------
|
||||
|
||||
const id = this.getNodeParameter('groupId', i);
|
||||
const endpoint = `/public/groups/${id}/member-ids`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'GET', endpoint, {}, {});
|
||||
responseData = responseData.map((memberId: string) => ({ memberId }));
|
||||
|
||||
} else if (operation === 'update') {
|
||||
|
||||
// ----------------------------------
|
||||
// group: update
|
||||
// ----------------------------------
|
||||
|
||||
const groupId = this.getNodeParameter('groupId', i);
|
||||
|
||||
const updateFields = this.getNodeParameter('updateFields', i) as GroupUpdateFields;
|
||||
|
||||
if (isEmpty(updateFields)) {
|
||||
throw new Error(`Please enter at least one field to update for the ${resource}.`);
|
||||
}
|
||||
|
||||
// set defaults for `name` and `accessAll`, required by Bitwarden but optional in n8n
|
||||
|
||||
let { name, accessAll } = updateFields;
|
||||
|
||||
if (name === undefined) {
|
||||
responseData = await bitwardenApiRequest.call(this, 'GET', `/public/groups/${groupId}`, {}, {}) as { name: string };
|
||||
name = responseData.name;
|
||||
}
|
||||
|
||||
if (accessAll === undefined) {
|
||||
accessAll = false;
|
||||
}
|
||||
|
||||
const body = {
|
||||
name,
|
||||
AccessAll: accessAll,
|
||||
} as IDataObject;
|
||||
|
||||
const { collections, externalId } = updateFields;
|
||||
|
||||
if (collections) {
|
||||
body.collections = collections.map((collectionId) => ({
|
||||
id: collectionId,
|
||||
ReadOnly: false,
|
||||
}));
|
||||
}
|
||||
|
||||
if (externalId) {
|
||||
body.externalId = externalId;
|
||||
}
|
||||
|
||||
const endpoint = `/public/groups/${groupId}`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'PUT', endpoint, {}, body);
|
||||
|
||||
} else if (operation === 'updateMembers') {
|
||||
|
||||
// ----------------------------------
|
||||
// group: updateMembers
|
||||
// ----------------------------------
|
||||
|
||||
const memberIds = this.getNodeParameter('memberIds', i) as string;
|
||||
|
||||
const body = {
|
||||
memberIds: memberIds.includes(',') ? memberIds.split(',') : [memberIds],
|
||||
};
|
||||
|
||||
const groupId = this.getNodeParameter('groupId', i);
|
||||
const endpoint = `/public/groups/${groupId}/member-ids`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'PUT', endpoint, {}, body);
|
||||
responseData = { success: true };
|
||||
}
|
||||
|
||||
} else if (resource === 'member') {
|
||||
|
||||
// *********************************************************************
|
||||
// member
|
||||
// *********************************************************************
|
||||
|
||||
if (operation === 'create') {
|
||||
|
||||
// ----------------------------------
|
||||
// member: create
|
||||
// ----------------------------------
|
||||
|
||||
const body = {
|
||||
email: this.getNodeParameter('email', i),
|
||||
type: this.getNodeParameter('type', i),
|
||||
AccessAll: this.getNodeParameter('accessAll', i),
|
||||
} as IDataObject;
|
||||
|
||||
const {
|
||||
collections,
|
||||
externalId,
|
||||
} = this.getNodeParameter('additionalFields', i) as MemberCreationAdditionalFields;
|
||||
|
||||
if (collections) {
|
||||
body.collections = collections.map((collectionId) => ({
|
||||
id: collectionId,
|
||||
ReadOnly: false,
|
||||
}));
|
||||
}
|
||||
|
||||
if (externalId) {
|
||||
body.externalId = externalId;
|
||||
}
|
||||
|
||||
const endpoint = '/public/members/';
|
||||
responseData = await bitwardenApiRequest.call(this, 'POST', endpoint, {}, body);
|
||||
|
||||
} else if (operation === 'delete') {
|
||||
|
||||
// ----------------------------------
|
||||
// member: delete
|
||||
// ----------------------------------
|
||||
|
||||
const id = this.getNodeParameter('memberId', i);
|
||||
const endpoint = `/public/members/${id}`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'DELETE', endpoint, {}, {});
|
||||
responseData = { success: true };
|
||||
|
||||
} else if (operation === 'get') {
|
||||
|
||||
// ----------------------------------
|
||||
// member: get
|
||||
// ----------------------------------
|
||||
|
||||
const id = this.getNodeParameter('memberId', i);
|
||||
const endpoint = `/public/members/${id}`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'GET', endpoint, {}, {});
|
||||
|
||||
} else if (operation === 'getAll') {
|
||||
|
||||
// ----------------------------------
|
||||
// member: getAll
|
||||
// ----------------------------------
|
||||
|
||||
const endpoint = '/public/members';
|
||||
responseData = await handleGetAll.call(this, i, 'GET', endpoint, {}, {});
|
||||
|
||||
} else if (operation === 'getGroups') {
|
||||
|
||||
// ----------------------------------
|
||||
// member: getGroups
|
||||
// ----------------------------------
|
||||
|
||||
const id = this.getNodeParameter('memberId', i);
|
||||
const endpoint = `/public/members/${id}/group-ids`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'GET', endpoint, {}, {});
|
||||
responseData = responseData.map((groupId: string) => ({ groupId }));
|
||||
|
||||
} else if (operation === 'update') {
|
||||
|
||||
// ----------------------------------
|
||||
// member: update
|
||||
// ----------------------------------
|
||||
|
||||
const body = {} as IDataObject;
|
||||
|
||||
const updateFields = this.getNodeParameter('updateFields', i) as MemberUpdateFields;
|
||||
|
||||
if (isEmpty(updateFields)) {
|
||||
throw new Error(`Please enter at least one field to update for the ${resource}.`);
|
||||
}
|
||||
|
||||
const { accessAll, collections, externalId, type } = updateFields;
|
||||
|
||||
if (accessAll !== undefined) {
|
||||
body.AccessAll = accessAll;
|
||||
}
|
||||
|
||||
if (collections) {
|
||||
body.collections = collections.map((collectionId) => ({
|
||||
id: collectionId,
|
||||
ReadOnly: false,
|
||||
}));
|
||||
}
|
||||
|
||||
if (externalId) {
|
||||
body.externalId = externalId;
|
||||
}
|
||||
|
||||
if (type !== undefined) {
|
||||
body.Type = type;
|
||||
}
|
||||
|
||||
const id = this.getNodeParameter('memberId', i);
|
||||
const endpoint = `/public/members/${id}`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'PUT', endpoint, {}, body);
|
||||
|
||||
} else if (operation === 'updateGroups') {
|
||||
|
||||
// ----------------------------------
|
||||
// member: updateGroups
|
||||
// ----------------------------------
|
||||
|
||||
const groupIds = this.getNodeParameter('groupIds', i) as string;
|
||||
|
||||
const body = {
|
||||
groupIds: groupIds.includes(',') ? groupIds.split(',') : [groupIds],
|
||||
};
|
||||
|
||||
const memberId = this.getNodeParameter('memberId', i);
|
||||
const endpoint = `/public/members/${memberId}/group-ids`;
|
||||
responseData = await bitwardenApiRequest.call(this, 'PUT', endpoint, {}, body);
|
||||
responseData = { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
Array.isArray(responseData)
|
||||
? returnData.push(...responseData)
|
||||
: returnData.push(responseData);
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue