🔀 Merge branch 'master' into add-tags

This commit is contained in:
Ben Hesseldieck 2021-04-06 12:29:09 +02:00
commit d65d2b0f73
290 changed files with 17723 additions and 2000 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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?

View file

@ -21,7 +21,7 @@ import {
WorkflowCredentials,
WorkflowHelpers,
WorkflowRunner,
} from "../src";
} from '../src';
export class Execute extends Command {
@ -127,7 +127,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;

View file

@ -140,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.');

View file

@ -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.');

View file

@ -56,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 {
@ -69,10 +81,6 @@ export class ImportCredentialsCommand extends Command {
throw new Error(`File does not seem to contain credentials.`);
}
const encryptionKey = await UserSettings.getEncryptionKey();
if (encryptionKey === undefined) {
throw new Error('No encryption key got found to encrypt the credentials!');
}
for (i = 0; i < fileContents.length; i++) {
if (typeof fileContents[i].data === 'object') {
// plain data / decrypted input. Should be encrypted first.

View file

@ -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;
@ -168,7 +172,7 @@ export class Start extends Command {
const redisDB = config.get('queue.bull.redis.db');
const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold');
let lastTimer = 0, cumulativeTimeout = 0;
const settings = {
retryStrategy: (times: number): number | null => {
const now = Date.now();
@ -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);
}
}
@ -200,7 +204,7 @@ export class Start extends Command {
if (redisDB) {
settings.db = redisDB;
}
// This connection is going to be our heartbeat
// IORedis automatically pings redis and tries to reconnect
// We will be using the retryStrategy above
@ -215,13 +219,13 @@ export class Start extends Command {
}
});
}
const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType;
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 = '';

View file

@ -9,7 +9,7 @@ import {
import {
Db,
GenericHelpers,
} from "../../src";
} from '../../src';
export class UpdateWorkflowCommand extends Command {

View file

@ -17,7 +17,7 @@ import {
NodeTypes,
TestWebhooks,
WebhookServer,
} from "../src";
} from '../src';
import { IDataObject } from 'n8n-workflow';
@ -98,7 +98,7 @@ export class Webhook extends Command {
// Wrap that the process does not close but we can still use async
await (async () => {
if (config.get('executions.mode') !== 'queue') {
/**
/**
* It is technically possible to run without queues but
* there are 2 known bugs when running in this mode:
* - Executions list will be problematic as the main process
@ -154,7 +154,7 @@ export class Webhook extends Command {
const redisDB = config.get('queue.bull.redis.db');
const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold');
let lastTimer = 0, cumulativeTimeout = 0;
const settings = {
retryStrategy: (times: number): number | null => {
const now = Date.now();
@ -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);
}
}
@ -186,7 +186,7 @@ export class Webhook extends Command {
if (redisDB) {
settings.db = redisDB;
}
// This connection is going to be our heartbeat
// IORedis automatically pings redis and tries to reconnect
// We will be using the retryStrategy above
@ -201,7 +201,7 @@ export class Webhook extends Command {
}
});
}
await WebhookServer.start();
// Start to get active workflows and run their triggers

View file

@ -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);
}
}

View file

@ -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',
@ -471,7 +485,7 @@ const config = convict({
doc: 'Disable production webhooks from main process. This helps ensures no http traffic load to main process when using webhook-specific processes.',
},
skipWebhoooksDeregistrationOnShutdown: {
/**
/**
* Longer explanation: n8n deregisters webhooks on shutdown / deactivation
* and registers on startup / activation. If we skip
* deactivation on shutdown, webhooks will remain active on 3rd party services.

View file

@ -1,137 +1,138 @@
{
"name": "n8n",
"version": "0.110.3",
"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/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",
"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/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",
"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.64.0",
"n8n-editor-ui": "~0.80.0",
"n8n-nodes-base": "~0.107.3",
"n8n-workflow": "~0.53.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"
]
}
}

View file

@ -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) {

View file

@ -95,10 +95,10 @@ function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDat
for (const key of configKeyParts) {
if (configSchema[key] === undefined) {
throw new Error(`Key "${key}" of ConfigKey "${configKey}" does not exist`);
} else if ((configSchema[key]! as IDataObject).properties === undefined) {
} else if ((configSchema[key]! as IDataObject)._cvtProperties === undefined) {
configSchema = configSchema[key] as IDataObject;
} else {
configSchema = (configSchema[key] as IDataObject).properties as IDataObject;
configSchema = (configSchema[key] as IDataObject)._cvtProperties as IDataObject;
}
}
return configSchema;
@ -114,7 +114,8 @@ function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDat
export async function getConfigValue(configKey: string): Promise<string | boolean | number | undefined> {
// Get the environment variable
const configSchema = config.getSchema();
const currentSchema = extractSchemaForKey(configKey, configSchema.properties as IDataObject);
// @ts-ignore
const currentSchema = extractSchemaForKey(configKey, configSchema._cvtProperties as IDataObject);
// Check if environment variable is defined for config key
if (currentSchema.env === undefined) {
// No environment variable defined, so return value from config
@ -152,7 +153,8 @@ export async function getConfigValue(configKey: string): Promise<string | boolea
export function getConfigValueSync(configKey: string): string | boolean | number | undefined {
// Get the environment variable
const configSchema = config.getSchema();
const currentSchema = extractSchemaForKey(configKey, configSchema.properties as IDataObject);
// @ts-ignore
const currentSchema = extractSchemaForKey(configKey, configSchema._cvtProperties as IDataObject);
// Check if environment variable is defined for config key
if (currentSchema.env === undefined) {
// No environment variable defined, so return value from config

View file

@ -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();
},
@ -454,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
@ -603,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;
@ -649,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);
@ -658,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,
@ -1728,7 +1748,7 @@ class App {
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished,
};
return returnData;
}

View file

@ -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);

View file

@ -230,54 +230,64 @@ 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);
},
],
};
@ -343,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;
}
@ -389,6 +399,77 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
}
/**
* 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.
*
* @returns {IWorkflowExecuteHooks}
*/
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';
@ -544,6 +625,7 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
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;
@ -603,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

View file

@ -346,7 +346,27 @@ 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);
});

View file

@ -41,8 +41,18 @@ export class WorkflowRunnerProcess {
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;
@ -111,7 +121,15 @@ export class WorkflowRunnerProcess {
resolve(executionId);
};
});
const result: IRun = await executeWorkflowFunction(workflowInfo, additionalData, inputData, executionId, workflowData, runData);
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);
@ -174,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: [

View file

@ -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');
}
}

View file

@ -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';
import { CreateTagEntity1617268711084 } from './1617268711084-CreateTagEntity';
export const mysqlMigrations = [
@ -11,5 +12,6 @@ export const mysqlMigrations = [
CreateIndexStoppedAt1594902918301,
AddWebhookId1611149998770,
MakeStoppedAtNullable1607431743767,
ChangeDataSize1615306975123,
CreateTagEntity1617268711084,
];

View file

@ -1,74 +1,74 @@
{
"name": "n8n-core",
"version": "0.64.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.53.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"
]
}
}

View file

@ -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)];
}

View file

@ -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[];

View file

@ -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
@ -532,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 => {
@ -544,6 +548,9 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio
getMode: (): WorkflowExecuteMode => {
return mode;
},
getActivationMode: (): WorkflowActivateMode => {
return activation;
},
getNode: () => {
return getNode(node);
},
@ -595,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 => {
@ -610,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;
@ -907,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 {
@ -916,6 +926,9 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio
getMode: (): WorkflowExecuteMode => {
return mode;
},
getActivationMode: (): WorkflowActivateMode => {
return activation;
},
getNode: () => {
return getNode(node);
},

View file

@ -557,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;
@ -722,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;
}

View file

@ -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

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.80.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.53.0",
"n8n-workflow": "~0.55.0",
"node-sass": "^4.12.0",
"normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1",

View file

@ -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>

View file

@ -175,6 +175,10 @@ import {
IDataObject,
} from 'n8n-workflow';
import {
range as _range,
} from 'lodash';
import mixins from 'vue-typed-mixins';
export default mixins(
@ -433,8 +437,24 @@ export default mixins(
this.$store.commit('setActiveExecutions', results[1]);
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);
@ -464,6 +484,7 @@ export default mixins(
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> {

View file

@ -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: {

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -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"
}
}

View 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: '',
},
];
}

View file

@ -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',

View 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: '',
},
];
}

View file

@ -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',
},
];
}

View file

@ -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',
},
];
}

View 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/.',
},
];
}

View file

@ -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(' '),
},
];
}

View file

@ -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.',
},
];
}

View 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: '',
},
];
}

View 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: '',
},
];
}

View file

@ -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',

View file

@ -41,5 +41,11 @@ export class TheHiveApi implements ICredentialType {
},
],
},
{
displayName: 'Ignore SSL Issues',
name: 'allowUnauthorizedCerts',
type: 'boolean' as NodePropertyTypes,
default: false,
},
];
}

View 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',
},
],
},
];
}

View 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,
};
}
}

View file

@ -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 {

View file

@ -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,
};

View file

@ -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"]}}',

View file

@ -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

View 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

View 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)];
}
}

View 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),
],
};
}
}

View 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[];

View file

@ -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[];

View 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[];

View 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;
}

View 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[];

View 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

View file

@ -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`;
}

View file

@ -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) {

View 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/"
}
]
}
}

View 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)];
}
}

View 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

View file

@ -303,7 +303,7 @@ export class Bitwarden implements INodeType {
// group: update
// ----------------------------------
const body = {} as IDataObject;
const groupId = this.getNodeParameter('groupId', i);
const updateFields = this.getNodeParameter('updateFields', i) as GroupUpdateFields;
@ -311,7 +311,25 @@ export class Bitwarden implements INodeType {
throw new Error(`Please enter at least one field to update for the ${resource}.`);
}
const { name, collections, externalId, accessAll } = updateFields;
// 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) => ({
@ -320,20 +338,11 @@ export class Bitwarden implements INodeType {
}));
}
if (name) {
body.name = name;
}
if (externalId) {
body.externalId = externalId;
}
if (accessAll !== undefined) {
body.AccessAll = accessAll;
}
const id = this.getNodeParameter('groupId', i);
const endpoint = `/public/groups/${id}`;
const endpoint = `/public/groups/${groupId}`;
responseData = await bitwardenApiRequest.call(this, 'PUT', endpoint, {}, body);
} else if (operation === 'updateMembers') {

View file

@ -157,9 +157,9 @@ export async function loadResource(
const { data } = await bitwardenApiRequest.call(this, 'GET', endpoint, {}, {}, token);
data.forEach(({ id, name }: { id: string, name: string }) => {
data.forEach(({ id, name, externalId }: { id: string, name: string, externalId?: string }) => {
returnData.push({
name: name || id,
name: externalId || name || id,
value: id,
});
});

View file

@ -203,12 +203,12 @@ export const groupFields = [
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Access All',
name: 'accessAll',
type: 'boolean',
default: false,
description: 'Allow this group to access all collections within the organization, instead of only its associated collections.<br>If set to true, this option overrides any collection assignments.',
},
{
displayName: 'Collections',

View file

@ -1,19 +1,23 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
clearbitApiRequest,
} from './GenericFunctions';
import {
companyFields,
companyOperations,
} from './CompanyDescription';
import {
personFields,
personOperations,
@ -23,7 +27,7 @@ export class Clearbit implements INodeType {
description: INodeTypeDescription = {
displayName: 'Clearbit',
name: 'clearbit',
icon: 'file:clearbit.png',
icon: 'file:clearbit.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}',
@ -109,7 +113,7 @@ export class Clearbit implements INodeType {
if (additionalFields.facebook) {
qs.facebook = additionalFields.facebook as string;
}
responseData = await clearbitApiRequest.call(this, 'GET', resource, '/v2/people/find', {}, qs);
responseData = await clearbitApiRequest.call(this, 'GET', `${resource}-stream`, '/v2/people/find', {}, qs);
}
}
if (resource === 'company') {
@ -129,7 +133,7 @@ export class Clearbit implements INodeType {
if (additionalFields.facebook) {
qs.facebook = additionalFields.facebook as string;
}
responseData = await clearbitApiRequest.call(this, 'GET', resource, '/v2/companies/find', {}, qs);
responseData = await clearbitApiRequest.call(this, 'GET', `${resource}-stream`, '/v2/companies/find', {}, qs);
}
if (operation === 'autocomplete') {
const name = this.getNodeParameter('name', i) as string;

View file

@ -13,16 +13,16 @@ export const companyOperations = [
},
},
options: [
{
name: 'Enrich',
value: 'enrich',
description: 'Look up person and company data based on an email or domain',
},
{
name: 'Autocomplete',
value: 'autocomplete',
description: 'Auto-complete company names and retrieve logo and domain',
},
{
name: 'Enrich',
value: 'enrich',
description: 'Look up person and company data based on an email or domain',
},
],
default: 'enrich',
description: 'The operation to perform.',
@ -31,9 +31,9 @@ export const companyOperations = [
export const companyFields = [
/* -------------------------------------------------------------------------- */
/* company:enrich */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* company:enrich */
/* -------------------------------------------------------------------------- */
{
displayName: 'Domain',
name: 'domain',
@ -99,25 +99,26 @@ export const companyFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* company:autocomplete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'company',
],
operation: [
'autocomplete',
],
/* -------------------------------------------------------------------------- */
/* company:autocomplete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'company',
],
operation: [
'autocomplete',
],
},
},
description: 'Name is the partial name of the company.',
},
description: 'Name is the partial name of the company.',
},
] as INodeProperties[];

View file

@ -1,11 +1,17 @@
import { OptionsWithUri } from 'request';
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import { IDataObject } from 'n8n-workflow';
import {
IDataObject,
} from 'n8n-workflow';
export async function clearbitApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, api: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('clearbitApi');
@ -13,11 +19,11 @@ export async function clearbitApiRequest(this: IHookFunctions | IExecuteFunction
throw new Error('No credentials got returned!');
}
let options: OptionsWithUri = {
headers: { Authorization: `Bearer ${credentials.apiKey}`},
headers: { Authorization: `Bearer ${credentials.apiKey}` },
method,
qs,
body,
uri: uri ||`https://${api}-stream.clearbit.com${resource}`,
uri: uri || `https://${api}.clearbit.com${resource}`,
json: true,
};
options = Object.assign({}, options, option);

View file

@ -26,9 +26,9 @@ export const personOperations = [
export const personFields = [
/* -------------------------------------------------------------------------- */
/* person:enrich */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* person:enrich */
/* -------------------------------------------------------------------------- */
{
displayName: 'Email',
name: 'email',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 634 B

View file

@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72">
<defs>
<linearGradient id="color-a" x1="50%" x2="100%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#DEF2FE"/>
<stop offset="100%" stop-color="#DBF1FE"/>
</linearGradient>
<linearGradient id="color-b" x1="0%" x2="50%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#57BCFD"/>
<stop offset="100%" stop-color="#51B5FD"/>
</linearGradient>
<linearGradient id="color-c" x1="37.5%" x2="62.5%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#1CA7FD"/>
<stop offset="100%" stop-color="#148CFC"/>
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<path fill="url(#color-a)" d="M72,36 L72,52.770861 L71.9958819,53.6375008 C71.9369091,59.6728148 71.2457056,61.9905403 69.9965047,64.3263428 C68.6892015,66.7707872 66.7707872,68.6892015 64.3263428,69.9965047 L64.0001583,70.1671829 C61.6579559,71.3643165 59.1600786,72 52.770861,72 L36,72 L36,36 L72,36 Z"/>
<path fill="url(#color-b)" d="M64.3263428,2.00349528 C66.7707872,3.31079847 68.6892015,5.22921278 69.9965047,7.67365722 L70.1671829,7.99984171 C71.3643165,10.3420441 72,12.8399214 72,19.229139 L72,36 L36,36 L36,0 L52.770861,0 C59.4572515,0 61.8818983,0.696192084 64.3263428,2.00349528 Z"/>
<path fill="url(#color-c)" d="M36,0 L36,72 L19.229139,72 L18.3624992,71.9958819 C12.3271852,71.9369091 10.0094597,71.2457056 7.67365722,69.9965047 C5.22921278,68.6892015 3.31079847,66.7707872 2.00349528,64.3263428 L1.83281705,64.0001583 C0.635683537,61.6579559 0,59.1600786 0,52.770861 L0,19.229139 C0,12.5427485 0.696192084,10.1181017 2.00349528,7.67365722 C3.31079847,5.22921278 5.22921278,3.31079847 7.67365722,2.00349528 L7.99984171,1.83281705 C10.3420441,0.635683537 12.8399214,0 19.229139,0 L36,0 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const checklistOperations = [
{
@ -38,9 +38,9 @@ export const checklistOperations = [
export const checklistFields = [
/* -------------------------------------------------------------------------- */
/* checklist:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* checklist:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'task',
@ -75,9 +75,9 @@ export const checklistFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* checklist:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* checklist:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Checklist ID',
name: 'checklist',
@ -95,9 +95,9 @@ export const checklistFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* checklist:update */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* checklist:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Checklist ID',
name: 'checklist',

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const checklistItemOperations = [
{
@ -38,9 +38,9 @@ export const checklistItemOperations = [
export const checklistItemFields = [
/* -------------------------------------------------------------------------- */
/* checklistItem:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* checklistItem:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Checklist ID',
name: 'checklist',
@ -100,9 +100,10 @@ export const checklistItemFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* checklistItem:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* checklistItem:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Checklist ID',
name: 'checklist',
@ -137,9 +138,10 @@ export const checklistItemFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* checklistItem:update */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* checklistItem:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Checklist ID',
name: 'checklist',

View file

@ -57,6 +57,21 @@ import {
taskOperations,
} from './TaskDescription';
import {
taskListFields,
taskListOperations,
} from './TaskListDescription';
import {
taskTagFields,
taskTagOperations,
} from './TaskTagDescription';
import {
spaceTagFields,
spaceTagOperations,
} from './SpaceTagDescription';
import {
taskDependencyFields,
taskDependencyOperations,
@ -91,7 +106,7 @@ export class ClickUp implements INodeType {
description: INodeTypeDescription = {
displayName: 'ClickUp',
name: 'clickUp',
icon: 'file:clickup.png',
icon: 'file:clickup.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}',
@ -180,10 +195,22 @@ export class ClickUp implements INodeType {
name: 'List',
value: 'list',
},
{
name: 'Space Tag',
value: 'spaceTag',
},
{
name: 'Task',
value: 'task',
},
{
name: 'Task List',
value: 'taskList',
},
{
name: 'Task Tag',
value: 'taskTag',
},
{
name: 'Task Dependency',
value: 'taskDependency',
@ -221,6 +248,15 @@ export class ClickUp implements INodeType {
// GUEST
// ...guestOperations,
// ...guestFields,
// TASK TAG
...taskTagOperations,
...taskTagFields,
// TASK LIST
...taskListOperations,
...taskListFields,
// SPACE TAG
...spaceTagOperations,
...spaceTagFields,
// TASK
...taskOperations,
...taskFields,
@ -1022,6 +1058,40 @@ export class ClickUp implements INodeType {
responseData = { success: true };
}
}
if (resource === 'taskTag') {
if (operation === 'add') {
const taskId = this.getNodeParameter('taskId', i) as string;
const name = this.getNodeParameter('tagName', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const qs: IDataObject = {};
Object.assign(qs, additionalFields);
responseData = await clickupApiRequest.call(this, 'POST', `/task/${taskId}/tag/${name}`, {}, qs);
responseData = { success: true };
}
if (operation === 'remove') {
const taskId = this.getNodeParameter('taskId', i) as string;
const name = this.getNodeParameter('tagName', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const qs: IDataObject = {};
Object.assign(qs, additionalFields);
responseData = await clickupApiRequest.call(this, 'DELETE', `/task/${taskId}/tag/${name}`, {}, qs);
responseData = { success: true };
}
}
if (resource === 'taskList') {
if (operation === 'add') {
const taskId = this.getNodeParameter('taskId', i) as string;
const listId = this.getNodeParameter('listId', i) as string;
responseData = await clickupApiRequest.call(this, 'POST', `/list/${listId}/task/${taskId}`);
responseData = { success: true };
}
if (operation === 'remove') {
const taskId = this.getNodeParameter('taskId', i) as string;
const listId = this.getNodeParameter('listId', i) as string;
responseData = await clickupApiRequest.call(this, 'DELETE', `/list/${listId}/task/${taskId}`);
responseData = { success: true };
}
}
if (resource === 'taskDependency') {
if (operation === 'create') {
const taskId = this.getNodeParameter('task', i) as string;
@ -1195,6 +1265,55 @@ export class ClickUp implements INodeType {
}
}
if (resource === 'spaceTag') {
if (operation === 'create') {
const spaceId = this.getNodeParameter('space', i) as string;
const name = this.getNodeParameter('name', i) as string;
const foregroundColor = this.getNodeParameter('foregroundColor', i) as string;
const backgroundColor = this.getNodeParameter('backgroundColor', i) as string;
const body: IDataObject = {
tag: {
name,
tag_bg: backgroundColor,
tag_fg: foregroundColor,
},
};
responseData = await clickupApiRequest.call(this, 'POST', `/space/${spaceId}/tag`, body);
responseData = { success: true };
}
if (operation === 'delete') {
const spaceId = this.getNodeParameter('space', i) as string;
const name = this.getNodeParameter('name', i) as string;
responseData = await clickupApiRequest.call(this, 'DELETE', `/space/${spaceId}/tag/${name}`);
responseData = { success: true };
}
if (operation === 'getAll') {
const spaceId = this.getNodeParameter('space', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
responseData = await clickupApiRequest.call(this, 'GET', `/space/${spaceId}/tag`);
responseData = responseData.tags;
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.splice(0, limit);
}
}
if (operation === 'update') {
const spaceId = this.getNodeParameter('space', i) as string;
const tagName = this.getNodeParameter('name', i) as string;
const newTagName = this.getNodeParameter('newName', i) as string;
const foregroundColor = this.getNodeParameter('foregroundColor', i) as string;
const backgroundColor = this.getNodeParameter('backgroundColor', i) as string;
const body: IDataObject = {
tag: {
name: newTagName,
tag_bg: backgroundColor,
tag_fg: foregroundColor,
},
};
await clickupApiRequest.call(this, 'PUT', `/space/${spaceId}/tag/${tagName}`, body);
responseData = { success: true };
}
}
if (resource === 'list') {
if (operation === 'create') {
const spaceId = this.getNodeParameter('space', i) as string;

View file

@ -16,13 +16,15 @@ import {
clickupApiRequest,
} from './GenericFunctions';
import { createHmac } from 'crypto';
import {
createHmac,
} from 'crypto';
export class ClickUpTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'ClickUp Trigger',
name: 'clickUpTrigger',
icon: 'file:clickup.png',
icon: 'file:clickup.svg',
group: ['trigger'],
version: 1,
description: 'Handle ClickUp events via webhooks (Beta)',
@ -302,16 +304,16 @@ export class ClickUpTrigger implements INodeType {
body.events = '*';
}
if (filters.listId) {
body.list_id = (filters.listId as string).replace('#','');
body.list_id = (filters.listId as string).replace('#', '');
}
if (filters.taskId) {
body.task_id = (filters.taskId as string).replace('#','');
body.task_id = (filters.taskId as string).replace('#', '');
}
if (filters.spaceId) {
body.space_id = (filters.spaceId as string).replace('#','');
body.space_id = (filters.spaceId as string).replace('#', '');
}
if (filters.folderId) {
body.folder_id = (filters.folderId as string).replace('#','');
body.folder_id = (filters.folderId as string).replace('#', '');
}
const { webhook } = await clickupApiRequest.call(this, 'POST', endpoint, body);
webhookData.webhookId = webhook.id;
@ -323,7 +325,7 @@ export class ClickUpTrigger implements INodeType {
const endpoint = `/webhook/${webhookData.webhookId}`;
try {
await clickupApiRequest.call(this, 'DELETE', endpoint);
} catch(error) {
} catch (error) {
return false;
}
delete webhookData.webhookId;

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const commentOperations = [
{
@ -43,9 +43,9 @@ export const commentOperations = [
export const commentFields = [
/* -------------------------------------------------------------------------- */
/* comment:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* comment:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Comment On',
name: 'commentOn',
@ -141,9 +141,10 @@ export const commentFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* comment:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* comment:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Comment ID',
name: 'comment',
@ -161,9 +162,10 @@ export const commentFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* comment:getAll */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* comment:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Comments On',
name: 'commentsOn',
@ -232,9 +234,10 @@ export const commentFields = [
default: 50,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* comment:update */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* comment:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Comment ID',
name: 'comment',

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const folderOperations = [
{
@ -48,9 +48,9 @@ export const folderOperations = [
export const folderFields = [
/* -------------------------------------------------------------------------- */
/* folder:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* folder:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -111,9 +111,10 @@ export const folderFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* folder:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* folder:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -180,9 +181,10 @@ export const folderFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* folder:get */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* folder:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -249,9 +251,10 @@ export const folderFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* folder:getAll */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* folder:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -341,9 +344,10 @@ export const folderFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* folder:update */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* folder:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',

View file

@ -1,6 +1,6 @@
import {
OptionsWithUri,
} from 'request';
} from 'request';
import {
IExecuteFunctions,
@ -13,7 +13,7 @@ import {
import {
IDataObject,
IOAuth2Options,
} from 'n8n-workflow';
} from 'n8n-workflow';
export async function clickupApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUri = {
@ -23,7 +23,7 @@ export async function clickupApiRequest(this: IHookFunctions | IExecuteFunctions
method,
qs,
body,
uri: uri ||`https://api.clickup.com/api/v2${resource}`,
uri: uri || `https://api.clickup.com/api/v2${resource}`,
json: true,
};
@ -51,7 +51,7 @@ export async function clickupApiRequest(this: IHookFunctions | IExecuteFunctions
return await this.helpers.requestOAuth2!.call(this, 'clickUpOAuth2Api', options, oAuth2Options);
}
} catch(error) {
} catch (error) {
let errorMessage = error;
if (error.err) {
errorMessage = error.err;
@ -61,7 +61,7 @@ export async function clickupApiRequest(this: IHookFunctions | IExecuteFunctions
}
export async function clickupApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string ,method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
export async function clickupApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const goalOperations = [
{
@ -48,9 +48,9 @@ export const goalOperations = [
export const goalFields = [
/* -------------------------------------------------------------------------- */
/* goal:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* goal:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -140,9 +140,10 @@ export const goalFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* goal:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* goal:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Goal ID',
name: 'goal',
@ -160,9 +161,10 @@ export const goalFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* goal:get */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* goal:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Goal ID',
name: 'goal',
@ -180,9 +182,10 @@ export const goalFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* goal:getAll */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* goal:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -224,9 +227,10 @@ export const goalFields = [
default: 50,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* goal:update */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* goal:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Goal ID',
name: 'goal',

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const goalKeyResultOperations = [
{
@ -38,9 +38,9 @@ export const goalKeyResultOperations = [
export const goalKeyResultFields = [
/* -------------------------------------------------------------------------- */
/* goalKeyResult:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* goalKeyResult:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Goal ID',
name: 'goal',
@ -178,9 +178,10 @@ export const goalKeyResultFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* goalKeyResult:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* goalKeyResult:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Key Result ID',
name: 'keyResult',
@ -198,9 +199,10 @@ export const goalKeyResultFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* goalKeyResult:update */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* goalKeyResult:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Key Result ID',
name: 'keyResult',

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const guestOperations = [
{
@ -43,9 +43,9 @@ export const guestOperations = [
export const guestFields = [
/* -------------------------------------------------------------------------- */
/* guest:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* guest:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -119,9 +119,10 @@ export const guestFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* guest:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* guest:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -159,9 +160,10 @@ export const guestFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* guest:get */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* guest:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -199,9 +201,10 @@ export const guestFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* guest:update */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* guest:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const listOperations = [
{
@ -58,9 +58,9 @@ export const listOperations = [
export const listFields = [
/* -------------------------------------------------------------------------- */
/* list:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* list:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -233,9 +233,10 @@ export const listFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* list:member */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* list:member */
/* -------------------------------------------------------------------------- */
{
displayName: 'List ID',
name: 'id',
@ -295,9 +296,9 @@ export const listFields = [
default: 50,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* list:customFields */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* list:customFields */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team',
name: 'team',
@ -436,9 +437,10 @@ export const listFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* list:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* list:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -542,9 +544,10 @@ export const listFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* list:get */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* list:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -648,9 +651,10 @@ export const listFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* list:getAll */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* list:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -783,9 +787,10 @@ export const listFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* list:update */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* list:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',

View file

@ -0,0 +1,204 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const spaceTagOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'spaceTag',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a space tag',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a space tag',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all space tags',
},
{
name: 'Update',
value: 'update',
description: 'Update a space tag',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const spaceTagFields = [
/* -------------------------------------------------------------------------- */
/* spaceTag:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Space ID',
name: 'space',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'spaceTag',
],
operation: [
'create',
'delete',
'getAll',
'update',
],
},
},
required: true,
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'spaceTag',
],
operation: [
'create',
],
},
},
required: true,
},
{
displayName: 'Name',
name: 'name',
type: 'options',
typeOptions: {
loadOptionsDependsOn: [
'space',
],
loadOptionsMethod: 'getTags',
},
default: '',
displayOptions: {
show: {
resource: [
'spaceTag',
],
operation: [
'delete',
'update',
],
},
},
required: true,
},
{
displayName: 'New Name',
name: 'newName',
type: 'string',
description: 'New name to set for the tag.',
default: '',
displayOptions: {
show: {
resource: [
'spaceTag',
],
operation: [
'update',
],
},
},
required: true,
},
{
displayName: 'Foreground Color',
name: 'foregroundColor',
type: 'color',
default: '#000000',
displayOptions: {
show: {
resource: [
'spaceTag',
],
operation: [
'create',
'update',
],
},
},
required: true,
},
{
displayName: 'Background Color',
name: 'backgroundColor',
type: 'color',
default: '#000000',
displayOptions: {
show: {
resource: [
'spaceTag',
],
operation: [
'create',
'update',
],
},
},
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'spaceTag',
],
operation: [
'getAll',
],
},
},
default: true,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'spaceTag',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const taskDependencyOperations = [
{
@ -33,9 +33,9 @@ export const taskDependencyOperations = [
export const taskDependencyFields = [
/* -------------------------------------------------------------------------- */
/* taskDependency:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* taskDependency:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'task',
@ -69,9 +69,10 @@ export const taskDependencyFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* taskDependency:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* taskDependency:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'task',

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const taskOperations = [
{
@ -58,9 +58,9 @@ export const taskOperations = [
export const taskFields = [
/* -------------------------------------------------------------------------- */
/* task:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* task:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -352,9 +352,10 @@ export const taskFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* task:update */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* task:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'id',
@ -489,9 +490,10 @@ export const taskFields = [
],
},
/* -------------------------------------------------------------------------- */
/* task:get */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* task:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'id',
@ -510,9 +512,10 @@ export const taskFields = [
},
description: 'Task ID',
},
/* -------------------------------------------------------------------------- */
/* task:getAll */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* task:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -920,9 +923,10 @@ export const taskFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* task:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* task:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'id',
@ -941,9 +945,10 @@ export const taskFields = [
},
description: 'task ID',
},
/* -------------------------------------------------------------------------- */
/* task:member */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* task:member */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'id',
@ -1003,9 +1008,10 @@ export const taskFields = [
default: 50,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* task:setCustomField */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* task:setCustomField */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'task',

View file

@ -0,0 +1,74 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const taskListOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'taskList',
],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add a task to a list',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove a task from a list',
},
],
default: 'add',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const taskListFields = [
/* -------------------------------------------------------------------------- */
/* taskList:add */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'taskId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'taskList',
],
operation: [
'remove',
'add',
],
},
},
required: true,
},
{
displayName: 'List ID',
name: 'listId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'taskList',
],
operation: [
'remove',
'add',
],
},
},
required: true,
},
] as INodeProperties[];

View file

@ -0,0 +1,111 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const taskTagOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'taskTag',
],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add a tag to a task',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove a tag from a task',
},
],
default: 'add',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const taskTagFields = [
/* -------------------------------------------------------------------------- */
/* taskTag:add */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'taskId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'taskTag',
],
operation: [
'remove',
'add',
],
},
},
required: true,
},
{
displayName: 'Tag Name',
name: 'tagName',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'taskTag',
],
operation: [
'remove',
'add',
],
},
},
required: true,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'taskTag',
],
operation: [
'remove',
'add',
],
},
},
options: [
{
displayName: 'Custom Task IDs',
name: 'custom_task_ids',
type: 'boolean',
default: false,
description: `If you want to reference a task by it's custom task id, this value must be true`,
},
{
displayName: 'Team ID',
name: 'team_id',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeams',
},
default: '',
description: `Only used when the parameter is set to custom_task_ids=true`,
},
],
},
] as INodeProperties[];

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const timeEntryTagOperations = [
{
@ -28,7 +28,7 @@ export const timeEntryTagOperations = [
{
name: 'Remove',
value: 'remove',
description:'Remove tag from time entry',
description: 'Remove tag from time entry',
},
],
default: 'add',
@ -38,9 +38,9 @@ export const timeEntryTagOperations = [
export const timeEntryTagFields = [
/* -------------------------------------------------------------------------- */
/* timeEntryTag:getAll */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* timeEntryTag:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -102,9 +102,10 @@ export const timeEntryTagFields = [
default: 5,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* timeEntryTag:add */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* timeEntryTag:add */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',
@ -188,9 +189,10 @@ export const timeEntryTagFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* timeEntryTag:remove */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* timeEntryTag:remove */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'team',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1 @@
<svg width="130" height="155" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="0%" y1="68.01%" y2="68.01%" id="a"><stop stop-color="#8930FD" offset="0%"/><stop stop-color="#49CCF9" offset="100%"/></linearGradient><linearGradient x1="0%" y1="68.01%" y2="68.01%" id="b"><stop stop-color="#FF02F0" offset="0%"/><stop stop-color="#FFC800" offset="100%"/></linearGradient></defs><g fill-rule="nonzero" fill="none"><path d="M.4 119.12l23.81-18.24C36.86 117.39 50.3 125 65.26 125c14.88 0 27.94-7.52 40.02-23.9l24.15 17.8C112 142.52 90.34 155 65.26 155c-25 0-46.87-12.4-64.86-35.88z" fill="url(#a)"/><path fill="url(#b)" d="M65.18 39.84L22.8 76.36 3.21 53.64 65.27.16l61.57 53.52-19.68 22.64z"/></g></svg>

After

Width:  |  Height:  |  Size: 709 B

View file

@ -0,0 +1,704 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
adjustCompanyFields,
adjustLeadFields,
adjustPersonFields,
adjustTaskFields,
copperApiRequest,
handleListing,
} from './GenericFunctions';
import {
companyFields,
companyOperations,
customerSourceFields,
customerSourceOperations,
leadFields,
leadOperations,
opportunityFields,
opportunityOperations,
personFields,
personOperations,
projectFields,
projectOperations,
taskFields,
taskOperations,
userFields,
userOperations,
} from './descriptions';
export class Copper implements INodeType {
description: INodeTypeDescription = {
displayName: 'Copper',
name: 'copper',
icon: 'file:copper.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume the Copper API',
defaults: {
name: 'Copper',
color: '#ff2564',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'copperApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Company',
value: 'company',
},
{
name: 'Customer Source',
value: 'customerSource',
},
{
name: 'Lead',
value: 'lead',
},
{
name: 'Opportunity',
value: 'opportunity',
},
{
name: 'Person',
value: 'person',
},
{
name: 'Project',
value: 'project',
},
{
name: 'Task',
value: 'task',
},
{
name: 'User',
value: 'user',
},
],
default: 'company',
description: 'Resource to consume',
},
...companyOperations,
...companyFields,
...customerSourceOperations,
...customerSourceFields,
...leadOperations,
...leadFields,
...opportunityOperations,
...opportunityFields,
...personOperations,
...personFields,
...projectOperations,
...projectFields,
...taskOperations,
...taskFields,
...userOperations,
...userFields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
let responseData;
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'company') {
// **********************************************************************
// company
// **********************************************************************
if (operation === 'create') {
// ----------------------------------------
// company: create
// ----------------------------------------
// https://developer.copper.com/companies/create-a-new-company.html
const body: IDataObject = {
name: this.getNodeParameter('name', i),
};
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (Object.keys(additionalFields).length) {
Object.assign(body, adjustCompanyFields(additionalFields));
}
responseData = await copperApiRequest.call(this, 'POST', '/companies', body);
} else if (operation === 'delete') {
// ----------------------------------------
// company: delete
// ----------------------------------------
// https://developer.copper.com/companies/delete-a-company.html
const companyId = this.getNodeParameter('companyId', i);
responseData = await copperApiRequest.call(this, 'DELETE', `/companies/${companyId}`);
} else if (operation === 'get') {
// ----------------------------------------
// company: get
// ----------------------------------------
// https://developer.copper.com/companies/fetch-a-company-by-id.html
const companyId = this.getNodeParameter('companyId', i);
responseData = await copperApiRequest.call(this, 'GET', `/companies/${companyId}`);
} else if (operation === 'getAll') {
// ----------------------------------------
// company: getAll
// ----------------------------------------
// https://developer.copper.com/companies/list-companies-search.html
const body: IDataObject = {};
const filterFields = this.getNodeParameter('filterFields', i) as IDataObject;
if (Object.keys(filterFields).length) {
Object.assign(body, filterFields);
}
responseData = await handleListing.call(this, 'POST', '/companies/search', body);
} else if (operation === 'update') {
// ----------------------------------------
// company: update
// ----------------------------------------
// https://developer.copper.com/companies/update-a-company.html
const companyId = this.getNodeParameter('companyId', i);
const body: IDataObject = {};
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
if (Object.keys(updateFields).length) {
Object.assign(body, adjustCompanyFields(updateFields));
}
responseData = await copperApiRequest.call(this, 'PUT', `/companies/${companyId}`, body);
}
} else if (resource === 'customerSource') {
// **********************************************************************
// customerSource
// **********************************************************************
if (operation === 'getAll') {
// ----------------------------------------
// customerSource: getAll
// ----------------------------------------
responseData = await handleListing.call(this, 'GET', '/customer_sources');
}
} else if (resource === 'lead') {
// **********************************************************************
// lead
// **********************************************************************
if (operation === 'create') {
// ----------------------------------------
// lead: create
// ----------------------------------------
// https://developer.copper.com/leads/create-a-new-lead.html
const body: IDataObject = {
name: this.getNodeParameter('name', i),
};
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (Object.keys(additionalFields).length) {
Object.assign(body, adjustLeadFields(additionalFields));
}
responseData = await copperApiRequest.call(this, 'POST', '/leads', body);
} else if (operation === 'delete') {
// ----------------------------------------
// lead: delete
// ----------------------------------------
// https://developer.copper.com/leads/delete-a-lead.html
const leadId = this.getNodeParameter('leadId', i);
responseData = await copperApiRequest.call(this, 'DELETE', `/leads/${leadId}`);
} else if (operation === 'get') {
// ----------------------------------------
// lead: get
// ----------------------------------------
// https://developer.copper.com/leads/fetch-a-lead-by-id.html
const leadId = this.getNodeParameter('leadId', i);
responseData = await copperApiRequest.call(this, 'GET', `/leads/${leadId}`);
} else if (operation === 'getAll') {
// ----------------------------------------
// lead: getAll
// ----------------------------------------
const body: IDataObject = {};
const filterFields = this.getNodeParameter('filterFields', i) as IDataObject;
if (Object.keys(filterFields).length) {
Object.assign(body, filterFields);
}
responseData = await handleListing.call(this, 'POST', '/leads/search', body);
} else if (operation === 'update') {
// ----------------------------------------
// lead: update
// ----------------------------------------
// https://developer.copper.com/leads/update-a-lead.html
const leadId = this.getNodeParameter('leadId', i);
const body: IDataObject = {};
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
if (Object.keys(updateFields).length) {
Object.assign(body, adjustLeadFields(updateFields));
}
responseData = await copperApiRequest.call(this, 'PUT', `/leads/${leadId}`, body);
}
} else if (resource === 'opportunity') {
// **********************************************************************
// opportunity
// **********************************************************************
if (operation === 'create') {
// ----------------------------------------
// opportunity: create
// ----------------------------------------
// https://developer.copper.com/opportunities/create-a-new-opportunity.html
const body: IDataObject = {
name: this.getNodeParameter('name', i),
customer_source_id: this.getNodeParameter('customerSourceId', i),
primary_contact_id: this.getNodeParameter('primaryContactId', i),
};
responseData = await copperApiRequest.call(this, 'POST', '/opportunities', body);
} else if (operation === 'delete') {
// ----------------------------------------
// opportunity: delete
// ----------------------------------------
// https://developer.copper.com/opportunities/delete-an-opportunity.html
const opportunityId = this.getNodeParameter('opportunityId', i);
responseData = await copperApiRequest.call(this, 'DELETE', `/opportunities/${opportunityId}`);
} else if (operation === 'get') {
// ----------------------------------------
// opportunity: get
// ----------------------------------------
// https://developer.copper.com/opportunities/fetch-an-opportunity-by-id.html
const opportunityId = this.getNodeParameter('opportunityId', i);
responseData = await copperApiRequest.call(this, 'GET', `/opportunities/${opportunityId}`);
} else if (operation === 'getAll') {
// ----------------------------------------
// opportunity: getAll
// ----------------------------------------
// https://developer.copper.com/opportunities/list-opportunities-search.html
const body: IDataObject = {};
const filterFields = this.getNodeParameter('filterFields', i) as IDataObject;
if (Object.keys(filterFields).length) {
Object.assign(body, filterFields);
}
responseData = await handleListing.call(this, 'POST', '/opportunities/search', body);
} else if (operation === 'update') {
// ----------------------------------------
// opportunity: update
// ----------------------------------------
// https://developer.copper.com/opportunities/update-an-opportunity.html
const opportunityId = this.getNodeParameter('opportunityId', i);
const body: IDataObject = {};
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
if (Object.keys(updateFields).length) {
Object.assign(body, updateFields);
}
responseData = await copperApiRequest.call(this, 'PUT', `/opportunities/${opportunityId}`, body);
}
} else if (resource === 'person') {
// **********************************************************************
// person
// **********************************************************************
if (operation === 'create') {
// ----------------------------------------
// person: create
// ----------------------------------------
// https://developer.copper.com/people/create-a-new-person.html
const body: IDataObject = {
name: this.getNodeParameter('name', i),
};
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (Object.keys(additionalFields).length) {
Object.assign(body, adjustPersonFields(additionalFields));
}
responseData = await copperApiRequest.call(this, 'POST', '/people', body);
} else if (operation === 'delete') {
// ----------------------------------------
// person: delete
// ----------------------------------------
// https://developer.copper.com/people/delete-a-person.html
const personId = this.getNodeParameter('personId', i);
responseData = await copperApiRequest.call(this, 'DELETE', `/people/${personId}`);
} else if (operation === 'get') {
// ----------------------------------------
// person: get
// ----------------------------------------
// https://developer.copper.com/people/fetch-a-person-by-id.html
const personId = this.getNodeParameter('personId', i);
responseData = await copperApiRequest.call(this, 'GET', `/people/${personId}`);
} else if (operation === 'getAll') {
// ----------------------------------------
// person: getAll
// ----------------------------------------
const body: IDataObject = {};
const filterFields = this.getNodeParameter('filterFields', i) as IDataObject;
if (Object.keys(filterFields).length) {
Object.assign(body, filterFields);
}
responseData = await handleListing.call(this, 'POST', '/people/search', body);
} else if (operation === 'update') {
// ----------------------------------------
// person: update
// ----------------------------------------
// https://developer.copper.com/people/update-a-person.html
const personId = this.getNodeParameter('personId', i);
const body: IDataObject = {};
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
if (Object.keys(updateFields).length) {
Object.assign(body, adjustPersonFields(updateFields));
}
responseData = await copperApiRequest.call(this, 'PUT', `/people/${personId}`, body);
}
} else if (resource === 'project') {
// **********************************************************************
// project
// **********************************************************************
if (operation === 'create') {
// ----------------------------------------
// project: create
// ----------------------------------------
// https://developer.copper.com/projects/create-a-new-project.html
const body: IDataObject = {
name: this.getNodeParameter('name', i),
};
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (Object.keys(additionalFields).length) {
Object.assign(body, additionalFields);
}
responseData = await copperApiRequest.call(this, 'POST', '/projects', body);
} else if (operation === 'delete') {
// ----------------------------------------
// project: delete
// ----------------------------------------
// https://developer.copper.com/projects/delete-a-project.html
const projectId = this.getNodeParameter('projectId', i);
responseData = await copperApiRequest.call(this, 'DELETE', `/projects/${projectId}`);
} else if (operation === 'get') {
// ----------------------------------------
// project: get
// ----------------------------------------
// https://developer.copper.com/projects/fetch-a-project-by-id.html
const projectId = this.getNodeParameter('projectId', i);
responseData = await copperApiRequest.call(this, 'GET', `/projects/${projectId}`);
} else if (operation === 'getAll') {
// ----------------------------------------
// project: getAll
// ----------------------------------------
// https://developer.copper.com/projects/list-projects-search.html
const body: IDataObject = {};
const filterFields = this.getNodeParameter('filterFields', i) as IDataObject;
if (Object.keys(filterFields).length) {
Object.assign(body, filterFields);
}
responseData = await handleListing.call(this, 'POST', '/projects/search', body);
} else if (operation === 'update') {
// ----------------------------------------
// project: update
// ----------------------------------------
// https://developer.copper.com/projects/update-a-project.html
const projectId = this.getNodeParameter('projectId', i);
const body: IDataObject = {};
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
if (Object.keys(updateFields).length) {
Object.assign(body, updateFields);
}
responseData = await copperApiRequest.call(this, 'PUT', `/projects/${projectId}`, body);
}
} else if (resource === 'task') {
// **********************************************************************
// task
// **********************************************************************
if (operation === 'create') {
// ----------------------------------------
// task: create
// ----------------------------------------
// https://developer.copper.com/tasks/create-a-new-task.html
const body: IDataObject = {
name: this.getNodeParameter('name', i),
};
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (Object.keys(additionalFields).length) {
Object.assign(body, additionalFields);
}
responseData = await copperApiRequest.call(this, 'POST', '/tasks', body);
} else if (operation === 'delete') {
// ----------------------------------------
// task: delete
// ----------------------------------------
// https://developer.copper.com/tasks/delete-a-task.html
const taskId = this.getNodeParameter('taskId', i);
responseData = await copperApiRequest.call(this, 'DELETE', `/tasks/${taskId}`);
} else if (operation === 'get') {
// ----------------------------------------
// task: get
// ----------------------------------------
// https://developer.copper.com/tasks/fetch-a-task-by-id.html
const taskId = this.getNodeParameter('taskId', i);
responseData = await copperApiRequest.call(this, 'GET', `/tasks/${taskId}`);
} else if (operation === 'getAll') {
// ----------------------------------------
// task: getAll
// ----------------------------------------
// https://developer.copper.com/tasks/list-tasks-search.html
const body: IDataObject = {};
const filterFields = this.getNodeParameter('filterFields', i) as IDataObject;
if (Object.keys(filterFields).length) {
Object.assign(body, adjustTaskFields(filterFields));
}
responseData = await handleListing.call(this, 'POST', '/tasks/search', body);
} else if (operation === 'update') {
// ----------------------------------------
// task: update
// ----------------------------------------
// https://developer.copper.com/tasks/update-a-task.html
const taskId = this.getNodeParameter('taskId', i);
const body: IDataObject = {};
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
if (Object.keys(updateFields).length) {
Object.assign(body, updateFields);
}
responseData = await copperApiRequest.call(this, 'PUT', `/tasks/${taskId}`, body);
}
} else if (resource === 'user') {
// **********************************************************************
// user
// **********************************************************************
if (operation === 'getAll') {
// ----------------------------------------
// user: getAll
// ----------------------------------------
responseData = await handleListing.call(this, 'POST', '/users/search');
}
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.toString() });
continue;
}
throw error;
}
Array.isArray(responseData)
? returnData.push(...responseData)
: returnData.push(responseData);
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -19,7 +19,7 @@ export class CopperTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Copper Trigger',
name: 'copperTrigger',
icon: 'file:copper.png',
icon: 'file:copper.svg',
group: ['trigger'],
version: 1,
description: 'Handle Copper events via webhooks',
@ -147,7 +147,7 @@ export class CopperTrigger implements INodeType {
const endpoint = `/webhooks/${webhookData.webhookId}`;
try {
await copperApiRequest.call(this, 'DELETE', endpoint);
} catch(error) {
} catch (error) {
return false;
}
delete webhookData.webhookId;

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