diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 568f8fc9a76..fcc046beeff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ The most important directories: execution, active webhooks and workflows - [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor - - [/packages/node-dev](/packages/node-dev) - Simple CLI to create new n8n-nodes + - [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes - [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes - [/packages/workflow](/packages/workflow) - Workflow code with interfaces which get used by front- & backend @@ -159,7 +159,7 @@ tests of all packages. ## Create Custom Nodes -It is very easy to create own nodes for n8n. More information about that can +It is very straightforward to create your own nodes for n8n. More information about that can be found in the documentation of "n8n-node-dev" which is a small CLI which helps with n8n-node-development. @@ -177,9 +177,9 @@ If you want to create a node which should be added to n8n follow these steps: 1. Create a new folder for the new node. For a service named "Example" the folder would be called: `/packages/nodes-base/nodes/Example` - 1. If there is already a similar node simply copy the existing one in the new folder and rename it. If none exists yet, create a boilerplate node with [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) and copy that one in the folder. + 1. If there is already a similar node, copy the existing one in the new folder and rename it. If none exists yet, create a boilerplate node with [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) and copy that one in the folder. - 1. If the node needs credentials because it has to authenticate with an API or similar create new ones. Existing ones can be found in folder `/packages/nodes-base/credentials`. Also there it is the easiest to simply copy existing similar ones. + 1. If the node needs credentials because it has to authenticate with an API or similar create new ones. Existing ones can be found in folder `/packages/nodes-base/credentials`. Also there it is the easiest to copy existing similar ones. 1. Add the path to the new node (and optionally credentials) to package.json of `nodes-base`. It already contains a property `n8n` with its own keys `credentials` and `nodes`. @@ -236,6 +236,6 @@ docsify serve ./docs That we do not have any potential problems later it is sadly necessary to sign a [Contributor License Agreement](CONTRIBUTOR_LICENSE_AGREEMENT.md). That can be done literally with the push of a button. -We used the most simple one that exists. It is from [Indie Open Source](https://indieopensource.com/forms/cla) which uses plain English and is literally just a few lines long. +We used the most simple one that exists. It is from [Indie Open Source](https://indieopensource.com/forms/cla) which uses plain English and is literally only a few lines long. A bot will automatically comment on the pull request once it got opened asking for the agreement to be signed. Before it did not get signed it is sadly not possible to merge it in. diff --git a/LICENSE.md b/LICENSE.md index aac54547eb9..24a7d38fc94 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/docs/configuration.md b/docs/configuration.md index ce57ef92dfb..63b8c95f12b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -14,6 +14,9 @@ Sets how n8n should be made available. # The port n8n should be made available on N8N_PORT=5678 +# The IP address n8n should listen on +N8N_LISTEN_ADDRESS=0.0.0.0 + # This ones are currently only important for the webhook URL creation. # So if "WEBHOOK_TUNNEL_URL" got set they do get ignored. It is however # encouraged to set them correctly anyway in case they will become diff --git a/docs/server-setup.md b/docs/server-setup.md index f2c830c48f3..d34d076a6f0 100644 --- a/docs/server-setup.md +++ b/docs/server-setup.md @@ -105,6 +105,7 @@ services: - N8N_BASIC_AUTH_PASSWORD - N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME} - N8N_PORT=5678 + - N8N_LISTEN_ADDRESS=0.0.0.0 - N8N_PROTOCOL=https - NODE_ENV=production - WEBHOOK_TUNNEL_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/ diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index e5601334803..68be5ce947e 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -3,7 +3,22 @@ This list shows all the versions which include breaking changes and how to upgrade. -## ??? +## 0.69.0 + +### What changed? + +We have simplified how attachments are handled by the Twitter node. Rather than clicking on `Add Attachments` and having to specify the `Catergory`, you can now add attachments by just clicking on `Add Field` and selecting `Attachments`. There's no longer an option to specify the type of attachment you are adding. + +### When is action necessary? + +If you have used the Attachments option in your Twitter nodes. + +### How to upgrade: + +You'll need to re-create the attachments for the Twitter node. + + +## 0.68.0 ### What changed? diff --git a/packages/cli/LICENSE.md b/packages/cli/LICENSE.md index aac54547eb9..24a7d38fc94 100644 --- a/packages/cli/LICENSE.md +++ b/packages/cli/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index cdea6a2a0db..3eb5956e9dd 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -11,6 +11,7 @@ import { ActiveExecutions, CredentialsOverwrites, Db, + ExternalHooks, GenericHelpers, IWorkflowBase, IWorkflowExecutionDataProcess, @@ -108,6 +109,10 @@ export class Execute extends Command { const credentialsOverwrites = CredentialsOverwrites(); await credentialsOverwrites.init(); + // Load all external hooks + const externalHooks = ExternalHooks(); + await externalHooks.init(); + // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); await nodeTypes.init(loadNodesAndCredentials.nodeTypes); diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index 10dde58b35d..1b76459de7c 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -5,7 +5,6 @@ import { } from 'n8n-core'; import { Command, flags } from '@oclif/command'; const open = require('open'); -// import { dirname } from 'path'; import * as config from '../config'; import { @@ -13,6 +12,7 @@ import { CredentialTypes, CredentialsOverwrites, Db, + ExternalHooks, GenericHelpers, LoadNodesAndCredentials, NodeTypes, @@ -113,6 +113,10 @@ export class Start extends Command { const credentialsOverwrites = CredentialsOverwrites(); await credentialsOverwrites.init(); + // Load all external hooks + const externalHooks = ExternalHooks(); + await externalHooks.init(); + // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); await nodeTypes.init(loadNodesAndCredentials.nodeTypes); diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 6148797615a..a3c2cfd03aa 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -63,6 +63,34 @@ const config = convict({ default: 'public', env: 'DB_POSTGRESDB_SCHEMA' }, + + ssl: { + ca: { + doc: 'SSL certificate authority', + format: String, + default: '', + env: 'DB_POSTGRESDB_SSL_CA', + }, + cert: { + doc: 'SSL certificate', + format: String, + default: '', + env: 'DB_POSTGRESDB_SSL_CERT', + }, + key: { + doc: 'SSL key', + format: String, + default: '', + env: 'DB_POSTGRESDB_SSL_KEY', + }, + rejectUnauthorized: { + doc: 'If unauthorized SSL connections should be rejected', + format: 'Boolean', + default: true, + env: 'DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED', + }, + } + }, mysqldb: { database: { @@ -182,6 +210,12 @@ const config = convict({ env: 'N8N_PORT', doc: 'HTTP port n8n can be reached' }, + listen_address: { + format: String, + default: '0.0.0.0', + env: 'N8N_LISTEN_ADDRESS', + doc: 'IP address n8n should listen on' + }, protocol: { format: ['http', 'https'], default: 'http', @@ -265,6 +299,13 @@ const config = convict({ }, }, + externalHookFiles: { + doc: 'Files containing external hooks. Multiple files can be separated by colon (":")', + format: String, + default: '', + env: 'EXTERNAL_HOOK_FILES' + }, + nodes: { exclude: { doc: 'Nodes not to load', diff --git a/packages/cli/package.json b/packages/cli/package.json index 5ab81530649..1628a80b82c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.67.3", + "version": "0.70.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -47,6 +47,7 @@ }, "files": [ "bin", + "templates", "dist", "oclif.manifest.json" ], @@ -99,10 +100,11 @@ "lodash.get": "^4.4.2", "mongodb": "^3.5.5", "mysql2": "^2.0.1", - "n8n-core": "~0.34.0", - "n8n-editor-ui": "~0.45.0", - "n8n-nodes-base": "~0.62.1", - "n8n-workflow": "~0.31.0", + "n8n-core": "~0.36.0", + "n8n-editor-ui": "~0.47.0", + "n8n-nodes-base": "~0.65.0", + "n8n-workflow": "~0.33.0", + "oauth-1.0a": "^2.2.6", "open": "^7.0.0", "pg": "^7.11.0", "request-promise-native": "^1.0.7", diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index 54633adb139..efda1f63665 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -14,6 +14,8 @@ import { getRepository, } from 'typeorm'; +import { TlsOptions } from 'tls'; + import * as config from '../config'; import { @@ -72,6 +74,22 @@ export async function init(): Promise { case 'postgresdb': entities = PostgresDb; + + const sslCa = await GenericHelpers.getConfigValue('database.postgresdb.ssl.ca') as string; + const sslCert = await GenericHelpers.getConfigValue('database.postgresdb.ssl.cert') as string; + const sslKey = await GenericHelpers.getConfigValue('database.postgresdb.ssl.key') as string; + const sslRejectUnauthorized = await GenericHelpers.getConfigValue('database.postgresdb.ssl.rejectUnauthorized') as boolean; + + let ssl: TlsOptions | undefined = undefined; + if (sslCa !== '' || sslCert !== '' || sslKey !== '' || sslRejectUnauthorized !== true) { + ssl = { + ca: sslCa || undefined, + cert: sslCert || undefined, + key: sslKey || undefined, + rejectUnauthorized: sslRejectUnauthorized, + }; + } + connectionOptions = { type: 'postgres', entityPrefix, @@ -84,7 +102,9 @@ export async function init(): Promise { migrations: [InitialMigration1587669153312], migrationsRun: true, migrationsTableName: `${entityPrefix}migrations`, + ssl, }; + break; case 'mariadb': diff --git a/packages/cli/src/ExternalHooks.ts b/packages/cli/src/ExternalHooks.ts new file mode 100644 index 00000000000..355415158ac --- /dev/null +++ b/packages/cli/src/ExternalHooks.ts @@ -0,0 +1,79 @@ +import { + Db, + IExternalHooksFunctions, + IExternalHooksClass, +} from './'; + +import * as config from '../config'; + + +class ExternalHooksClass implements IExternalHooksClass { + + externalHooks: { + [key: string]: Array<() => {}> + } = {}; + initDidRun = false; + + + async init(): Promise { + if (this.initDidRun === true) { + return; + } + + const externalHookFiles = config.get('externalHookFiles').split(':'); + + // Load all the provided hook-files + for (let hookFilePath of externalHookFiles) { + hookFilePath = hookFilePath.trim(); + if (hookFilePath !== '') { + try { + const hookFile = require(hookFilePath); + + for (const resource of Object.keys(hookFile)) { + for (const operation of Object.keys(hookFile[resource])) { + // Save all the hook functions directly under their string + // format in an array + const hookString = `${resource}.${operation}`; + if (this.externalHooks[hookString] === undefined) { + this.externalHooks[hookString] = []; + } + + this.externalHooks[hookString].push.apply(this.externalHooks[hookString], hookFile[resource][operation]); + } + } + } catch (error) { + throw new Error(`Problem loading external hook file "${hookFilePath}": ${error.message}`); + } + } + } + + this.initDidRun = true; + } + + async run(hookName: string, hookParameters?: any[]): Promise { // tslint:disable-line:no-any + const externalHookFunctions: IExternalHooksFunctions = { + dbCollections: Db.collections, + }; + + if (this.externalHooks[hookName] === undefined) { + return; + } + + for(const externalHookFunction of this.externalHooks[hookName]) { + await externalHookFunction.apply(externalHookFunctions, hookParameters); + } + } + +} + + + +let externalHooksInstance: ExternalHooksClass | undefined; + +export function ExternalHooks(): ExternalHooksClass { + if (externalHooksInstance === undefined) { + externalHooksInstance = new ExternalHooksClass(); + } + + return externalHooksInstance; +} diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index abab09bd139..b9f7144bf46 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -197,6 +197,30 @@ export interface IExecutingWorkflowData { workflowExecution?: PCancelable; } +export interface IExternalHooks { + credentials?: { + create?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise; }> + delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise; }> + update?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise; }> + }; + workflow?: { + activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise; }> + create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise; }> + delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise; }> + execute?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb, mode: WorkflowExecuteMode): Promise; }> + update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise; }> + }; +} + +export interface IExternalHooksFunctions { + dbCollections: IDatabaseCollections; +} + +export interface IExternalHooksClass { + init(): Promise; + run(hookName: string, hookParameters?: any[]): Promise; // tslint:disable-line:no-any +} + export interface IN8nConfig { database: IN8nConfigDatabase; endpoints: IN8nConfigEndpoints; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 9fd769451e0..1b3750a20ca 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -13,10 +13,13 @@ import { import * as bodyParser from 'body-parser'; require('body-parser-xml')(bodyParser); import * as history from 'connect-history-api-fallback'; -import * as requestPromise from 'request-promise-native'; import * as _ from 'lodash'; import * as clientOAuth2 from 'client-oauth2'; +import * as clientOAuth1 from 'oauth-1.0a'; +import { RequestOptions } from 'oauth-1.0a'; import * as csrf from 'csrf'; +import * as requestPromise from 'request-promise-native'; +import { createHmac } from 'crypto'; import { ActiveExecutions, @@ -24,6 +27,7 @@ import { CredentialsHelper, CredentialTypes, Db, + ExternalHooks, IActivationError, ICustomRequest, ICredentialsDb, @@ -38,6 +42,7 @@ import { IExecutionsListResponse, IExecutionsStopData, IExecutionsSummary, + IExternalHooksClass, IN8nUISettings, IPackageVersions, IWorkflowBase, @@ -90,7 +95,8 @@ import * as jwks from 'jwks-rsa'; // @ts-ignore import * as timezones from 'google-timezones-json'; import * as parseUrl from 'parseurl'; - +import * as querystring from 'querystring'; +import { OptionsWithUrl } from 'request-promise-native'; class App { @@ -99,6 +105,7 @@ class App { testWebhooks: TestWebhooks.TestWebhooks; endpointWebhook: string; endpointWebhookTest: string; + externalHooks: IExternalHooksClass; saveDataErrorExecution: string; saveDataSuccessExecution: string; saveManualExecutions: boolean; @@ -106,6 +113,7 @@ class App { activeExecutionsInstance: ActiveExecutions.ActiveExecutions; push: Push.Push; versions: IPackageVersions | undefined; + restEndpoint: string; protocol: string; sslKey: string; @@ -120,6 +128,7 @@ class App { this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean; this.timezone = config.get('generic.timezone') as string; + this.restEndpoint = config.get('endpoints.rest') as string; this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance(); this.testWebhooks = TestWebhooks.getInstance(); @@ -130,6 +139,8 @@ class App { this.protocol = config.get('protocol'); this.sslKey = config.get('ssl_key'); this.sslCert = config.get('ssl_cert'); + + this.externalHooks = ExternalHooks(); } @@ -226,7 +237,7 @@ class App { // Get push connections this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { - if (req.url.indexOf('/rest/push') === 0) { + if (req.url.indexOf(`/${this.restEndpoint}/push`) === 0) { // TODO: Later also has to add some kind of authentication token if (req.query.sessionId === undefined) { next(new Error('The query parameter "sessionId" is missing!')); @@ -275,7 +286,7 @@ class App { this.app.use(history({ rewrites: [ { - from: new RegExp(`^\/(rest|healthz|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`), + from: new RegExp(`^\/(${this.restEndpoint}|healthz|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`), to: (context) => { return context.parsedUrl!.pathname!.toString(); } @@ -345,9 +356,9 @@ class App { // Creates a new workflow - this.app.post('/rest/workflows', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.post(`/${this.restEndpoint}/workflows`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const newWorkflowData = req.body; + const newWorkflowData = req.body as IWorkflowBase; newWorkflowData.name = newWorkflowData.name.trim(); newWorkflowData.createdAt = this.getCurrentDate(); @@ -355,6 +366,8 @@ class App { newWorkflowData.id = undefined; + await this.externalHooks.run('workflow.create', [newWorkflowData]); + // Save the workflow in DB const result = await Db.collections.Workflow!.save(newWorkflowData); @@ -366,7 +379,7 @@ class App { // Reads and returns workflow data from an URL - this.app.get('/rest/workflows/from-url', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/workflows/from-url`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { if (req.query.url === undefined) { throw new ResponseHelper.ResponseError(`The parameter "url" is missing!`, undefined, 400); } @@ -394,7 +407,7 @@ class App { // Returns workflows - this.app.get('/rest/workflows', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/workflows`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const findQuery = {} as FindManyOptions; if (req.query.filter) { findQuery.where = JSON.parse(req.query.filter as string); @@ -414,7 +427,7 @@ class App { // Returns a specific workflow - this.app.get('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const result = await Db.collections.Workflow!.findOne(req.params.id); if (result === undefined) { @@ -428,11 +441,13 @@ class App { // Updates an existing workflow - this.app.patch('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.patch(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const newWorkflowData = req.body; + const newWorkflowData = req.body as IWorkflowBase; const id = req.params.id; + await this.externalHooks.run('workflow.update', [newWorkflowData]); + if (this.activeWorkflowRunner.isActive(id)) { // When workflow gets saved always remove it as the triggers could have been // changed and so the changes would not take effect @@ -474,6 +489,8 @@ class App { if (responseData.active === true) { // When the workflow is supposed to be active add it again try { + await this.externalHooks.run('workflow.activate', [responseData]); + await this.activeWorkflowRunner.add(id); } catch (error) { // If workflow could not be activated set it again to inactive @@ -495,9 +512,11 @@ class App { // Deletes a specific workflow - this.app.delete('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.delete(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const id = req.params.id; + await this.externalHooks.run('workflow.delete', [id]); + if (this.activeWorkflowRunner.isActive(id)) { // Before deleting a workflow deactivate it await this.activeWorkflowRunner.remove(id); @@ -509,7 +528,7 @@ class App { })); - this.app.post('/rest/workflows/run', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.post(`/${this.restEndpoint}/workflows/run`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const workflowData = req.body.workflowData; const runData: IRunData | undefined = req.body.runData; const startNodes: string[] | undefined = req.body.startNodes; @@ -558,7 +577,7 @@ class App { // Returns parameter values which normally get loaded from an external API or // get generated dynamically - this.app.get('/rest/node-parameter-options', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/node-parameter-options`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const nodeType = req.query.nodeType as string; let credentials: INodeCredentials | undefined = undefined; const currentNodeParameters = JSON.parse('' + req.query.currentNodeParameters) as INodeParameters; @@ -569,7 +588,7 @@ class App { const nodeTypes = NodeTypes(); - const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, credentials!); + const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, JSON.parse('' + req.query.currentNodeParameters), credentials!); const workflowData = loadDataInstance.getWorkflowData() as IWorkflowBase; const workflowCredentials = await WorkflowCredentials(workflowData.nodes); @@ -580,7 +599,7 @@ class App { // Returns all the node-types - this.app.get('/rest/node-types', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/node-types`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const returnData: INodeTypeDescription[] = []; @@ -603,7 +622,7 @@ class App { // Returns the node icon - this.app.get(['/rest/node-icon/:nodeType', '/rest/node-icon/:scope/:nodeType'], async (req: express.Request, res: express.Response): Promise => { + this.app.get([`/${this.restEndpoint}/node-icon/:nodeType`, `/${this.restEndpoint}/node-icon/:scope/:nodeType`], async (req: express.Request, res: express.Response): Promise => { const nodeTypeName = `${req.params.scope ? `${req.params.scope}/` : ''}${req.params.nodeType}`; const nodeTypes = NodeTypes(); @@ -637,13 +656,13 @@ class App { // Returns the active workflow ids - this.app.get('/rest/active', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/active`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { return this.activeWorkflowRunner.getActiveWorkflows(); })); // Returns if the workflow with the given id had any activation errors - this.app.get('/rest/active/error/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/active/error/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const id = req.params.id; return this.activeWorkflowRunner.getActivationError(id); })); @@ -656,16 +675,18 @@ class App { // Deletes a specific credential - this.app.delete('/rest/credentials/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.delete(`/${this.restEndpoint}/credentials/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const id = req.params.id; + await this.externalHooks.run('credentials.delete', [id]); + await Db.collections.Credentials!.delete({ id }); return true; })); // Creates new credentials - this.app.post('/rest/credentials', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.post(`/${this.restEndpoint}/credentials`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const incomingData = req.body; if (!incomingData.name || incomingData.name.length < 3) { @@ -704,6 +725,8 @@ class App { credentials.setData(incomingData.data, encryptionKey); const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; + await this.externalHooks.run('credentials.create', [newCredentialsData]); + // Add special database related data newCredentialsData.createdAt = this.getCurrentDate(); newCredentialsData.updatedAt = this.getCurrentDate(); @@ -721,7 +744,7 @@ class App { // Updates existing credentials - this.app.patch('/rest/credentials/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.patch(`/${this.restEndpoint}/credentials/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const incomingData = req.body; const id = req.params.id; @@ -779,6 +802,8 @@ class App { // Add special database related data newCredentialsData.updatedAt = this.getCurrentDate(); + await this.externalHooks.run('credentials.update', [newCredentialsData]); + // Update the credentials in DB await Db.collections.Credentials!.update(id, newCredentialsData); @@ -800,7 +825,7 @@ class App { // Returns specific credentials - this.app.get('/rest/credentials/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/credentials/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const findQuery = {} as FindManyOptions; // Make sure the variable has an expected value @@ -835,7 +860,7 @@ class App { // Returns all the saved credentials - this.app.get('/rest/credentials', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/credentials`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const findQuery = {} as FindManyOptions; if (req.query.filter) { findQuery.where = JSON.parse(req.query.filter as string); @@ -877,7 +902,7 @@ class App { // Returns all the credential types which are defined in the loaded n8n-modules - this.app.get('/rest/credential-types', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/credential-types`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const returnData: ICredentialType[] = []; @@ -890,6 +915,158 @@ class App { return returnData; })); + // ---------------------------------------- + // OAuth1-Credential/Auth + // ---------------------------------------- + + // Authorize OAuth Data + this.app.get(`/${this.restEndpoint}/oauth1-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + if (req.query.id === undefined) { + throw new Error('Required credential id is missing!'); + } + + const result = await Db.collections.Credentials!.findOne(req.query.id as string); + if (result === undefined) { + res.status(404).send('The credential is not known.'); + return ''; + } + + let encryptionKey = undefined; + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + throw new Error('No encryption key got found to decrypt the credentials!'); + } + + // Decrypt the currently saved credentials + const workflowCredentials: IWorkflowCredentials = { + [result.type as string]: { + [result.name as string]: result as ICredentialsEncrypted, + }, + }; + const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey); + const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type); + + const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string; + + const oauth = new clientOAuth1({ + consumer: { + key: _.get(oauthCredentials, 'consumerKey') as string, + secret: _.get(oauthCredentials, 'consumerSecret') as string, + }, + signature_method: signatureMethod, + hash_function(base, key) { + const algorithm = (signatureMethod === 'HMAC-SHA1') ? 'sha1' : 'sha256'; + return createHmac(algorithm, key) + .update(base) + .digest('base64'); + }, + }); + + const callback = `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth1-credential/callback?cid=${req.query.id}`; + + const options: RequestOptions = { + method: 'POST', + url: (_.get(oauthCredentials, 'requestTokenUrl') as string), + data: { + oauth_callback: callback, + }, + }; + + const data = oauth.toHeader(oauth.authorize(options as RequestOptions)); + + //@ts-ignore + options.headers = data; + + const response = await requestPromise(options); + + // Response comes as x-www-form-urlencoded string so convert it to JSON + + const responseJson = querystring.parse(response); + + const returnUri = `${_.get(oauthCredentials, 'authUrl')}?oauth_token=${responseJson.oauth_token}`; + + // Encrypt the data + const credentials = new Credentials(result.name, result.type, result.nodesAccess); + + credentials.setData(decryptedDataOriginal, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; + + // Add special database related data + newCredentialsData.updatedAt = this.getCurrentDate(); + + // Update the credentials in DB + await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData); + + return returnUri; + })); + + // Verify and store app code. Generate access tokens and store for respective credential. + this.app.get(`/${this.restEndpoint}/oauth1-credential/callback`, async (req: express.Request, res: express.Response) => { + const { oauth_verifier, oauth_token, cid } = req.query; + + if (oauth_verifier === undefined || oauth_token === undefined) { + throw new Error('Insufficient parameters for OAuth1 callback'); + } + + const result = await Db.collections.Credentials!.findOne(cid as any); // tslint:disable-line:no-any + if (result === undefined) { + const errorResponse = new ResponseHelper.ResponseError('The credential is not known.', undefined, 404); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + let encryptionKey = undefined; + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + const errorResponse = new ResponseHelper.ResponseError('No encryption key got found to decrypt the credentials!', undefined, 503); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + // Decrypt the currently saved credentials + const workflowCredentials: IWorkflowCredentials = { + [result.type as string]: { + [result.name as string]: result as ICredentialsEncrypted, + }, + }; + const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey); + const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type); + + const options: OptionsWithUrl = { + method: 'POST', + url: _.get(oauthCredentials, 'accessTokenUrl') as string, + qs: { + oauth_token, + oauth_verifier, + } + }; + + let oauthToken; + + try { + oauthToken = await requestPromise(options); + } catch (error) { + const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + // Response comes as x-www-form-urlencoded string so convert it to JSON + + const oauthTokenJson = querystring.parse(oauthToken); + + decryptedDataOriginal.oauthTokenData = oauthTokenJson; + + const credentials = new Credentials(result.name, result.type, result.nodesAccess); + credentials.setData(decryptedDataOriginal, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; + // Add special database related data + newCredentialsData.updatedAt = this.getCurrentDate(); + // Save the credentials in DB + await Db.collections.Credentials!.update(cid as any, newCredentialsData); // tslint:disable-line:no-any + + res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html')); + }); + // ---------------------------------------- // OAuth2-Credential/Auth @@ -897,7 +1074,7 @@ class App { // Authorize OAuth Data - this.app.get('/rest/oauth2-credential/auth', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/oauth2-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { if (req.query.id === undefined) { throw new Error('Required credential id is missing!'); } @@ -938,7 +1115,7 @@ class App { clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string, authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string, - redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}rest/oauth2-credential/callback`, + redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`, scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','), state: stateEncodedStr, }); @@ -971,7 +1148,7 @@ class App { // ---------------------------------------- // Verify and store app code. Generate access tokens and store for respective credential. - this.app.get('/rest/oauth2-credential/callback', async (req: express.Request, res: express.Response) => { + this.app.get(`/${this.restEndpoint}/oauth2-credential/callback`, async (req: express.Request, res: express.Response) => { const {code, state: stateEncoded } = req.query; if (code === undefined || stateEncoded === undefined) { @@ -1031,7 +1208,7 @@ class App { clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string, authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string, - redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}rest/oauth2-credential/callback`, + redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`, scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',') }); @@ -1071,7 +1248,7 @@ class App { // Returns all finished executions - this.app.get('/rest/executions', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/executions`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { let filter: any = {}; // tslint:disable-line:no-any if (req.query.filter) { @@ -1136,7 +1313,7 @@ class App { // Returns a specific execution - this.app.get('/rest/executions/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/executions/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const result = await Db.collections.Execution!.findOne(req.params.id); if (result === undefined) { @@ -1150,7 +1327,7 @@ class App { // Retries a failed execution - this.app.post('/rest/executions/:id/retry', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.post(`/${this.restEndpoint}/executions/:id/retry`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { // Get the data to execute const fullExecutionDataFlatted = await Db.collections.Execution!.findOne(req.params.id); @@ -1224,7 +1401,7 @@ class App { // Delete Executions // INFORMATION: We use POST instead of DELETE to not run into any issues // with the query data getting to long - this.app.post('/rest/executions/delete', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.post(`/${this.restEndpoint}/executions/delete`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const deleteData = req.body as IExecutionDeleteFilter; if (deleteData.deleteBefore !== undefined) { @@ -1251,7 +1428,7 @@ class App { // Returns all the currently working executions - this.app.get('/rest/executions-current', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/executions-current`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const executingWorkflows = this.activeExecutionsInstance.getActiveExecutions(); const returnData: IExecutionsSummary[] = []; @@ -1280,7 +1457,7 @@ class App { })); // Forces the execution to stop - this.app.post('/rest/executions-current/:id/stop', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.post(`/${this.restEndpoint}/executions-current/:id/stop`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const executionId = req.params.id; // Stopt he execution and wait till it is done and we got the data @@ -1302,7 +1479,7 @@ class App { // Removes a test webhook - this.app.delete('/rest/test-webhook/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.delete(`/${this.restEndpoint}/test-webhook/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const workflowId = req.params.id; return this.testWebhooks.cancelTestWebhook(workflowId); })); @@ -1314,7 +1491,7 @@ class App { // ---------------------------------------- // Returns all the available timezones - this.app.get('/rest/options/timezones', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/options/timezones`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { return timezones; })); @@ -1327,7 +1504,7 @@ class App { // Returns the settings which are needed in the UI - this.app.get('/rest/settings', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get(`/${this.restEndpoint}/settings`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { return { endpointWebhook: this.endpointWebhook, endpointWebhookTest: this.endpointWebhookTest, @@ -1493,6 +1670,7 @@ class App { export async function start(): Promise { const PORT = config.get('port'); + const ADDRESS = config.get('listen_address'); const app = new App(); @@ -1511,9 +1689,9 @@ export async function start(): Promise { server = http.createServer(app.app); } - server.listen(PORT, async () => { + server.listen(PORT, ADDRESS, async () => { const versions = await GenericHelpers.getVersions(); - console.log(`n8n ready on port ${PORT}`); + console.log(`n8n ready on ${ADDRESS}, port ${PORT}`); console.log(`Version: ${versions.cli}`); }); } diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index d39e3f5deef..64e4c101ff2 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -149,6 +149,9 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo }; } + // Save static data if it changed + await WorkflowHelpers.saveStaticData(workflow); + if (webhookData.webhookDescription['responseHeaders'] !== undefined) { const responseHeaders = workflow.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], undefined) as { entries?: Array<{ diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 1c672410c57..7a61bfd6575 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -1,6 +1,7 @@ import { CredentialsHelper, Db, + ExternalHooks, IExecutionDb, IExecutionFlattedDb, IPushDataExecutionFinished, @@ -303,6 +304,10 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi workflowData = workflowInfo.code; } + const externalHooks = ExternalHooks(); + await externalHooks.init(); + await externalHooks.run('workflow.execute', [workflowData, mode]); + const nodeTypes = NodeTypes(); const workflowName = workflowData ? workflowData.name : undefined; diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 48ecff4cc02..c0d08f446ce 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -2,6 +2,7 @@ import { ActiveExecutions, CredentialsOverwrites, CredentialTypes, + ExternalHooks, ICredentialsOverwrite, ICredentialsTypeData, IProcessMessageDataHook, @@ -100,6 +101,9 @@ export class WorkflowRunner { * @memberof WorkflowRunner */ async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise { + const externalHooks = ExternalHooks(); + await externalHooks.run('workflow.execute', [data.workflowData, data.executionMode]); + const executionsProcess = config.get('executions.process') as string; if (executionsProcess === 'main') { return this.runMainProcess(data, loadStaticData); diff --git a/packages/cli/src/databases/mysqldb/migrations/1588157391238-InitialMigration.ts b/packages/cli/src/databases/mysqldb/migrations/1588157391238-InitialMigration.ts index fc11ef32fb3..1d1d4d8cc5f 100644 --- a/packages/cli/src/databases/mysqldb/migrations/1588157391238-InitialMigration.ts +++ b/packages/cli/src/databases/mysqldb/migrations/1588157391238-InitialMigration.ts @@ -8,8 +8,8 @@ export class InitialMigration1588157391238 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { const tablePrefix = config.get('database.tablePrefix'); - await queryRunner.query('CREATE TABLE IF NOT EXISTS `' + tablePrefix + 'credentials_entity` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL, `data` text NOT NULL, `type` varchar(32) NOT NULL, `nodesAccess` json NOT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, INDEX `IDX_07fde106c0b471d8cc80a64fc8` (`type`), PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined); - await queryRunner.query('CREATE TABLE IF NOT EXISTS `' + tablePrefix + 'execution_entity` (`id` int NOT NULL AUTO_INCREMENT, `data` text NOT NULL, `finished` tinyint NOT NULL, `mode` varchar(255) NOT NULL, `retryOf` varchar(255) NULL, `retrySuccessId` varchar(255) NULL, `startedAt` datetime NOT NULL, `stoppedAt` datetime NOT NULL, `workflowData` json NOT NULL, `workflowId` varchar(255) NULL, INDEX `IDX_c4d999a5e90784e8caccf5589d` (`workflowId`), PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined); + await queryRunner.query('CREATE TABLE IF NOT EXISTS `' + tablePrefix + 'credentials_entity` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL, `data` text NOT NULL, `type` varchar(32) NOT NULL, `nodesAccess` json NOT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, INDEX `IDX_' + tablePrefix + '07fde106c0b471d8cc80a64fc8` (`type`), PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined); + await queryRunner.query('CREATE TABLE IF NOT EXISTS `' + tablePrefix + 'execution_entity` (`id` int NOT NULL AUTO_INCREMENT, `data` text NOT NULL, `finished` tinyint NOT NULL, `mode` varchar(255) NOT NULL, `retryOf` varchar(255) NULL, `retrySuccessId` varchar(255) NULL, `startedAt` datetime NOT NULL, `stoppedAt` datetime NOT NULL, `workflowData` json NOT NULL, `workflowId` varchar(255) NULL, INDEX `IDX_' + tablePrefix + 'c4d999a5e90784e8caccf5589d` (`workflowId`), PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined); await queryRunner.query('CREATE TABLE IF NOT EXISTS`' + tablePrefix + 'workflow_entity` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL, `active` tinyint NOT NULL, `nodes` json NOT NULL, `connections` json NOT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, `settings` json NULL, `staticData` json NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined); } @@ -17,9 +17,9 @@ export class InitialMigration1588157391238 implements MigrationInterface { const tablePrefix = config.get('database.tablePrefix'); await queryRunner.query('DROP TABLE `' + tablePrefix + 'workflow_entity`', undefined); - await queryRunner.query('DROP INDEX `IDX_c4d999a5e90784e8caccf5589d` ON `' + tablePrefix + 'execution_entity`', undefined); + await queryRunner.query('DROP INDEX `IDX_' + tablePrefix + 'c4d999a5e90784e8caccf5589d` ON `' + tablePrefix + 'execution_entity`', undefined); await queryRunner.query('DROP TABLE `' + tablePrefix + 'execution_entity`', undefined); - await queryRunner.query('DROP INDEX `IDX_07fde106c0b471d8cc80a64fc8` ON `' + tablePrefix + 'credentials_entity`', undefined); + await queryRunner.query('DROP INDEX `IDX_' + tablePrefix + '07fde106c0b471d8cc80a64fc8` ON `' + tablePrefix + 'credentials_entity`', undefined); await queryRunner.query('DROP TABLE `' + tablePrefix + 'credentials_entity`', undefined); } diff --git a/packages/cli/src/databases/postgresdb/migrations/1587669153312-InitialMigration.ts b/packages/cli/src/databases/postgresdb/migrations/1587669153312-InitialMigration.ts index 555015c10da..29a80e434fa 100644 --- a/packages/cli/src/databases/postgresdb/migrations/1587669153312-InitialMigration.ts +++ b/packages/cli/src/databases/postgresdb/migrations/1587669153312-InitialMigration.ts @@ -7,29 +7,31 @@ export class InitialMigration1587669153312 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { let tablePrefix = config.get('database.tablePrefix'); + const tablePrefixIndex = tablePrefix; const schema = config.get('database.postgresdb.schema'); if (schema) { tablePrefix = schema + '.' + tablePrefix; } - await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}credentials_entity ("id" SERIAL NOT NULL, "name" character varying(128) NOT NULL, "data" text NOT NULL, "type" character varying(32) NOT NULL, "nodesAccess" json NOT NULL, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, CONSTRAINT PK_814c3d3c36e8a27fa8edb761b0e PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_07fde106c0b471d8cc80a64fc8 ON ${tablePrefix}credentials_entity (type) `, undefined); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}execution_entity ("id" SERIAL NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" character varying NOT NULL, "retryOf" character varying, "retrySuccessId" character varying, "startedAt" TIMESTAMP NOT NULL, "stoppedAt" TIMESTAMP NOT NULL, "workflowData" json NOT NULL, "workflowId" character varying, CONSTRAINT PK_e3e63bbf986767844bbe1166d4e PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_c4d999a5e90784e8caccf5589d ON ${tablePrefix}execution_entity ("workflowId") `, undefined); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}workflow_entity ("id" SERIAL NOT NULL, "name" character varying(128) NOT NULL, "active" boolean NOT NULL, "nodes" json NOT NULL, "connections" json NOT NULL, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, "settings" json, "staticData" json, CONSTRAINT PK_eded7d72664448da7745d551207 PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}credentials_entity ("id" SERIAL NOT NULL, "name" character varying(128) NOT NULL, "data" text NOT NULL, "type" character varying(32) NOT NULL, "nodesAccess" json NOT NULL, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, CONSTRAINT PK_${tablePrefixIndex}814c3d3c36e8a27fa8edb761b0e PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_${tablePrefixIndex}07fde106c0b471d8cc80a64fc8 ON ${tablePrefix}credentials_entity (type) `, undefined); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}execution_entity ("id" SERIAL NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" character varying NOT NULL, "retryOf" character varying, "retrySuccessId" character varying, "startedAt" TIMESTAMP NOT NULL, "stoppedAt" TIMESTAMP NOT NULL, "workflowData" json NOT NULL, "workflowId" character varying, CONSTRAINT PK_${tablePrefixIndex}e3e63bbf986767844bbe1166d4e PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_${tablePrefixIndex}c4d999a5e90784e8caccf5589d ON ${tablePrefix}execution_entity ("workflowId") `, undefined); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}workflow_entity ("id" SERIAL NOT NULL, "name" character varying(128) NOT NULL, "active" boolean NOT NULL, "nodes" json NOT NULL, "connections" json NOT NULL, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, "settings" json, "staticData" json, CONSTRAINT PK_${tablePrefixIndex}eded7d72664448da7745d551207 PRIMARY KEY ("id"))`, undefined); } async down(queryRunner: QueryRunner): Promise { let tablePrefix = config.get('database.tablePrefix'); + const tablePrefixIndex = tablePrefix; const schema = config.get('database.postgresdb.schema'); if (schema) { tablePrefix = schema + '.' + tablePrefix; } await queryRunner.query(`DROP TABLE ${tablePrefix}workflow_entity`, undefined); - await queryRunner.query(`DROP INDEX IDX_c4d999a5e90784e8caccf5589d`, undefined); + await queryRunner.query(`DROP INDEX IDX_${tablePrefixIndex}c4d999a5e90784e8caccf5589d`, undefined); await queryRunner.query(`DROP TABLE ${tablePrefix}execution_entity`, undefined); - await queryRunner.query(`DROP INDEX IDX_07fde106c0b471d8cc80a64fc8`, undefined); + await queryRunner.query(`DROP INDEX IDX_${tablePrefixIndex}07fde106c0b471d8cc80a64fc8`, undefined); await queryRunner.query(`DROP TABLE ${tablePrefix}credentials_entity`, undefined); } diff --git a/packages/cli/src/databases/sqlite/migrations/1588102412422-InitialMigration.ts b/packages/cli/src/databases/sqlite/migrations/1588102412422-InitialMigration.ts index 31b271d6335..c2bb55040ec 100644 --- a/packages/cli/src/databases/sqlite/migrations/1588102412422-InitialMigration.ts +++ b/packages/cli/src/databases/sqlite/migrations/1588102412422-InitialMigration.ts @@ -9,9 +9,9 @@ export class InitialMigration1588102412422 implements MigrationInterface { const tablePrefix = config.get('database.tablePrefix'); await queryRunner.query(`CREATE TABLE IF NOT EXISTS "${tablePrefix}credentials_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "data" text NOT NULL, "type" varchar(32) NOT NULL, "nodesAccess" text NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL)`, undefined); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_07fde106c0b471d8cc80a64fc8" ON "${tablePrefix}credentials_entity" ("type") `, undefined); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}07fde106c0b471d8cc80a64fc8" ON "${tablePrefix}credentials_entity" ("type") `, undefined); await queryRunner.query(`CREATE TABLE IF NOT EXISTS "${tablePrefix}execution_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" varchar NOT NULL, "retryOf" varchar, "retrySuccessId" varchar, "startedAt" datetime NOT NULL, "stoppedAt" datetime NOT NULL, "workflowData" text NOT NULL, "workflowId" varchar)`, undefined); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_c4d999a5e90784e8caccf5589d" ON "${tablePrefix}execution_entity" ("workflowId") `, undefined); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}c4d999a5e90784e8caccf5589d" ON "${tablePrefix}execution_entity" ("workflowId") `, undefined); await queryRunner.query(`CREATE TABLE IF NOT EXISTS "${tablePrefix}workflow_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "active" boolean NOT NULL, "nodes" text NOT NULL, "connections" text NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL, "settings" text, "staticData" text)`, undefined); } @@ -19,9 +19,9 @@ export class InitialMigration1588102412422 implements MigrationInterface { const tablePrefix = config.get('database.tablePrefix'); await queryRunner.query(`DROP TABLE "${tablePrefix}workflow_entity"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_c4d999a5e90784e8caccf5589d"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}c4d999a5e90784e8caccf5589d"`, undefined); await queryRunner.query(`DROP TABLE "${tablePrefix}execution_entity"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_07fde106c0b471d8cc80a64fc8"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}07fde106c0b471d8cc80a64fc8"`, undefined); await queryRunner.query(`DROP TABLE "${tablePrefix}credentials_entity"`, undefined); } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 3916e79edd9..3a6337a35d8 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,6 +1,7 @@ export * from './CredentialsHelper'; export * from './CredentialTypes'; export * from './CredentialsOverwrites'; +export * from './ExternalHooks'; export * from './Interfaces'; export * from './LoadNodesAndCredentials'; export * from './NodeTypes'; diff --git a/packages/core/LICENSE.md b/packages/core/LICENSE.md index aac54547eb9..24a7d38fc94 100644 --- a/packages/core/LICENSE.md +++ b/packages/core/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/core/package.json b/packages/core/package.json index c0616f5aeb1..598ae14b23f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.34.0", + "version": "0.36.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -45,7 +45,7 @@ "crypto-js": "3.1.9-1", "lodash.get": "^4.4.2", "mmmagic": "^0.5.2", - "n8n-workflow": "~0.31.0", + "n8n-workflow": "~0.32.0", "p-cancelable": "^2.0.0", "request": "^2.88.2", "request-promise-native": "^1.0.7" diff --git a/packages/core/src/Interfaces.ts b/packages/core/src/Interfaces.ts index 4b0a5ed4518..5aa0e10a64e 100644 --- a/packages/core/src/Interfaces.ts +++ b/packages/core/src/Interfaces.ts @@ -18,7 +18,7 @@ import { } from 'n8n-workflow'; -import { OptionsWithUri } from 'request'; +import { OptionsWithUri, OptionsWithUrl } from 'request'; import * as requestPromise from 'request-promise-native'; interface Constructable { @@ -36,7 +36,8 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -46,7 +47,8 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any }; } @@ -55,7 +57,8 @@ export interface IPollFunctions extends IPollFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -70,7 +73,8 @@ export interface ITriggerFunctions extends ITriggerFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -94,7 +98,8 @@ export interface IUserSettings { export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { helpers: { request?: requestPromise.RequestPromiseAPI, - requestOAuth?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions) => Promise, // tslint:disable-line:no-any + requestOAuth2?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions) => Promise, // tslint:disable-line:no-any + requestOAuth1?(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any }; } @@ -102,7 +107,8 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { export interface IHookFunctions extends IHookFunctionsBase { helpers: { request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any }; } @@ -111,7 +117,8 @@ export interface IWebhookFunctions extends IWebhookFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index f11abba582b..d500d56f88d 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -36,15 +36,20 @@ import { WorkflowExecuteMode, } from 'n8n-workflow'; +import * as clientOAuth1 from 'oauth-1.0a'; +import { RequestOptions, Token } from 'oauth-1.0a'; import * as clientOAuth2 from 'client-oauth2'; import { get } from 'lodash'; import * as express from 'express'; import * as path from 'path'; -import { OptionsWithUri } from 'request'; +import { OptionsWithUrl, OptionsWithUri } from 'request'; import * as requestPromise from 'request-promise-native'; import { Magic, MAGIC_MIME_TYPE } from 'mmmagic'; +import { createHmac } from 'crypto'; + + const magic = new Magic(MAGIC_MIME_TYPE); @@ -116,7 +121,7 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m * @param {IWorkflowExecuteAdditionalData} additionalData * @returns */ -export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string, property?: string) { +export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string, property?: string) { const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; if (credentials === undefined) { @@ -170,6 +175,68 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string }); } +/* Makes a request using OAuth1 data for authentication +* +* @export +* @param {IAllExecuteFunctions} this +* @param {string} credentialsType +* @param {(OptionsWithUrl | requestPromise.RequestPromiseOptions)} requestOptionså +* @returns +*/ +export function requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions) { + const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + if (credentials.oauthTokenData === undefined) { + throw new Error('OAuth credentials not connected!'); + } + + const oauth = new clientOAuth1({ + consumer: { + key: credentials.consumerKey as string, + secret: credentials.consumerSecret as string, + }, + signature_method: credentials.signatureMethod as string, + hash_function(base, key) { + const algorithm = (credentials.signatureMethod === 'HMAC-SHA1') ? 'sha1' : 'sha256'; + return createHmac(algorithm, key) + .update(base) + .digest('base64'); + }, + }); + + const oauthTokenData = credentials.oauthTokenData as IDataObject; + + const token: Token = { + key: oauthTokenData.oauth_token as string, + secret: oauthTokenData.oauth_token_secret as string, + }; + + const newRequestOptions = { + //@ts-ignore + url: requestOptions.url, + method: requestOptions.method, + data: { ...requestOptions.qs, ...requestOptions.body }, + json: requestOptions.json, + }; + + if (Object.keys(requestOptions.qs).length !== 0) { + //@ts-ignore + newRequestOptions.qs = oauth.authorize(newRequestOptions as RequestOptions, token); + } else { + //@ts-ignore + newRequestOptions.form = oauth.authorize(newRequestOptions as RequestOptions, token); + } + + return this.helpers.request!(newRequestOptions) + .catch(async (error: IResponseError) => { + // Unknown error so simply throw it + throw error; + }); +} /** @@ -462,8 +529,11 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, }, @@ -522,8 +592,11 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, }, @@ -615,8 +688,11 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, }, @@ -710,8 +786,11 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, }, }; @@ -763,8 +842,11 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, }, }; @@ -827,8 +909,11 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, }, }; @@ -918,8 +1003,11 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, }, diff --git a/packages/editor-ui/LICENSE.md b/packages/editor-ui/LICENSE.md index aac54547eb9..24a7d38fc94 100644 --- a/packages/editor-ui/LICENSE.md +++ b/packages/editor-ui/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index ec505a172cb..48caca7524e 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.45.0", + "version": "0.47.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -64,7 +64,7 @@ "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "n8n-workflow": "~0.31.0", + "n8n-workflow": "~0.33.0", "node-sass": "^4.12.0", "prismjs": "^1.17.1", "quill": "^2.0.0-dev.3", diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index e0e63553593..63ea4223a9c 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -145,6 +145,7 @@ export interface IRestApi { deleteExecutions(sendData: IExecutionDeleteFilter): Promise; retryExecution(id: string, loadWorkflow?: boolean): Promise; getTimezones(): Promise; + oAuth1CredentialAuthorize(sendData: ICredentialsResponse): Promise; oAuth2CredentialAuthorize(sendData: ICredentialsResponse): Promise; oAuth2Callback(code: string, state: string): Promise; } diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index db9f643d521..3d8edec3ecf 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -47,13 +47,13 @@ Not all required credential properties are filled - + Is connected - + Is NOT connected @@ -219,11 +219,11 @@ export default mixins( return this.credentialDataTemp; }, isOAuthType (): boolean { - if (this.credentialTypeData.name === 'oAuth2Api') { + if (['oAuth1Api', 'oAuth2Api'].includes(this.credentialTypeData.name)) { return true; } const types = this.parentTypes(this.credentialTypeData.name); - return types.includes('oAuth2Api'); + return types.includes('oAuth1Api') || types.includes('oAuth2Api'); }, isOAuthConnected (): boolean { if (this.isOAuthType === false) { @@ -233,7 +233,9 @@ export default mixins( return this.credentialDataDynamic !== null && !!this.credentialDataDynamic.data!.oauthTokenData; }, oAuthCallbackUrl (): string { - return this.$store.getters.getWebhookBaseUrl + 'rest/oauth2-credential/callback'; + const types = this.parentTypes(this.credentialTypeData.name); + const oauthType = (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) ? 'oauth2' : 'oauth1'; + return this.$store.getters.getWebhookBaseUrl + `rest/${oauthType}-credential/callback`; }, requiredPropertiesFilled (): boolean { for (const property of this.credentialProperties) { @@ -323,7 +325,7 @@ export default mixins( return result; }, - async oAuth2CredentialAuthorize () { + async oAuthCredentialAuthorize () { let url; let credentialData = this.credentialDataDynamic; @@ -350,8 +352,14 @@ export default mixins( } } + const types = this.parentTypes(this.credentialTypeData.name); + try { - url = await this.restApi().oAuth2CredentialAuthorize(credentialData as ICredentialsResponse) as string; + if (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) { + url = await this.restApi().oAuth2CredentialAuthorize(credentialData as ICredentialsResponse) as string; + } else if (this.credentialTypeData.name === 'oAuth1Api' || types.includes('oAuth1Api')) { + url = await this.restApi().oAuth1CredentialAuthorize(credentialData as ICredentialsResponse) as string; + } } catch (error) { this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:'); return; diff --git a/packages/editor-ui/src/components/mixins/nodeBase.ts b/packages/editor-ui/src/components/mixins/nodeBase.ts index 03ad84db00b..ba3661700a0 100644 --- a/packages/editor-ui/src/components/mixins/nodeBase.ts +++ b/packages/editor-ui/src/components/mixins/nodeBase.ts @@ -29,12 +29,6 @@ export const nodeBase = mixins(nodeIndex).extend({ isMacOs (): boolean { return /(ipad|iphone|ipod|mac)/i.test(navigator.platform); }, - isReadOnly (): boolean { - if (['NodeViewExisting', 'NodeViewNew'].includes(this.$route.name as string)) { - return false; - } - return true; - }, nodeName (): string { return NODE_NAME_PREFIX + this.nodeIndex; }, @@ -276,63 +270,71 @@ export const nodeBase = mixins(nodeIndex).extend({ this.instance.addEndpoint(this.nodeName, newEndpointData); }); - if (this.isReadOnly === false) { - // Make nodes draggable - this.instance.draggable(this.nodeName, { - grid: [10, 10], - start: (params: { e: MouseEvent }) => { - if (params.e && !this.$store.getters.isNodeSelected(this.data.name)) { - // Only the node which gets dragged directly gets an event, for all others it is - // undefined. So check if the currently dragged node is selected and if not clear - // the drag-selection. - this.instance.clearDragSelection(); - this.$store.commit('resetSelectedNodes'); + // TODO: This caused problems with displaying old information + // https://github.com/jsplumb/katavorio/wiki + // https://jsplumb.github.io/jsplumb/home.html + // Make nodes draggable + this.instance.draggable(this.nodeName, { + grid: [10, 10], + start: (params: { e: MouseEvent }) => { + if (this.isReadOnly === true) { + // Do not allow to move nodes in readOnly mode + return false; + } + + if (params.e && !this.$store.getters.isNodeSelected(this.data.name)) { + // Only the node which gets dragged directly gets an event, for all others it is + // undefined. So check if the currently dragged node is selected and if not clear + // the drag-selection. + this.instance.clearDragSelection(); + this.$store.commit('resetSelectedNodes'); + } + + this.$store.commit('addActiveAction', 'dragActive'); + return true; + }, + stop: (params: { e: MouseEvent }) => { + if (this.$store.getters.isActionActive('dragActive')) { + const moveNodes = this.$store.getters.getSelectedNodes.slice(); + const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name); + if (!selectedNodeNames.includes(this.data.name)) { + // If the current node is not in selected add it to the nodes which + // got moved manually + moveNodes.push(this.data); } - this.$store.commit('addActiveAction', 'dragActive'); - }, - stop: (params: { e: MouseEvent }) => { - if (this.$store.getters.isActionActive('dragActive')) { - const moveNodes = this.$store.getters.getSelectedNodes.slice(); - const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name); - if (!selectedNodeNames.includes(this.data.name)) { - // If the current node is not in selected add it to the nodes which - // got moved manually - moveNodes.push(this.data); + // This does for some reason just get called once for the node that got clicked + // even though "start" and "drag" gets called for all. So lets do for now + // some dirty DOM query to get the new positions till I have more time to + // create a proper solution + let newNodePositon: XYPositon; + moveNodes.forEach((node: INodeUi) => { + const nodeElement = `node-${this.getNodeIndex(node.name)}`; + const element = document.getElementById(nodeElement); + if (element === null) { + return; } - // This does for some reason just get called once for the node that got clicked - // even though "start" and "drag" gets called for all. So lets do for now - // some dirty DOM query to get the new positions till I have more time to - // create a proper solution - let newNodePositon: XYPositon; - moveNodes.forEach((node: INodeUi) => { - const nodeElement = `node-${this.getNodeIndex(node.name)}`; - const element = document.getElementById(nodeElement); - if (element === null) { - return; - } + newNodePositon = [ + parseInt(element.style.left!.slice(0, -2), 10), + parseInt(element.style.top!.slice(0, -2), 10), + ]; - newNodePositon = [ - parseInt(element.style.left!.slice(0, -2), 10), - parseInt(element.style.top!.slice(0, -2), 10), - ]; + const updateInformation = { + name: node.name, + properties: { + // @ts-ignore, draggable does not have definitions + position: newNodePositon, + }, + }; - const updateInformation = { - name: node.name, - properties: { - // @ts-ignore, draggable does not have definitions - position: newNodePositon, - }, - }; + this.$store.commit('updateNodeProperties', updateInformation); + }); + } + }, + filter: '.node-description, .node-description .node-name, .node-description .node-subtitle', + }); - this.$store.commit('updateNodeProperties', updateInformation); - }); - } - }, - filter: '.node-description, .node-description .node-name, .node-description .node-subtitle', - }); - } }, isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean { diff --git a/packages/editor-ui/src/components/mixins/restApi.ts b/packages/editor-ui/src/components/mixins/restApi.ts index be114786a9a..bca3fff338a 100644 --- a/packages/editor-ui/src/components/mixins/restApi.ts +++ b/packages/editor-ui/src/components/mixins/restApi.ts @@ -252,6 +252,11 @@ export const restApi = Vue.extend({ return self.restApi().makeRestApiRequest('GET', `/credential-types`); }, + // Get OAuth1 Authorization URL using the stored credentials + oAuth1CredentialAuthorize: (sendData: ICredentialsResponse): Promise => { + return self.restApi().makeRestApiRequest('GET', `/oauth1-credential/auth`, sendData); + }, + // Get OAuth2 Authorization URL using the stored credentials oAuth2CredentialAuthorize: (sendData: ICredentialsResponse): Promise => { return self.restApi().makeRestApiRequest('GET', `/oauth2-credential/auth`, sendData); diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index b9762bb6161..80454fc9e43 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -29,8 +29,6 @@ import { XYPositon, } from './Interface'; -import { get } from 'lodash'; - Vue.use(Vuex); export const store = new Vuex.Store({ diff --git a/packages/editor-ui/vue.config.js b/packages/editor-ui/vue.config.js index f70f41c5b23..cdcd8259f95 100644 --- a/packages/editor-ui/vue.config.js +++ b/packages/editor-ui/vue.config.js @@ -29,4 +29,5 @@ module.exports = { }, }, }, + publicPath: process.env.VUE_APP_PUBLIC_PATH ? process.env.VUE_APP_PUBLIC_PATH : '/', }; diff --git a/packages/node-dev/LICENSE.md b/packages/node-dev/LICENSE.md index aac54547eb9..24a7d38fc94 100644 --- a/packages/node-dev/LICENSE.md +++ b/packages/node-dev/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 9d859de3942..66394fec299 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -1,6 +1,6 @@ { "name": "n8n-node-dev", - "version": "0.7.0", + "version": "0.9.0", "description": "CLI to simplify n8n credentials/node development", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/node-dev/src/Build.ts b/packages/node-dev/src/Build.ts index ddb74add0a5..fd695efb4b7 100644 --- a/packages/node-dev/src/Build.ts +++ b/packages/node-dev/src/Build.ts @@ -105,10 +105,10 @@ export async function buildFiles (options?: IBuildOptions): Promise { } return new Promise((resolve, reject) => { + copyfiles([join(process.cwd(), './*.png'), outputDirectory], { up: true }, () => resolve(outputDirectory)); buildProcess.on('exit', code => { // Remove the tmp tsconfig file tsconfigData.cleanup(); - copyfiles([join(process.cwd(), './*.png'), outputDirectory], { up: true }, () => resolve(outputDirectory)); }); }); } diff --git a/packages/nodes-base/LICENSE.md b/packages/nodes-base/LICENSE.md index aac54547eb9..24a7d38fc94 100644 --- a/packages/nodes-base/LICENSE.md +++ b/packages/nodes-base/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/nodes-base/credentials/DriftOAuth2Api.credentials.ts b/packages/nodes-base/credentials/DriftOAuth2Api.credentials.ts new file mode 100644 index 00000000000..1d25c49dfe2 --- /dev/null +++ b/packages/nodes-base/credentials/DriftOAuth2Api.credentials.ts @@ -0,0 +1,47 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class DriftOAuth2Api implements ICredentialType { + name = 'driftOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Drift OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://dev.drift.com/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://driftapi.com/oauth2/token', + required: true, + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/credentials/EventbriteApi.credentials.ts b/packages/nodes-base/credentials/EventbriteApi.credentials.ts index 9fa48753fbe..e54be8580cd 100644 --- a/packages/nodes-base/credentials/EventbriteApi.credentials.ts +++ b/packages/nodes-base/credentials/EventbriteApi.credentials.ts @@ -8,7 +8,7 @@ export class EventbriteApi implements ICredentialType { displayName = 'Eventbrite API'; properties = [ { - displayName: 'API Key', + displayName: 'Private Key', name: 'apiKey', type: 'string' as NodePropertyTypes, default: '', diff --git a/packages/nodes-base/credentials/EventbriteOAuth2Api.credentials.ts b/packages/nodes-base/credentials/EventbriteOAuth2Api.credentials.ts new file mode 100644 index 00000000000..46a9df4266a --- /dev/null +++ b/packages/nodes-base/credentials/EventbriteOAuth2Api.credentials.ts @@ -0,0 +1,47 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class EventbriteOAuth2Api implements ICredentialType { + name = 'eventbriteOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Eventbrite OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://www.eventbrite.com/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://www.eventbrite.com/oauth/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body' + }, + ]; +} diff --git a/packages/nodes-base/credentials/GoogleDriveOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GoogleDriveOAuth2Api.credentials.ts new file mode 100644 index 00000000000..77b0a9504c7 --- /dev/null +++ b/packages/nodes-base/credentials/GoogleDriveOAuth2Api.credentials.ts @@ -0,0 +1,26 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/drive.appdata', + 'https://www.googleapis.com/auth/drive.photos.readonly', +]; + +export class GoogleDriveOAuth2Api implements ICredentialType { + name = 'googleDriveOAuth2Api'; + extends = [ + 'googleOAuth2Api', + ]; + displayName = 'Google Drive OAuth2 API'; + properties = [ + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + ]; +} diff --git a/packages/nodes-base/credentials/GoogleTasksOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GoogleTasksOAuth2Api.credentials.ts new file mode 100644 index 00000000000..ac1242be2b6 --- /dev/null +++ b/packages/nodes-base/credentials/GoogleTasksOAuth2Api.credentials.ts @@ -0,0 +1,22 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'https://www.googleapis.com/auth/tasks', +]; + +export class GoogleTasksOAuth2Api implements ICredentialType { + name = 'googleTasksOAuth2Api'; + extends = ['googleOAuth2Api']; + displayName = 'Google Tasks OAuth2 API'; + properties = [ + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' ') + }, + ]; +} diff --git a/packages/nodes-base/credentials/HubspotOAuth2Api.credentials.ts b/packages/nodes-base/credentials/HubspotOAuth2Api.credentials.ts new file mode 100644 index 00000000000..ce18b899df4 --- /dev/null +++ b/packages/nodes-base/credentials/HubspotOAuth2Api.credentials.ts @@ -0,0 +1,53 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'contacts', + 'forms', + 'tickets', +]; + +export class HubspotOAuth2Api implements ICredentialType { + name = 'hubspotOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Hubspot OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://app.hubspot.com/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.hubapi.com/oauth/v1/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'grant_type=authorization_code', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + description: 'Resource to consume.', + }, + ]; +} diff --git a/packages/nodes-base/credentials/MailchimpOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MailchimpOAuth2Api.credentials.ts new file mode 100644 index 00000000000..886424b6a65 --- /dev/null +++ b/packages/nodes-base/credentials/MailchimpOAuth2Api.credentials.ts @@ -0,0 +1,54 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class MailchimpOAuth2Api implements ICredentialType { + name = 'mailchimpOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Mailchimp OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://login.mailchimp.com/oauth2/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://login.mailchimp.com/oauth2/token', + required: true, + }, + { + displayName: 'Metadata', + name: 'metadataUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://login.mailchimp.com/oauth2/metadata', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + }, + ]; +} diff --git a/packages/nodes-base/credentials/MauticOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MauticOAuth2Api.credentials.ts new file mode 100644 index 00000000000..8c52f9372c1 --- /dev/null +++ b/packages/nodes-base/credentials/MauticOAuth2Api.credentials.ts @@ -0,0 +1,55 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class MauticOAuth2Api implements ICredentialType { + name = 'mauticOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Mautic OAuth2 API'; + properties = [ + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'https://name.mautic.net', + }, + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'https://name.mautic.net/oauth/v2/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'https://name.mautic.net/oauth/v2/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + }, + ]; +} diff --git a/packages/nodes-base/credentials/MessageBirdApi.credentials.ts b/packages/nodes-base/credentials/MessageBirdApi.credentials.ts new file mode 100644 index 00000000000..e67c6a0c9e4 --- /dev/null +++ b/packages/nodes-base/credentials/MessageBirdApi.credentials.ts @@ -0,0 +1,14 @@ +import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; + +export class MessageBirdApi implements ICredentialType { + name = 'messageBirdApi'; + displayName = 'MessageBird API'; + properties = [ + { + displayName: 'API Key', + name: 'accessKey', + type: 'string' as NodePropertyTypes, + default: '' + } + ]; +} diff --git a/packages/nodes-base/credentials/OAuth1Api.credentials.ts b/packages/nodes-base/credentials/OAuth1Api.credentials.ts new file mode 100644 index 00000000000..d5c18445f07 --- /dev/null +++ b/packages/nodes-base/credentials/OAuth1Api.credentials.ts @@ -0,0 +1,63 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class OAuth1Api implements ICredentialType { + name = 'oAuth1Api'; + displayName = 'OAuth1 API'; + properties = [ + { + displayName: 'Consumer Key', + name: 'consumerKey', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Consumer Secret', + name: 'consumerSecret', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Request Token URL', + name: 'requestTokenUrl', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Signature Method', + name: 'signatureMethod', + type: 'options' as NodePropertyTypes, + options: [ + { + name: 'HMAC-SHA1', + value: 'HMAC-SHA1' + }, + { + name: 'HMAC-SHA256', + value: 'HMAC-SHA256' + }, + ], + default: '', + required: true, + }, + ]; +} diff --git a/packages/nodes-base/credentials/PagerDutyOAuth2Api.credentials.ts b/packages/nodes-base/credentials/PagerDutyOAuth2Api.credentials.ts new file mode 100644 index 00000000000..28b44011db4 --- /dev/null +++ b/packages/nodes-base/credentials/PagerDutyOAuth2Api.credentials.ts @@ -0,0 +1,45 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class PagerDutyOAuth2Api implements ICredentialType { + name = 'pagerDutyOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'PagerDuty OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://app.pagerduty.com/oauth/authorize', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://app.pagerduty.com/oauth/token', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + description: 'Method of authentication.', + }, + ]; +} diff --git a/packages/nodes-base/credentials/Signl4Api.credentials.ts b/packages/nodes-base/credentials/Signl4Api.credentials.ts new file mode 100644 index 00000000000..842136de021 --- /dev/null +++ b/packages/nodes-base/credentials/Signl4Api.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class Signl4Api implements ICredentialType { + name = 'signl4Api'; + displayName = 'SIGNL4 Webhook'; + properties = [ + { + displayName: 'Team Secret', + name: 'teamSecret', + type: 'string' as NodePropertyTypes, + default: '', + description: 'The team secret is the last part of your SIGNL4 webhook URL.' + }, + ]; +} diff --git a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts index b56699fe680..0426ceee02d 100644 --- a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts @@ -6,15 +6,12 @@ import { //https://api.slack.com/authentication/oauth-v2 const userScopes = [ 'chat:write', - 'conversations:history', - 'conversations:read', 'files:read', 'files:write', 'stars:read', 'stars:write', ]; - export class SlackOAuth2Api implements ICredentialType { name = 'slackOAuth2Api'; extends = [ diff --git a/packages/nodes-base/credentials/SpotifyOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SpotifyOAuth2Api.credentials.ts new file mode 100644 index 00000000000..1dc9f1057a4 --- /dev/null +++ b/packages/nodes-base/credentials/SpotifyOAuth2Api.credentials.ts @@ -0,0 +1,53 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class SpotifyOAuth2Api implements ICredentialType { + name = 'spotifyOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Spotify OAuth2 API'; + properties = [ + { + displayName: 'Spotify Server', + name: 'server', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.spotify.com/', + }, + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://accounts.spotify.com/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://accounts.spotify.com/api/token', + required: true, + }, + { + 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', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + } + ]; +} diff --git a/packages/nodes-base/credentials/SurveyMonkeyOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SurveyMonkeyOAuth2Api.credentials.ts new file mode 100644 index 00000000000..26949d879f1 --- /dev/null +++ b/packages/nodes-base/credentials/SurveyMonkeyOAuth2Api.credentials.ts @@ -0,0 +1,55 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'surveys_read', + 'collectors_read', + 'responses_read', + 'responses_read_detail', + 'webhooks_write', + 'webhooks_read', +]; + +export class SurveyMonkeyOAuth2Api implements ICredentialType { + name = 'surveyMonkeyOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'SurveyMonkey OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.surveymonkey.com/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.surveymonkey.com/oauth/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(','), + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body' + }, + ]; +} diff --git a/packages/nodes-base/credentials/TestOAuth2Api.credentials.ts b/packages/nodes-base/credentials/TestOAuth2Api.credentials.ts deleted file mode 100644 index 2a350faecf9..00000000000 --- a/packages/nodes-base/credentials/TestOAuth2Api.credentials.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - ICredentialType, - NodePropertyTypes, -} from 'n8n-workflow'; - -const scopes = [ - 'https://www.googleapis.com/auth/calendar', - 'https://www.googleapis.com/auth/calendar.events', -]; - -export class TestOAuth2Api implements ICredentialType { - name = 'testOAuth2Api'; - extends = [ - 'googleOAuth2Api', - ]; - displayName = 'Test OAuth2 API'; - properties = [ - { - displayName: 'Scope', - name: 'scope', - type: 'string' as NodePropertyTypes, - default: '', - placeholder: 'asdf', - }, - ]; -} diff --git a/packages/nodes-base/credentials/TwitterOAuth1Api.credentials.ts b/packages/nodes-base/credentials/TwitterOAuth1Api.credentials.ts new file mode 100644 index 00000000000..60c1d70064b --- /dev/null +++ b/packages/nodes-base/credentials/TwitterOAuth1Api.credentials.ts @@ -0,0 +1,38 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class TwitterOAuth1Api implements ICredentialType { + name = 'twitterOAuth1Api'; + extends = [ + 'oAuth1Api', + ]; + displayName = 'Twitter OAuth API'; + properties = [ + { + displayName: 'Request Token URL', + name: 'requestTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.twitter.com/oauth/request_token', + }, + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.twitter.com/oauth/authorize', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.twitter.com/oauth/access_token', + }, + { + displayName: 'Signature Method', + name: 'signatureMethod', + type: 'hidden' as NodePropertyTypes, + default: 'HMAC-SHA1', + }, + ]; +} diff --git a/packages/nodes-base/credentials/TypeformOAuth2Api.credentials.ts b/packages/nodes-base/credentials/TypeformOAuth2Api.credentials.ts new file mode 100644 index 00000000000..a876e87ed08 --- /dev/null +++ b/packages/nodes-base/credentials/TypeformOAuth2Api.credentials.ts @@ -0,0 +1,53 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'webhooks:write', + 'webhooks:read', + 'forms:read', +]; + + +export class TypeformOAuth2Api implements ICredentialType { + name = 'typeformOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Typeform OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.typeform.com/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.typeform.com/oauth/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(','), + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + }, + ]; +} diff --git a/packages/nodes-base/credentials/WebflowOAuth2Api.credentials.ts b/packages/nodes-base/credentials/WebflowOAuth2Api.credentials.ts new file mode 100644 index 00000000000..ba5501910ce --- /dev/null +++ b/packages/nodes-base/credentials/WebflowOAuth2Api.credentials.ts @@ -0,0 +1,50 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class WebflowOAuth2Api implements ICredentialType { + name = 'webflowOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Webflow OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://webflow.com/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.webflow.com/oauth/access_token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + description: 'For some services additional query parameters have to be set which can be defined here.', + placeholder: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + description: '', + }, + ]; +} diff --git a/packages/nodes-base/credentials/ZendeskApi.credentials.ts b/packages/nodes-base/credentials/ZendeskApi.credentials.ts index 29048c1172e..4f285912b6b 100644 --- a/packages/nodes-base/credentials/ZendeskApi.credentials.ts +++ b/packages/nodes-base/credentials/ZendeskApi.credentials.ts @@ -8,10 +8,11 @@ export class ZendeskApi implements ICredentialType { displayName = 'Zendesk API'; properties = [ { - displayName: 'URL', - name: 'url', + displayName: 'Subdomain', + name: 'subdomain', type: 'string' as NodePropertyTypes, - default: '', + description: 'The subdomain of your Zendesk work environment.', + default: 'n8n', }, { displayName: 'Email', diff --git a/packages/nodes-base/credentials/ZendeskOAuth2Api.credentials.ts b/packages/nodes-base/credentials/ZendeskOAuth2Api.credentials.ts new file mode 100644 index 00000000000..06428456e62 --- /dev/null +++ b/packages/nodes-base/credentials/ZendeskOAuth2Api.credentials.ts @@ -0,0 +1,79 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'read', + 'write', +]; + +export class ZendeskOAuth2Api implements ICredentialType { + name = 'zendeskOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Zendesk OAuth2 API'; + properties = [ + { + displayName: 'Subdomain', + name: 'subdomain', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'n8n', + description: 'The subdomain of your Zendesk work environment.', + required: true, + }, + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'string' as NodePropertyTypes, + default: 'https://{SUBDOMAIN_HERE}.zendesk.com/oauth/authorizations/new', + description: 'URL to get authorization code. Replace {SUBDOMAIN_HERE} with your subdomain.', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string' as NodePropertyTypes, + default: 'https://{SUBDOMAIN_HERE}.zendesk.com/oauth/tokens', + description: 'URL to get access token. Replace {SUBDOMAIN_HERE} with your subdomain.', + required: true, + }, + { + displayName: 'Client ID', + name: 'clientId', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Client Secret', + name: 'clientSecret', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + description: 'For some services additional query parameters have to be set which can be defined here.', + placeholder: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + description: 'Resource to consume.', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Drift/Drift.node.ts b/packages/nodes-base/nodes/Drift/Drift.node.ts index 53a9a2695e6..46afa3d5190 100644 --- a/packages/nodes-base/nodes/Drift/Drift.node.ts +++ b/packages/nodes-base/nodes/Drift/Drift.node.ts @@ -37,9 +37,44 @@ export class Drift implements INodeType { { name: 'driftApi', required: true, + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + }, + }, + }, + { + name: 'driftOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'accessToken', + description: 'The resource to operate on.', + }, { displayName: 'Resource', name: 'resource', diff --git a/packages/nodes-base/nodes/Drift/GenericFunctions.ts b/packages/nodes-base/nodes/Drift/GenericFunctions.ts index 47b38a86e35..904fc4c6b3c 100644 --- a/packages/nodes-base/nodes/Drift/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Drift/GenericFunctions.ts @@ -12,25 +12,15 @@ import { } from 'n8n-workflow'; export async function driftApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - - const credentials = this.getCredentials('driftApi'); - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - const endpoint = 'https://driftapi.com'; - let options: OptionsWithUri = { - headers: { - Authorization: `Bearer ${credentials.accessToken}`, - }, + headers: {}, method, body, qs: query, - uri: uri || `${endpoint}${resource}`, + uri: uri || `https://driftapi.com${resource}`, json: true }; + if (!Object.keys(body).length) { delete options.form; } @@ -38,11 +28,27 @@ export async function driftApiRequest(this: IExecuteFunctions | IWebhookFunction delete options.qs; } options = Object.assign({}, options, option); + + const authenticationMethod = this.getNodeParameter('authentication', 0); + try { - return await this.helpers.request!(options); + if (authenticationMethod === 'accessToken') { + const credentials = this.getCredentials('driftApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + options.headers!['Authorization'] = `Bearer ${credentials.accessToken}`; + + return await this.helpers.request!(options); + } else { + return await this.helpers.requestOAuth2!.call(this, 'driftOAuth2Api', options); + } } catch (error) { - if (error.response) { - const errorMessage = error.message || (error.response.body && error.response.body.message ); + + if (error.response && error.response.body && error.response.body.error) { + const errorMessage = error.response.body.error.message; throw new Error(`Drift error response [${error.statusCode}]: ${errorMessage}`); } throw error; diff --git a/packages/nodes-base/nodes/Eventbrite/EventbriteTrigger.node.ts b/packages/nodes-base/nodes/Eventbrite/EventbriteTrigger.node.ts index fdeae599c23..0c9158b6c3a 100644 --- a/packages/nodes-base/nodes/Eventbrite/EventbriteTrigger.node.ts +++ b/packages/nodes-base/nodes/Eventbrite/EventbriteTrigger.node.ts @@ -35,7 +35,25 @@ export class EventbriteTrigger implements INodeType { { name: 'eventbriteApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'privateKey', + ], + }, + }, + }, + { + name: 'eventbriteOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], webhooks: [ { @@ -46,6 +64,23 @@ export class EventbriteTrigger implements INodeType { }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Private Key', + value: 'privateKey', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'privateKey', + description: 'The resource to operate on.', + }, { displayName: 'Organization', name: 'organization', @@ -149,7 +184,6 @@ export class EventbriteTrigger implements INodeType { description: 'By default does the webhook-data only contain the URL to receive
the object data manually. If this option gets activated it
will resolve the data automatically.', }, ], - }; methods = { @@ -192,23 +226,39 @@ export class EventbriteTrigger implements INodeType { default: { async checkExists(this: IHookFunctions): Promise { const webhookData = this.getWorkflowStaticData('node'); - if (webhookData.webhookId === undefined) { - return false; + const webhookUrl = this.getNodeWebhookUrl('default'); + const organisation = this.getNodeParameter('organization') as string; + const actions = this.getNodeParameter('actions') as string[]; + + const endpoint = `/organizations/${organisation}/webhooks/`; + + const { webhooks } = await eventbriteApiRequest.call(this, 'GET', endpoint); + + const check = (currentActions: string[], webhookActions: string[]) => { + for (const currentAction of currentActions) { + if (!webhookActions.includes(currentAction)) { + return false; + } + } + return true; + }; + + for (const webhook of webhooks) { + if (webhook.endpoint_url === webhookUrl && check(actions, webhook.actions)) { + webhookData.webhookId = webhook.id; + return true; + } } - const endpoint = `/webhooks/${webhookData.webhookId}/`; - try { - await eventbriteApiRequest.call(this, 'GET', endpoint); - } catch (e) { - return false; - } - return true; + + return false; }, async create(this: IHookFunctions): Promise { const webhookUrl = this.getNodeWebhookUrl('default'); const webhookData = this.getWorkflowStaticData('node'); + const organisation = this.getNodeParameter('organization') as string; const event = this.getNodeParameter('event') as string; const actions = this.getNodeParameter('actions') as string[]; - const endpoint = `/webhooks/`; + const endpoint = `/organizations/${organisation}/webhooks/`; const body: IDataObject = { endpoint_url: webhookUrl, actions: actions.join(','), diff --git a/packages/nodes-base/nodes/Eventbrite/GenericFunctions.ts b/packages/nodes-base/nodes/Eventbrite/GenericFunctions.ts index 285c89b1f8c..2392a0ae209 100644 --- a/packages/nodes-base/nodes/Eventbrite/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Eventbrite/GenericFunctions.ts @@ -1,4 +1,7 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; + import { IExecuteFunctions, IExecuteSingleFunctions, @@ -6,16 +9,14 @@ import { ILoadOptionsFunctions, IWebhookFunctions, } from 'n8n-core'; -import { IDataObject } from 'n8n-workflow'; + +import { + IDataObject, +} from 'n8n-workflow'; export async function eventbriteApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('eventbriteApi'); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - let options: OptionsWithUri = { - headers: { 'Authorization': `Bearer ${credentials.apiKey}`}, + headers: {}, method, qs, body, @@ -27,14 +28,26 @@ export async function eventbriteApiRequest(this: IHookFunctions | IExecuteFuncti delete options.body; } + const authenticationMethod = this.getNodeParameter('authentication', 0); + try { - return await this.helpers.request!(options); + if (authenticationMethod === 'privateKey') { + const credentials = this.getCredentials('eventbriteApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + options.headers!['Authorization'] = `Bearer ${credentials.apiKey}`; + + return await this.helpers.request!(options); + } else { + return await this.helpers.requestOAuth2!.call(this, 'eventbriteOAuth2Api', options); + } } catch (error) { let errorMessage = error.message; if (error.response.body && error.response.body.error_description) { errorMessage = error.response.body.error_description; } - throw new Error('Eventbrite Error: ' + errorMessage); } } diff --git a/packages/nodes-base/nodes/Github/GenericFunctions.ts b/packages/nodes-base/nodes/Github/GenericFunctions.ts index f6c5d076c64..de20d34062c 100644 --- a/packages/nodes-base/nodes/Github/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Github/GenericFunctions.ts @@ -50,8 +50,8 @@ export async function githubApiRequest(this: IHookFunctions | IExecuteFunctions, const baseUrl = credentials!.server || 'https://api.github.com'; options.uri = `${baseUrl}${endpoint}`; - - return await this.helpers.requestOAuth.call(this, 'githubOAuth2Api', options); + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'githubOAuth2Api', options); } } catch (error) { if (error.statusCode === 401) { diff --git a/packages/nodes-base/nodes/Github/Github.node.ts b/packages/nodes-base/nodes/Github/Github.node.ts index 00133ec3025..035b3810911 100644 --- a/packages/nodes-base/nodes/Github/Github.node.ts +++ b/packages/nodes-base/nodes/Github/Github.node.ts @@ -244,11 +244,6 @@ export class Github implements INodeType { }, }, options: [ - { - name: 'Get Emails', - value: 'getEmails', - description: 'Returns the email addresses of a user', - }, { name: 'Get Repositories', value: 'getRepositories', diff --git a/packages/nodes-base/nodes/Github/GithubTrigger.node.ts b/packages/nodes-base/nodes/Github/GithubTrigger.node.ts index 2a14b5d3db3..1360d7536a0 100644 --- a/packages/nodes-base/nodes/Github/GithubTrigger.node.ts +++ b/packages/nodes-base/nodes/Github/GithubTrigger.node.ts @@ -34,7 +34,25 @@ export class GithubTrigger implements INodeType { { name: 'githubApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + }, + }, + }, + { + name: 'githubOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], webhooks: [ { @@ -45,6 +63,23 @@ export class GithubTrigger implements INodeType { }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'accessToken', + description: 'The resource to operate on.', + }, { displayName: 'Repository Owner', name: 'owner', diff --git a/packages/nodes-base/nodes/Gitlab/GenericFunctions.ts b/packages/nodes-base/nodes/Gitlab/GenericFunctions.ts index 8f1811b8c74..6362896cfff 100644 --- a/packages/nodes-base/nodes/Gitlab/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Gitlab/GenericFunctions.ts @@ -1,6 +1,7 @@ import { IExecuteFunctions, IHookFunctions, + ILoadOptionsFunctions, } from 'n8n-core'; import { @@ -16,7 +17,7 @@ import { * @param {object} body * @returns {Promise} */ -export async function gitlabApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query?: object): Promise { // tslint:disable-line:no-any +export async function gitlabApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: object): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('gitlabApi'); if (credentials === undefined) { throw new Error('No credentials got returned!'); @@ -34,7 +35,9 @@ export async function gitlabApiRequest(this: IHookFunctions | IExecuteFunctions, }; try { - return await this.helpers.request(options); + //@ts-ignore + return await this.helpers?.request(options); + } catch (error) { if (error.statusCode === 401) { // Return a clear error diff --git a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts index 28987c2458f..a97f2be536d 100644 --- a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts +++ b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts @@ -135,7 +135,10 @@ export class GitlabTrigger implements INodeType { // Webhook got created before so check if it still exists const owner = this.getNodeParameter('owner') as string; const repository = this.getNodeParameter('repository') as string; - const endpoint = `/projects/${owner}%2F${repository}/hooks/${webhookData.webhookId}`; + + const path = (`${owner}/${repository}`).replace(/\//g,'%2F'); + + const endpoint = `/projects/${path}/hooks/${webhookData.webhookId}`; try { await gitlabApiRequest.call(this, 'GET', endpoint, {}); @@ -175,15 +178,22 @@ export class GitlabTrigger implements INodeType { events[`${e}_events`] = true; } - const endpoint = `/projects/${owner}%2F${repository}/hooks`; + // gitlab set the push_events to true when the field it's not sent. + // set it to false when it's not picked by the user. + if (events['push_events'] === undefined) { + events['push_events'] = false; + } + + const path = (`${owner}/${repository}`).replace(/\//g,'%2F'); + + const endpoint = `/projects/${path}/hooks`; const body = { url: webhookUrl, - events, + ...events, enable_ssl_verification: false, }; - let responseData; try { responseData = await gitlabApiRequest.call(this, 'POST', endpoint, body); @@ -208,7 +218,10 @@ export class GitlabTrigger implements INodeType { if (webhookData.webhookId !== undefined) { const owner = this.getNodeParameter('owner') as string; const repository = this.getNodeParameter('repository') as string; - const endpoint = `/projects/${owner}%2F${repository}/hooks/${webhookData.webhookId}`; + + const path = (`${owner}/${repository}`).replace(/\//g,'%2F'); + + const endpoint = `/projects/${path}/hooks/${webhookData.webhookId}`; const body = {}; try { diff --git a/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts b/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts index e92582a847f..545ed841f16 100644 --- a/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts +++ b/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from "n8n-workflow"; +import { + INodeProperties, +} from 'n8n-workflow'; export const eventOperations = [ { @@ -37,37 +39,36 @@ export const eventOperations = [ name: 'Update', value: 'update', description: 'Update an event', - }, + } ], default: 'create', - description: 'The operation to perform.', - }, + description: 'The operation to perform.' + } ] as INodeProperties[]; export const eventFields = [ - -/* -------------------------------------------------------------------------- */ -/* event:create */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* event:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Calendar', name: 'calendar', type: 'options', typeOptions: { - loadOptionsMethod: 'getCalendars', + loadOptionsMethod: 'getCalendars' }, required: true, displayOptions: { show: { operation: [ - 'create', + 'create' ], resource: [ - 'event', + 'event' ], }, }, - default: '', + default: '' }, { displayName: 'Start', @@ -85,7 +86,7 @@ export const eventFields = [ }, }, default: '', - description: 'Start time of the event.', + description: 'Start time of the event.' }, { displayName: 'End', @@ -103,7 +104,7 @@ export const eventFields = [ }, }, default: '', - description: 'End time of the event.', + description: 'End time of the event.' }, { displayName: 'Use Default Reminders', @@ -119,7 +120,7 @@ export const eventFields = [ ], }, }, - default: true, + default: true }, { displayName: 'Additional Fields', @@ -153,7 +154,7 @@ export const eventFields = [ }, ], default: 'no', - description: 'Wheater the event is all day or not', + description: 'Wheater the event is all day or not' }, { displayName: 'Attendees', @@ -176,6 +177,15 @@ export const eventFields = [ default: '', description: 'The color of the event.', }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, { displayName: 'Guests Can Invite Others', name: 'guestsCanInviteOthers', @@ -239,7 +249,7 @@ export const eventFields = [ { name: 'Yearly', value: 'yearly', - }, + } ], default: '', }, @@ -254,9 +264,9 @@ export const eventFields = [ name: 'repeatHowManyTimes', type: 'number', typeOptions: { - minValue: 1, + minValue: 1 }, - default: 1, + default: 1 }, { displayName: 'Send Updates', @@ -266,7 +276,7 @@ export const eventFields = [ { name: 'All', value: 'all', - description: ' Notifications are sent to all guests', + description: 'Notifications are sent to all guests' }, { name: 'External Only', @@ -276,8 +286,8 @@ export const eventFields = [ { name: 'None', value: 'none', - description: ' No notifications are sent. This value should only be used for migration use case', - }, + description: 'No notifications are sent. This value should only be used for migration use case', + } ], description: 'Whether to send notifications about the creation of the new event', default: '', @@ -303,7 +313,7 @@ export const eventFields = [ name: 'Busy', value: 'opaque', description: ' The event does block time on the calendar.', - }, + } ], default: 'opaque', description: 'Whether the event blocks time on the calendar', @@ -316,7 +326,7 @@ export const eventFields = [ loadOptionsMethod: 'getTimezones', }, default: '', - description: 'The timezone the event will have set. By default events are schedule on timezone set in n8n.' + description: 'The timezone the event will have set. By default events are schedule on timezone set in n8n.', }, { displayName: 'Visibility', @@ -331,7 +341,7 @@ export const eventFields = [ { name: 'Default', value: 'default', - description: ' Uses the default visibility for events on the calendar.', + description: 'Uses the default visibility for events on the calendar.', }, { name: 'Private', @@ -345,7 +355,7 @@ export const eventFields = [ }, ], default: 'default', - description: 'Visibility of the event.', + description: 'Visibility of the event.' }, ], }, @@ -356,7 +366,7 @@ export const eventFields = [ default: '', placeholder: 'Add Reminder', typeOptions: { - multipleValues: true, + multipleValues: true }, required: false, displayOptions: { @@ -404,13 +414,13 @@ export const eventFields = [ default: 0, }, ], - } + }, ], - description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`, + description: `If the event doesn't use the default reminders, this lists the reminders specific to the event` }, -/* -------------------------------------------------------------------------- */ -/* event:delete */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* event:delete */ + /* -------------------------------------------------------------------------- */ { displayName: 'Calendar', name: 'calendar', @@ -429,7 +439,7 @@ export const eventFields = [ ], }, }, - default: '', + default: '' }, { displayName: 'Event ID', @@ -473,7 +483,7 @@ export const eventFields = [ { name: 'All', value: 'all', - description: ' Notifications are sent to all guests', + description: 'Notifications are sent to all guests', }, { name: 'External Only', @@ -483,17 +493,17 @@ export const eventFields = [ { name: 'None', value: 'none', - description: ' No notifications are sent. This value should only be used for migration use case', - }, + description: 'No notifications are sent. This value should only be used for migration use case', + } ], description: 'Whether to send notifications about the creation of the new event', default: '', }, ], }, -/* -------------------------------------------------------------------------- */ -/* event:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* event:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Calendar', name: 'calendar', @@ -512,7 +522,7 @@ export const eventFields = [ ], }, }, - default: '', + default: '' }, { displayName: 'Event ID', @@ -565,12 +575,12 @@ export const eventFields = [ }, default: '', description: `Time zone used in the response. The default is the time zone of the calendar.`, - }, - ], + } + ] }, -/* -------------------------------------------------------------------------- */ -/* event:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* event:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Calendar', name: 'calendar', @@ -589,7 +599,7 @@ export const eventFields = [ ], }, }, - default: '', + default: '' }, { displayName: 'Return All', @@ -678,7 +688,7 @@ export const eventFields = [ name: 'Updated', value: 'updated', description: 'Order by last modification time (ascending).', - }, + } ], default: '', description: 'The order of the events returned in the result.', @@ -743,18 +753,18 @@ export const eventFields = [ default: '', description: `Lower bound for an event's last modification time (as a RFC3339 timestamp) to filter by. When specified, entries deleted since this time will always be included regardless of showDeleted`, - }, - ], + } + ] }, -/* -------------------------------------------------------------------------- */ -/* event:update */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* event:update */ + /* -------------------------------------------------------------------------- */ { displayName: 'Calendar', name: 'calendar', type: 'options', typeOptions: { - loadOptionsMethod: 'getCalendars', + loadOptionsMethod: 'getCalendars' }, required: true, displayOptions: { @@ -800,7 +810,7 @@ export const eventFields = [ ], }, }, - default: true, + default: true }, { displayName: 'Update Fields', @@ -831,7 +841,7 @@ export const eventFields = [ { name: 'No', value: 'no', - }, + } ], default: 'no', description: 'Wheater the event is all day or not', @@ -857,6 +867,15 @@ export const eventFields = [ default: '', description: 'The color of the event.', }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, { displayName: 'End', name: 'end', @@ -927,7 +946,7 @@ export const eventFields = [ { name: 'Yearly', value: 'yearly', - }, + } ], default: '', }, @@ -971,8 +990,8 @@ export const eventFields = [ { name: 'None', value: 'none', - description: ' No notifications are sent. This value should only be used for migration use case', - }, + description: 'No notifications are sent. This value should only be used for migration use case', + } ], description: 'Whether to send notifications about the creation of the new event', default: '', @@ -1011,7 +1030,7 @@ export const eventFields = [ loadOptionsMethod: 'getTimezones', }, default: '', - description: 'The timezone the event will have set. By default events are schedule on n8n timezone ' + description: 'The timezone the event will have set. By default events are schedule on n8n timezone', }, { displayName: 'Visibility', @@ -1026,7 +1045,7 @@ export const eventFields = [ { name: 'Default', value: 'default', - description: ' Uses the default visibility for events on the calendar.', + description: 'Uses the default visibility for events on the calendar.', }, { name: 'Public', @@ -1037,7 +1056,7 @@ export const eventFields = [ name: 'Private', value: 'private', description: 'The event is private and only event attendees may view event details.', - }, + } ], default: 'default', description: 'Visibility of the event.', @@ -1051,7 +1070,7 @@ export const eventFields = [ default: '', placeholder: 'Add Reminder', typeOptions: { - multipleValues: true, + multipleValues: true }, required: false, displayOptions: { @@ -1084,7 +1103,7 @@ export const eventFields = [ { name: 'Popup', value: 'popup', - }, + } ], default: '', }, @@ -1099,8 +1118,8 @@ export const eventFields = [ default: 0, }, ], - } + }, ], description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`, - }, + } ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/Calendar/EventInterface.ts b/packages/nodes-base/nodes/Google/Calendar/EventInterface.ts index 72bf96cc80f..14cda0fe415 100644 --- a/packages/nodes-base/nodes/Google/Calendar/EventInterface.ts +++ b/packages/nodes-base/nodes/Google/Calendar/EventInterface.ts @@ -1,4 +1,6 @@ -import { IDataObject } from "n8n-workflow"; +import { + IDataObject, + } from 'n8n-workflow'; export interface IReminder { useDefault?: boolean; diff --git a/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts index b772a47d0c7..caf4c9868d4 100644 --- a/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts @@ -31,11 +31,17 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF delete options.body; } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'googleCalendarOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'googleCalendarOAuth2Api', options); } catch (error) { - if (error.response && error.response.body && error.response.body.message) { + if (error.response && error.response.body && error.response.body.error) { + + let errors = error.response.body.error.errors; + + errors = errors.map((e: IDataObject) => e.message); // Try to return the error prettier - throw new Error(`Google Calendar error response [${error.statusCode}]: ${error.response.body.message}`); + throw new Error( + `Google Calendar error response [${error.statusCode}]: ${errors.join('|')}` + ); } throw error; } diff --git a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts index 1f185935d7b..4c8208590e2 100644 --- a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts +++ b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts @@ -46,7 +46,7 @@ export class GoogleCalendar implements INodeType { { name: 'googleCalendarOAuth2Api', required: true, - }, + } ], properties: [ { @@ -60,7 +60,7 @@ export class GoogleCalendar implements INodeType { }, ], default: 'event', - description: 'The resource to operate on.', + description: 'The resource to operate on.' }, ...eventOperations, ...eventFields, @@ -71,55 +71,70 @@ export class GoogleCalendar implements INodeType { loadOptions: { // Get all the calendars to display them to user so that he can // select them easily - async getCalendars(this: ILoadOptionsFunctions): Promise { + async getCalendars( + this: ILoadOptionsFunctions + ): Promise { const returnData: INodePropertyOptions[] = []; - const calendars = await googleApiRequestAllItems.call(this, 'items', 'GET', '/calendar/v3/users/me/calendarList'); + const calendars = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + '/calendar/v3/users/me/calendarList' + ); for (const calendar of calendars) { const calendarName = calendar.summary; const calendarId = calendar.id; returnData.push({ name: calendarName, - value: calendarId, + value: calendarId }); } return returnData; }, // Get all the colors to display them to user so that he can // select them easily - async getColors(this: ILoadOptionsFunctions): Promise { + async getColors( + this: ILoadOptionsFunctions + ): Promise { const returnData: INodePropertyOptions[] = []; - const { calendar } = await googleApiRequest.call(this, 'GET', '/calendar/v3/colors'); - for (const key of Object.keys(calendar)) { - const colorName = calendar[key].background; + const { event } = await googleApiRequest.call( + this, + 'GET', + '/calendar/v3/colors' + ); + for (const key of Object.keys(event)) { + const colorName = `Background: ${event[key].background} - Foreground: ${event[key].foreground}`; const colorId = key; returnData.push({ - name: `${colorName} - ${colorId}`, - value: colorId, + name: `${colorName}`, + value: colorId }); } return returnData; }, // Get all the timezones to display them to user so that he can // select them easily - async getTimezones(this: ILoadOptionsFunctions): Promise { + async getTimezones( + this: ILoadOptionsFunctions + ): Promise { const returnData: INodePropertyOptions[] = []; for (const timezone of moment.tz.names()) { const timezoneName = timezone; const timezoneId = timezone; returnData.push({ name: timezoneName, - value: timezoneId, + value: timezoneId }); } return returnData; - }, - }, + } + } }; async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; - const length = items.length as unknown as number; + const length = (items.length as unknown) as number; const qs: IDataObject = {}; let responseData; const resource = this.getNodeParameter('resource', 0) as string; @@ -131,8 +146,14 @@ export class GoogleCalendar implements INodeType { const calendarId = this.getNodeParameter('calendar', i) as string; const start = this.getNodeParameter('start', i) as string; const end = this.getNodeParameter('end', i) as string; - const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const useDefaultReminders = this.getNodeParameter( + 'useDefaultReminders', + i + ) as boolean; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; if (additionalFields.maxAttendees) { qs.maxAttendees = additionalFields.maxAttendees as number; } @@ -145,17 +166,19 @@ export class GoogleCalendar implements INodeType { const body: IEvent = { start: { dateTime: start, - timeZone: additionalFields.timeZone || this.getTimezone(), + timeZone: additionalFields.timeZone || this.getTimezone() }, end: { dateTime: end, - timeZone: additionalFields.timeZone || this.getTimezone(), + timeZone: additionalFields.timeZone || this.getTimezone() } }; if (additionalFields.attendees) { - body.attendees = (additionalFields.attendees as string[]).map(attendee => { - return { email: attendee }; - }); + body.attendees = (additionalFields.attendees as string[]).map( + attendee => { + return { email: attendee }; + } + ); } if (additionalFields.color) { body.colorId = additionalFields.color as string; @@ -188,9 +211,12 @@ export class GoogleCalendar implements INodeType { body.visibility = additionalFields.visibility as string; } if (!useDefaultReminders) { - const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[]; + const reminders = (this.getNodeParameter( + 'remindersUi', + i + ) as IDataObject).remindersValues as IDataObject[]; body.reminders = { - useDefault: false, + useDefault: false }; if (reminders) { body.reminders.overrides = reminders; @@ -198,32 +224,54 @@ export class GoogleCalendar implements INodeType { } if (additionalFields.allday) { body.start = { - date: moment(start).utc().format('YYYY-MM-DD'), + date: moment(start) + .utc() + .format('YYYY-MM-DD') }; body.end = { - date: moment(end).utc().format('YYYY-MM-DD'), + date: moment(end) + .utc() + .format('YYYY-MM-DD') }; } //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html body.recurrence = []; - if (additionalFields.repeatHowManyTimes - && additionalFields.repeatUntil) { - throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`); + if ( + additionalFields.repeatHowManyTimes && + additionalFields.repeatUntil + ) { + throw new Error( + `You can set either 'Repeat How Many Times' or 'Repeat Until' but not both` + ); } if (additionalFields.repeatFrecuency) { - body.recurrence?.push(`FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`); + body.recurrence?.push( + `FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};` + ); } if (additionalFields.repeatHowManyTimes) { - body.recurrence?.push(`COUNT=${additionalFields.repeatHowManyTimes};`); + body.recurrence?.push( + `COUNT=${additionalFields.repeatHowManyTimes};` + ); } if (additionalFields.repeatUntil) { - body.recurrence?.push(`UNTIL=${moment(additionalFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`); + body.recurrence?.push( + `UNTIL=${moment(additionalFields.repeatUntil as string) + .utc() + .format('YYYYMMDDTHHmmss')}Z` + ); } if (body.recurrence.length !== 0) { body.recurrence = [`RRULE:${body.recurrence.join('')}`]; } - responseData = await googleApiRequest.call(this, 'POST', `/calendar/v3/calendars/${calendarId}/events`, body, qs); + responseData = await googleApiRequest.call( + this, + 'POST', + `/calendar/v3/calendars/${calendarId}/events`, + body, + qs + ); } //https://developers.google.com/calendar/v3/reference/events/delete if (operation === 'delete') { @@ -233,8 +281,13 @@ export class GoogleCalendar implements INodeType { if (options.sendUpdates) { qs.sendUpdates = options.sendUpdates as number; } - responseData = await googleApiRequest.call(this, 'DELETE', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}); - responseData = { success: true }; + responseData = await googleApiRequest.call( + this, + 'DELETE', + `/calendar/v3/calendars/${calendarId}/events/${eventId}`, + {} + ); + responseData = { success: true }; } //https://developers.google.com/calendar/v3/reference/events/get if (operation === 'get') { @@ -247,7 +300,13 @@ export class GoogleCalendar implements INodeType { if (options.timeZone) { qs.timeZone = options.timeZone as string; } - responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}, qs); + responseData = await googleApiRequest.call( + this, + 'GET', + `/calendar/v3/calendars/${calendarId}/events/${eventId}`, + {}, + qs + ); } //https://developers.google.com/calendar/v3/reference/events/list if (operation === 'getAll') { @@ -288,10 +347,23 @@ export class GoogleCalendar implements INodeType { qs.updatedMin = options.updatedMin as string; } if (returnAll) { - responseData = await googleApiRequestAllItems.call(this, 'items', 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs); + responseData = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + `/calendar/v3/calendars/${calendarId}/events`, + {}, + qs + ); } else { qs.maxResults = this.getNodeParameter('limit', i) as number; - responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs); + responseData = await googleApiRequest.call( + this, + 'GET', + `/calendar/v3/calendars/${calendarId}/events`, + {}, + qs + ); responseData = responseData.items; } } @@ -299,8 +371,14 @@ export class GoogleCalendar implements INodeType { if (operation === 'update') { const calendarId = this.getNodeParameter('calendar', i) as string; const eventId = this.getNodeParameter('eventId', i) as string; - const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean; - const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const useDefaultReminders = this.getNodeParameter( + 'useDefaultReminders', + i + ) as boolean; + const updateFields = this.getNodeParameter( + 'updateFields', + i + ) as IDataObject; if (updateFields.maxAttendees) { qs.maxAttendees = updateFields.maxAttendees as number; } @@ -314,19 +392,21 @@ export class GoogleCalendar implements INodeType { if (updateFields.start) { body.start = { dateTime: updateFields.start, - timeZone: updateFields.timeZone || this.getTimezone(), + timeZone: updateFields.timeZone || this.getTimezone() }; } if (updateFields.end) { body.end = { dateTime: updateFields.end, - timeZone: updateFields.timeZone || this.getTimezone(), + timeZone: updateFields.timeZone || this.getTimezone() }; } if (updateFields.attendees) { - body.attendees = (updateFields.attendees as string[]).map(attendee => { - return { email: attendee }; - }); + body.attendees = (updateFields.attendees as string[]).map( + attendee => { + return { email: attendee }; + } + ); } if (updateFields.color) { body.colorId = updateFields.color as string; @@ -359,46 +439,64 @@ export class GoogleCalendar implements INodeType { body.visibility = updateFields.visibility as string; } if (!useDefaultReminders) { - const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[]; + const reminders = (this.getNodeParameter( + 'remindersUi', + i + ) as IDataObject).remindersValues as IDataObject[]; body.reminders = { - useDefault: false, + useDefault: false }; if (reminders) { body.reminders.overrides = reminders; } } - if (updateFields.allday - && updateFields.start - && updateFields.end) { + if (updateFields.allday && updateFields.start && updateFields.end) { body.start = { - date: moment(updateFields.start as string).utc().format('YYYY-MM-DD'), + date: moment(updateFields.start as string) + .utc() + .format('YYYY-MM-DD') }; body.end = { - date: moment(updateFields.end as string).utc().format('YYYY-MM-DD'), + date: moment(updateFields.end as string) + .utc() + .format('YYYY-MM-DD') }; } //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html body.recurrence = []; - if (updateFields.repeatHowManyTimes - && updateFields.repeatUntil) { - throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`); + if (updateFields.repeatHowManyTimes && updateFields.repeatUntil) { + throw new Error( + `You can set either 'Repeat How Many Times' or 'Repeat Until' but not both` + ); } if (updateFields.repeatFrecuency) { - body.recurrence?.push(`FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`); + body.recurrence?.push( + `FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};` + ); } if (updateFields.repeatHowManyTimes) { body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`); } if (updateFields.repeatUntil) { - body.recurrence?.push(`UNTIL=${moment(updateFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`); + body.recurrence?.push( + `UNTIL=${moment(updateFields.repeatUntil as string) + .utc() + .format('YYYYMMDDTHHmmss')}Z` + ); } if (body.recurrence.length !== 0) { body.recurrence = [`RRULE:${body.recurrence.join('')}`]; } else { delete body.recurrence; } - responseData = await googleApiRequest.call(this, 'PATCH', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, body, qs); + responseData = await googleApiRequest.call( + this, + 'PATCH', + `/calendar/v3/calendars/${calendarId}/events/${eventId}`, + body, + qs + ); } } } diff --git a/packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts new file mode 100644 index 00000000000..7d15e492ad5 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts @@ -0,0 +1,142 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import * as moment from 'moment-timezone'; + +import * as jwt from 'jsonwebtoken'; + +export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string; + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://www.googleapis.com${resource}`, + json: true, + }; + options = Object.assign({}, options, option); + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (authenticationMethod === 'serviceAccount') { + const credentials = this.getCredentials('googleApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const { access_token } = await getAccessToken.call(this, credentials as IDataObject); + + options.headers!.Authorization = `Bearer ${access_token}`; + //@ts-ignore + return await this.helpers.request(options); + } else { + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'googleDriveOAuth2Api', options); + } + } catch (error) { + if (error.response && error.response.body && error.response.body.error) { + + let errorMessages; + + if (error.response.body.error.errors) { + // Try to return the error prettier + errorMessages = error.response.body.error.errors; + + errorMessages = errorMessages.map((errorItem: IDataObject) => errorItem.message); + + errorMessages = errorMessages.join('|'); + + } else if (error.response.body.error.message) { + errorMessages = error.response.body.error.message; + } + + throw new Error(`Google Drive error response [${error.statusCode}]: ${errorMessages}`); + } + throw error; + } +} + +export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.maxResults = 100; + + do { + responseData = await googleApiRequest.call(this, method, endpoint, body, query); + query.pageToken = responseData['nextPageToken']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['nextPageToken'] !== undefined && + responseData['nextPageToken'] !== '' + ); + + return returnData; +} + +function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, credentials: IDataObject): Promise { + //https://developers.google.com/identity/protocols/oauth2/service-account#httprest + + const scopes = [ + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/drive.appdata', + 'https://www.googleapis.com/auth/drive.photos.readonly', + ]; + + const now = moment().unix(); + + const signature = jwt.sign( + { + 'iss': credentials.email as string, + 'sub': credentials.email as string, + 'scope': scopes.join(' '), + 'aud': `https://oauth2.googleapis.com/token`, + 'iat': now, + 'exp': now + 3600, + }, + credentials.privateKey as string, + { + algorithm: 'RS256', + header: { + 'kid': credentials.privateKey as string, + 'typ': 'JWT', + 'alg': 'RS256', + }, + } + ); + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + form: { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: signature, + }, + uri: 'https://oauth2.googleapis.com/token', + json: true + }; + + //@ts-ignore + return this.helpers.request(options); +} diff --git a/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts b/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts index 387a3d7bc04..f6291b10452 100644 --- a/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts +++ b/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts @@ -1,10 +1,8 @@ -import { google } from 'googleapis'; -const { Readable } = require('stream'); - import { BINARY_ENCODING, IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, INodeTypeDescription, @@ -12,8 +10,9 @@ import { INodeType, } from 'n8n-workflow'; -import { getAuthenticationClient } from '../GoogleApi'; - +import { + googleApiRequest, +} from './GenericFunctions'; export class GoogleDrive implements INodeType { description: INodeTypeDescription = { @@ -34,9 +33,43 @@ export class GoogleDrive implements INodeType { { name: 'googleApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'serviceAccount', + ], + }, + }, + }, + { + name: 'googleDriveOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Service Account', + value: 'serviceAccount', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'serviceAccount', + }, { displayName: 'Resource', name: 'resource', @@ -764,7 +797,7 @@ export class GoogleDrive implements INodeType { { name: 'domain', value: 'domain', - description:"All files shared to the user's domain that are searchable", + description: 'All files shared to the user\'s domain that are searchable', }, { name: 'drive', @@ -813,26 +846,6 @@ export class GoogleDrive implements INodeType { const items = this.getInputData(); const returnData: IDataObject[] = []; - const credentials = this.getCredentials('googleApi'); - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - const scopes = [ - 'https://www.googleapis.com/auth/drive', - 'https://www.googleapis.com/auth/drive.appdata', - 'https://www.googleapis.com/auth/drive.photos.readonly', - ]; - - const client = await getAuthenticationClient(credentials.email as string, credentials.privateKey as string, scopes); - - const drive = google.drive({ - version: 'v3', - // @ts-ignore - auth: client, - }); - const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; @@ -857,22 +870,20 @@ export class GoogleDrive implements INodeType { const fileId = this.getNodeParameter('fileId', i) as string; - const copyOptions = { - fileId, + const body: IDataObject = { fields: queryFields, - requestBody: {} as IDataObject, }; const optionProperties = ['name', 'parents']; for (const propertyName of optionProperties) { if (options[propertyName] !== undefined) { - copyOptions.requestBody[propertyName] = options[propertyName]; + body[propertyName] = options[propertyName]; } } - const response = await drive.files.copy(copyOptions); + const response = await googleApiRequest.call(this, 'POST', `/drive/v3/files/${fileId}/copy`, body); - returnData.push(response.data as IDataObject); + returnData.push(response as IDataObject); } else if (operation === 'download') { // ---------------------------------- @@ -881,15 +892,13 @@ export class GoogleDrive implements INodeType { const fileId = this.getNodeParameter('fileId', i) as string; - const response = await drive.files.get( - { - fileId, - alt: 'media', - }, - { - responseType: 'arraybuffer', - }, - ); + const requestOptions = { + resolveWithFullResponse: true, + encoding: null, + json: false, + }; + + const response = await googleApiRequest.call(this, 'GET', `/drive/v3/files/${fileId}`, {}, { alt: 'media' }, undefined, requestOptions); let mimeType: string | undefined; if (response.headers['content-type']) { @@ -912,7 +921,7 @@ export class GoogleDrive implements INodeType { const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string; - const data = Buffer.from(response.data as string); + const data = Buffer.from(response.body as string); items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, undefined, mimeType); @@ -936,7 +945,7 @@ export class GoogleDrive implements INodeType { queryCorpora = options.corpora as string; } - let driveId : string | undefined; + let driveId: string | undefined; driveId = options.driveId as string; if (driveId === '') { driveId = undefined; @@ -988,20 +997,19 @@ export class GoogleDrive implements INodeType { const pageSize = this.getNodeParameter('limit', i) as number; - const res = await drive.files.list({ + const qs = { pageSize, orderBy: 'modifiedTime', fields: `nextPageToken, files(${queryFields})`, spaces: querySpaces, - corpora: queryCorpora, - driveId, q: queryString, - includeItemsFromAllDrives: (queryCorpora !== '' || driveId !== ''), // Actually depracated, - supportsAllDrives: (queryCorpora !== '' || driveId !== ''), // see https://developers.google.com/drive/api/v3/reference/files/list - // However until June 2020 still needs to be set, to avoid API errors. - }); + includeItemsFromAllDrives: (queryCorpora !== '' || driveId !== ''), + supportsAllDrives: (queryCorpora !== '' || driveId !== ''), + }; - const files = res!.data.files; + const response = await googleApiRequest.call(this, 'GET', `/drive/v3/files`, {}, qs); + + const files = response!.files; return [this.helpers.returnJsonArray(files as IDataObject[])]; @@ -1044,29 +1052,35 @@ export class GoogleDrive implements INodeType { const name = this.getNodeParameter('name', i) as string; const parents = this.getNodeParameter('parents', i) as string[]; - const response = await drive.files.create({ - requestBody: { - name, - originalFilename, - parents, - }, + let qs: IDataObject = { fields: queryFields, - media: { - mimeType, - body: ((buffer: Buffer) => { - const readableInstanceStream = new Readable({ - read() { - this.push(buffer); - this.push(null); - } - }); + uploadType: 'media', + }; - return readableInstanceStream; - })(body), + const requestOptions = { + headers: { + 'Content-Type': mimeType, + 'Content-Length': body.byteLength, }, - }); + encoding: null, + json: false, + }; - returnData.push(response.data as IDataObject); + let response = await googleApiRequest.call(this, 'POST', `/upload/drive/v3/files`, body, qs, undefined, requestOptions); + + body = { + mimeType, + name, + originalFilename, + }; + + qs = { + addParents: parents.join(','), + }; + + response = await googleApiRequest.call(this, 'PATCH', `/drive/v3/files/${JSON.parse(response).id}`, body, qs); + + returnData.push(response as IDataObject); } } else if (resource === 'folder') { @@ -1077,19 +1091,19 @@ export class GoogleDrive implements INodeType { const name = this.getNodeParameter('name', i) as string; - const fileMetadata = { + const body = { name, mimeType: 'application/vnd.google-apps.folder', parents: options.parents || [], }; - const response = await drive.files.create({ - // @ts-ignore - resource: fileMetadata, + const qs = { fields: queryFields, - }); + }; - returnData.push(response.data as IDataObject); + const response = await googleApiRequest.call(this, 'POST', '/drive/v3/files', body, qs); + + returnData.push(response as IDataObject); } } if (['file', 'folder'].includes(resource)) { @@ -1100,9 +1114,7 @@ export class GoogleDrive implements INodeType { const fileId = this.getNodeParameter('fileId', i) as string; - await drive.files.delete({ - fileId, - }); + const response = await googleApiRequest.call(this, 'DELETE', `/drive/v3/files/${fileId}`); // If we are still here it did succeed returnData.push({ diff --git a/packages/nodes-base/nodes/Google/GoogleApi.ts b/packages/nodes-base/nodes/Google/GoogleApi.ts deleted file mode 100644 index 64ba9f9bec5..00000000000 --- a/packages/nodes-base/nodes/Google/GoogleApi.ts +++ /dev/null @@ -1,23 +0,0 @@ - -import { JWT } from 'google-auth-library'; -import { google } from 'googleapis'; - - -/** - * Returns the authentication client needed to access spreadsheet - */ -export async function getAuthenticationClient(email: string, privateKey: string, scopes: string[]): Promise { - const client = new google.auth.JWT( - email, - undefined, - privateKey, - scopes, - undefined - ); - - // TODO: Check later if this or the above should be cached - await client.authorize(); - - // @ts-ignore - return client; -} diff --git a/packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts index e492d595473..dff54fa6a8a 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts @@ -50,7 +50,7 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF return await this.helpers.request(options); } else { //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'googleSheetsOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'googleSheetsOAuth2Api', options); } } catch (error) { if (error.response && error.response.body && error.response.body.message) { diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts index 90e4e8b1fd0..042eb391050 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts @@ -622,7 +622,7 @@ export class GoogleSheets implements INodeType { // ---------------------------------- // append // ---------------------------------- - const keyRow = this.getNodeParameter('keyRow', 0) as number; + const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10); const items = this.getInputData(); @@ -670,7 +670,7 @@ export class GoogleSheets implements INodeType { sheetId: range.sheetId, dimension: deletePropertyToDimensions[propertyName] as string, startIndex: range.startIndex, - endIndex: range.startIndex + range.amount, + endIndex: parseInt(range.startIndex.toString(), 10) + parseInt(range.amount.toString(), 10), } } }); @@ -693,8 +693,8 @@ export class GoogleSheets implements INodeType { return []; } - const dataStartRow = this.getNodeParameter('dataStartRow', 0) as number; - const keyRow = this.getNodeParameter('keyRow', 0) as number; + const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10); + const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10); const items = this.getInputData(); @@ -735,8 +735,8 @@ export class GoogleSheets implements INodeType { } ]; } else { - const dataStartRow = this.getNodeParameter('dataStartRow', 0) as number; - const keyRow = this.getNodeParameter('keyRow', 0) as number; + const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10); + const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10); returnData = sheet.structureArrayDataByColumn(sheetData, keyRow, dataStartRow); } @@ -769,8 +769,8 @@ export class GoogleSheets implements INodeType { const data = await sheet.batchUpdate(updateData, valueInputMode); } else { const keyName = this.getNodeParameter('key', 0) as string; - const keyRow = this.getNodeParameter('keyRow', 0) as number; - const dataStartRow = this.getNodeParameter('dataStartRow', 0) as number; + const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10); + const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10); const setData: IDataObject[] = []; items.forEach((item) => { diff --git a/packages/nodes-base/nodes/Google/Task/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Task/GenericFunctions.ts new file mode 100644 index 00000000000..c3a50315114 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Task/GenericFunctions.ts @@ -0,0 +1,92 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function googleApiRequest( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: any = {}, + qs: IDataObject = {}, + uri?: string, + headers: IDataObject = {} +): Promise { + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json' + }, + method, + body, + qs, + uri: uri || `https://www.googleapis.com${resource}`, + json: true + }; + + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth2.call( + this, + 'googleTasksOAuth2Api', + options + ); + } catch (error) { + if (error.response && error.response.body && error.response.body.error) { + + let errors = error.response.body.error.errors; + + errors = errors.map((e: IDataObject) => e.message); + // Try to return the error prettier + throw new Error( + `Google Tasks error response [${error.statusCode}]: ${errors.join('|')}` + ); + } + throw error; + } +} + +export async function googleApiRequestAllItems( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + body: any = {}, + query: IDataObject = {} +): Promise { + const returnData: IDataObject[] = []; + + let responseData; + query.maxResults = 100; + + do { + responseData = await googleApiRequest.call( + this, + method, + endpoint, + body, + query + ); + query.pageToken = responseData['nextPageToken']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['nextPageToken'] !== undefined && + responseData['nextPageToken'] !== '' + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts b/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts new file mode 100644 index 00000000000..8e2f06dcf5a --- /dev/null +++ b/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts @@ -0,0 +1,283 @@ +import { + IExecuteFunctions, + } from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + googleApiRequest, + googleApiRequestAllItems, +} from './GenericFunctions'; + +import { + taskOperations, + taskFields, +} from './TaskDescription'; + +export class GoogleTasks implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Tasks', + name: 'googleTasks', + icon: 'file:googleTasks.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Google Tasks API.', + defaults: { + name: 'Google Tasks', + color: '#3E87E4' + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'googleTasksOAuth2Api', + required: true + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Task', + value: 'task' + } + ], + default: 'task', + description: 'The resource to operate on.' + }, + ...taskOperations, + ...taskFields + ] + }; + methods = { + loadOptions: { + // Get all the tasklists to display them to user so that he can select them easily + + async getTasks( + this: ILoadOptionsFunctions + ): Promise { + const returnData: INodePropertyOptions[] = []; + const tasks = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + '/tasks/v1/users/@me/lists' + ); + for (const task of tasks) { + const taskName = task.title; + const taskId = task.id; + returnData.push({ + name: taskName, + value: taskId + }); + } + return returnData; + } + } + }; + + async execute(this: IExecuteFunctions): Promise { + 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; + let body: IDataObject = {}; + for (let i = 0; i < length; i++) { + if (resource === 'task') { + if (operation === 'create') { + body = {}; + //https://developers.google.com/tasks/v1/reference/tasks/insert + const taskId = this.getNodeParameter('task', i) as string; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + + if (additionalFields.parent) { + qs.parent = additionalFields.parent as string; + } + if (additionalFields.previous) { + qs.previous = additionalFields.previous as string; + } + + if (additionalFields.status) { + body.status = additionalFields.status as string; + } + + if (additionalFields.notes) { + body.notes = additionalFields.notes as string; + } + + if (additionalFields.title) { + body.title = additionalFields.title as string; + } + + if (additionalFields.dueDate) { + body.dueDate = additionalFields.dueDate as string; + } + + if (additionalFields.completed) { + body.completed = additionalFields.completed as string; + } + + if (additionalFields.deleted) { + body.deleted = additionalFields.deleted as boolean; + } + + responseData = await googleApiRequest.call( + this, + 'POST', + `/tasks/v1/lists/${taskId}/tasks`, + body, + qs + ); + } + if (operation === 'delete') { + //https://developers.google.com/tasks/v1/reference/tasks/delete + const taskListId = this.getNodeParameter('task', i) as string; + const taskId = this.getNodeParameter('taskId', i) as string; + + responseData = await googleApiRequest.call( + this, + 'DELETE', + `/tasks/v1/lists/${taskListId}/tasks/${taskId}`, + {} + ); + responseData = { success: true }; + } + if (operation === 'get') { + //https://developers.google.com/tasks/v1/reference/tasks/get + const taskListId = this.getNodeParameter('task', i) as string; + const taskId = this.getNodeParameter('taskId', i) as string; + responseData = await googleApiRequest.call( + this, + 'GET', + `/tasks/v1/lists/${taskListId}/tasks/${taskId}`, + {}, + qs + ); + } + if (operation === 'getAll') { + //https://developers.google.com/tasks/v1/reference/tasks/list + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const taskListId = this.getNodeParameter('task', i) as string; + const options = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + if (options.completedMax) { + qs.completedMax = options.completedMax as string; + } + if (options.completedMin) { + qs.completedMin = options.completedMin as string; + } + if (options.dueMin) { + qs.dueMin = options.dueMin as string; + } + if (options.dueMax) { + qs.dueMax = options.dueMax as string; + } + if (options.showCompleted) { + qs.showCompleted = options.showCompleted as boolean; + } + if (options.showDeleted) { + qs.showDeleted = options.showDeleted as boolean; + } + if (options.showHidden) { + qs.showHidden = options.showHidden as boolean; + } + if (options.updatedMin) { + qs.updatedMin = options.updatedMin as string; + } + + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + `/tasks/v1/lists/${taskListId}/tasks`, + {}, + qs + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/tasks/v1/lists/${taskListId}/tasks`, + {}, + qs + ); + responseData = responseData.items; + } + } + if (operation === 'update') { + body = {}; + //https://developers.google.com/tasks/v1/reference/tasks/patch + const taskListId = this.getNodeParameter('task', i) as string; + const taskId = this.getNodeParameter('taskId', i) as string; + const updateFields = this.getNodeParameter( + 'updateFields', + i + ) as IDataObject; + + if (updateFields.previous) { + qs.previous = updateFields.previous as string; + } + + if (updateFields.status) { + body.status = updateFields.status as string; + } + + if (updateFields.notes) { + body.notes = updateFields.notes as string; + } + + if (updateFields.title) { + body.title = updateFields.title as string; + } + + if (updateFields.dueDate) { + body.dueDate = updateFields.dueDate as string; + } + + if (updateFields.completed) { + body.completed = updateFields.completed as string; + } + + if (updateFields.deleted) { + body.deleted = updateFields.deleted as boolean; + } + + responseData = await googleApiRequest.call( + this, + 'PATCH', + `/tasks/v1/lists/${taskListId}/tasks/${taskId}`, + body, + qs + ); + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Google/Task/TaskDescription.ts b/packages/nodes-base/nodes/Google/Task/TaskDescription.ts new file mode 100644 index 00000000000..8030f0a7f33 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Task/TaskDescription.ts @@ -0,0 +1,492 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const taskOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'task', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Add a task to tasklist', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a task', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a task', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all tasks from a tasklist', + }, + { + name: 'Update', + value: 'update', + description: 'Update a task', + } + ], + default: 'create', + description: 'The operation to perform.', + } +] as INodeProperties[]; + +export const taskFields = [ + /* -------------------------------------------------------------------------- */ + /* task:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'TaskList', + name: 'task', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTasks', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'task', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'task', + ], + } + }, + options: [ + { + displayName: 'Completion Date', + name: 'completed', + type: 'dateTime', + default: '', + description: `Completion date of the task (as a RFC 3339 timestamp). This field is omitted if the task has not been completed.`, + }, + { + displayName: 'Deleted', + name: 'deleted', + type: 'boolean', + default: false, + description: 'Flag indicating whether the task has been deleted.', + }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'dateTime', + default: '', + description: 'Due date of the task.', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'Additional Notes.', + }, + { + displayName: 'Parent', + name: 'parent', + type: 'string', + default: '', + description: 'Parent task identifier. If the task is created at the top level, this parameter is omitted.', + }, + { + displayName: 'Previous', + name: 'previous', + type: 'string', + default: '', + description: 'Previous sibling task identifier. If the task is created at the first position among its siblings, this parameter is omitted.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Needs Action', + value: 'needsAction', + }, + { + name: 'Completed', + value: 'completed', + } + ], + default: '', + description: 'Current status of the task.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the task.', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* task:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'TaskList', + name: 'task', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTasks', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'task', + ], + }, + }, + default: '', + }, + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'task', + ], + }, + }, + default: '', + }, + /* -------------------------------------------------------------------------- */ + /* task:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'TaskList', + name: 'task', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTasks', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'task', + ], + } + }, + default: '', + }, + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'task', + ], + }, + }, + default: '', + }, + /* -------------------------------------------------------------------------- */ + /* task:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'TaskList', + name: 'task', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTasks', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'task', + ], + }, + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'task', + ], + }, + }, + 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: [ + 'task', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100 + }, + default: 20, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'task', + ], + }, + }, + options: [ + { + displayName: 'Completed Max', + name: 'completedMax', + type: 'dateTime', + default: '', + description: 'Upper bound for a task completion date (as a RFC 3339 timestamp) to filter by.', + }, + { + displayName: 'Completed Min', + name: 'completedMin', + type: 'dateTime', + default: '', + description: 'Lower bound for a task completion date (as a RFC 3339 timestamp) to filter by.', + }, + { + displayName: 'Due Min', + name: 'dueMin', + type: 'dateTime', + default: '', + description: 'Lower bound for a task due date (as a RFC 3339 timestamp) to filter by.', + }, + { + displayName: 'Due Max', + name: 'dueMax', + type: 'dateTime', + default: '', + description: 'Upper bound for a task due date (as a RFC 3339 timestamp) to filter by.', + }, + { + displayName: 'Show Completed', + name: 'showCompleted', + type: 'boolean', + default: true, + description: 'Flag indicating whether completed tasks are returned in the result', + }, + { + displayName: 'Show Deleted', + name: 'showDeleted', + type: 'boolean', + default: false, + description: 'Flag indicating whether deleted tasks are returned in the result', + }, + { + displayName: 'Show Hidden', + name: 'showHidden', + type: 'boolean', + default: false, + description: 'Flag indicating whether hidden tasks are returned in the result', + }, + { + displayName: 'Updated Min', + name: 'updatedMin', + type: 'dateTime', + default: '', + description: 'Lower bound for a task last modification time (as a RFC 3339 timestamp) to filter by.', + }, + ] + }, + /* -------------------------------------------------------------------------- */ + /* task:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'TaskList', + name: 'task', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTasks', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'task', + ], + }, + }, + default: '', + }, + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'task', + ], + }, + }, + default: '', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Update Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'task', + ], + } + }, + options: [ + { + displayName: 'Completion Date', + name: 'completed', + type: 'dateTime', + default: '', + description: `Completion date of the task (as a RFC 3339 timestamp). This field is omitted if the task has not been completed.`, + }, + + { + displayName: 'Deleted', + name: 'deleted', + type: 'boolean', + default: false, + description: 'Flag indicating whether the task has been deleted.', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Additional Notes.', + }, + { + displayName: 'Previous', + name: 'previous', + type: 'string', + default: '', + description: 'Previous sibling task identifier. If the task is created at the first position among its siblings, this parameter is omitted.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Needs Update', + value: 'needsAction', + }, + { + name: 'Completed', + value: 'completed', + } + ], + default: '', + description: 'Current status of the task.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the task.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/Task/googleTasks.png b/packages/nodes-base/nodes/Google/Task/googleTasks.png new file mode 100644 index 00000000000..bbc31280afd Binary files /dev/null and b/packages/nodes-base/nodes/Google/Task/googleTasks.png differ diff --git a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts index 76b64ed3e73..a43545a32ee 100644 --- a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts +++ b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts @@ -32,7 +32,7 @@ export async function helpscoutApiRequest(this: IExecuteFunctions | IExecuteSing delete options.body; } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'helpScoutOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'helpScoutOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body._embedded diff --git a/packages/nodes-base/nodes/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest.node.ts index 990c007e074..417d80eeea3 100644 --- a/packages/nodes-base/nodes/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest.node.ts @@ -801,7 +801,8 @@ export class HttpRequest implements INodeType { // Now that the options are all set make the actual http request if (oAuth2Api !== undefined) { - response = await this.helpers.requestOAuth.call(this, 'oAuth2Api', requestOptions); + //@ts-ignore + response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions); } else { response = await this.helpers.request(requestOptions); } diff --git a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts index 969c392cdd3..d8d68abfb6e 100644 --- a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts @@ -15,11 +15,12 @@ import { export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, uri?: string): Promise { // tslint:disable-line:no-any - const node = this.getNode(); - const credentialName = Object.keys(node.credentials!)[0]; - const credentials = this.getCredentials(credentialName); + let authenticationMethod = this.getNodeParameter('authentication', 0); + + if (this.getNode().type.includes('Trigger')) { + authenticationMethod = 'developerApi'; + } - query!.hapikey = credentials!.apiKey as string; const options: OptionsWithUri = { method, qs: query, @@ -28,18 +29,42 @@ export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions json: true, useQuerystring: true, }; + try { - return await this.helpers.request!(options); + if (authenticationMethod === 'apiKey') { + const credentials = this.getCredentials('hubspotApi'); + + options.qs.hapikey = credentials!.apiKey as string; + + return await this.helpers.request!(options); + } else if (authenticationMethod === 'developerApi') { + const credentials = this.getCredentials('hubspotDeveloperApi'); + + options.qs.hapikey = credentials!.apiKey as string; + + return await this.helpers.request!(options); + } else { + // @ts-ignore + return await this.helpers.requestOAuth2!.call(this, 'hubspotOAuth2Api', options, 'Bearer'); + } } catch (error) { - if (error.response && error.response.body && error.response.body.errors) { - // Try to return the error prettier - let errorMessages = error.response.body.errors; + let errorMessages; - if (errorMessages[0].message) { - // @ts-ignore - errorMessages = errorMessages.map(errorItem => errorItem.message); + if (error.response && error.response.body) { + + if (error.response.body.message) { + + errorMessages = [error.response.body.message]; + + } else if (error.response.body.errors) { + // Try to return the error prettier + errorMessages = error.response.body.errors; + + if (errorMessages[0].message) { + // @ts-ignore + errorMessages = errorMessages.map(errorItem => errorItem.message); + } } - throw new Error(`Hubspot error response [${error.statusCode}]: ${errorMessages.join('|')}`); } diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index 6c1d7be400e..adf01d1ec31 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -73,9 +73,44 @@ export class Hubspot implements INodeType { { name: 'hubspotApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'apiKey', + ], + }, + }, + }, + { + name: 'hubspotOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Key', + value: 'apiKey', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'apiKey', + description: 'The method of authentication.', + }, { displayName: 'Resource', name: 'resource', diff --git a/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts b/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts index 25028f88e8f..47b2318b36f 100644 --- a/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts +++ b/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts @@ -246,7 +246,13 @@ export class HubspotTrigger implements INodeType { }; async webhook(this: IWebhookFunctions): Promise { - const credentials = this.getCredentials('hubspotDeveloperApi'); + + const credentials = this.getCredentials('hubspotDeveloperApi') as IDataObject; + + if (credentials === undefined) { + throw new Error('No credentials found!'); + } + const req = this.getRequestObject(); const bodyData = req.body; const headerData = this.getHeaderData(); @@ -254,12 +260,18 @@ export class HubspotTrigger implements INodeType { if (headerData['x-hubspot-signature'] === undefined) { return {}; } - const hash = `${credentials!.clientSecret}${JSON.stringify(bodyData)}`; - const signature = createHash('sha256').update(hash).digest('hex'); - //@ts-ignore - if (signature !== headerData['x-hubspot-signature']) { - return {}; + + // check signare if client secret is defined + + if (credentials.clientSecret !== '') { + const hash = `${credentials!.clientSecret}${JSON.stringify(bodyData)}`; + const signature = createHash('sha256').update(hash).digest('hex'); + //@ts-ignore + if (signature !== headerData['x-hubspot-signature']) { + return {}; + } } + for (let i = 0; i < bodyData.length; i++) { const subscriptionType = bodyData[i].subscriptionType as string; if (subscriptionType.includes('contact')) { diff --git a/packages/nodes-base/nodes/Keap/GenericFunctions.ts b/packages/nodes-base/nodes/Keap/GenericFunctions.ts index c04fb057d57..d00228fadc1 100644 --- a/packages/nodes-base/nodes/Keap/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Keap/GenericFunctions.ts @@ -37,7 +37,7 @@ export async function keapApiRequest(this: IWebhookFunctions | IHookFunctions | delete options.body; } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'keapOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'keapOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body.message) { // Try to return the error prettier diff --git a/packages/nodes-base/nodes/Keap/Keap.node.ts b/packages/nodes-base/nodes/Keap/Keap.node.ts index f67c07bbc4d..3ba98cda7fb 100644 --- a/packages/nodes-base/nodes/Keap/Keap.node.ts +++ b/packages/nodes-base/nodes/Keap/Keap.node.ts @@ -104,7 +104,7 @@ import * as moment from 'moment-timezone'; export class Keap implements INodeType { description: INodeTypeDescription = { displayName: 'Keap', - name: ' keap', + name: 'keap', icon: 'file:keap.png', group: ['input'], version: 1, diff --git a/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts b/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts index 91dfcdde859..59362b50d64 100644 --- a/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts @@ -1,5 +1,5 @@ import { - OptionsWithUri, + OptionsWithUrl, } from 'request'; import { @@ -14,37 +14,54 @@ import { } from 'n8n-workflow'; export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, qs: IDataObject = {} ,headers?: object): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('mailchimpApi'); - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - const headerWithAuthentication = Object.assign({}, headers, { Authorization: `apikey ${credentials.apiKey}` }); - - if (!(credentials.apiKey as string).includes('-')) { - throw new Error('The API key is not valid!'); - } - - const datacenter = (credentials.apiKey as string).split('-').pop(); + const authenticationMethod = this.getNodeParameter('authentication', 0) as string; const host = 'api.mailchimp.com/3.0'; - const options: OptionsWithUri = { - headers: headerWithAuthentication, + const options: OptionsWithUrl = { + headers: { + 'Accept': 'application/json' + }, method, qs, - uri: `https://${datacenter}.${host}${endpoint}`, + body, + url: ``, json: true, }; - if (Object.keys(body).length !== 0) { - options.body = body; + if (Object.keys(body).length === 0) { + delete options.body; } + try { - return await this.helpers.request!(options); + if (authenticationMethod === 'apiKey') { + const credentials = this.getCredentials('mailchimpApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + options.headers = Object.assign({}, headers, { Authorization: `apikey ${credentials.apiKey}` }); + + if (!(credentials.apiKey as string).includes('-')) { + throw new Error('The API key is not valid!'); + } + + const datacenter = (credentials.apiKey as string).split('-').pop(); + options.url = `https://${datacenter}.${host}${endpoint}`; + + return await this.helpers.request!(options); + } else { + const credentials = this.getCredentials('mailchimpOAuth2Api') as IDataObject; + + const { api_endpoint } = await getMetadata.call(this, credentials.oauthTokenData as IDataObject); + + options.url = `${api_endpoint}/3.0${endpoint}`; + //@ts-ignore + return await this.helpers.requestOAuth2!.call(this, 'mailchimpOAuth2Api', options, 'Bearer'); + } } catch (error) { - if (error.response.body && error.response.body.detail) { + if (error.respose && error.response.body && error.response.body.detail) { throw new Error(`Mailchimp Error response [${error.statusCode}]: ${error.response.body.detail}`); } throw error; @@ -80,3 +97,17 @@ export function validateJSON(json: string | undefined): any { // tslint:disable- } return result; } + +function getMetadata(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, oauthTokenData: IDataObject) { + const credentials = this.getCredentials('mailchimpOAuth2Api') as IDataObject; + const options: OptionsWithUrl = { + headers: { + 'Accept': 'application/json', + 'Authorization': `OAuth ${oauthTokenData.access_token}`, + }, + method: 'GET', + url: credentials.metadataUrl as string, + json: true, + }; + return this.helpers.request!(options); +} diff --git a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts index cb54ac24e93..25dbeb9984f 100644 --- a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts +++ b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts @@ -69,9 +69,44 @@ export class Mailchimp implements INodeType { { name: 'mailchimpApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'apiKey', + ], + }, + }, + }, + { + name: 'mailchimpOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Key', + value: 'apiKey', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'apiKey', + description: 'Method of authentication.', + }, { displayName: 'Resource', name: 'resource', @@ -1536,6 +1571,7 @@ export class Mailchimp implements INodeType { responseData = { success: true }; } } + if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else { diff --git a/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts index 25eac04c202..9fbd80a0f56 100644 --- a/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts +++ b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts @@ -33,7 +33,25 @@ export class MailchimpTrigger implements INodeType { { name: 'mailchimpApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'apiKey', + ], + }, + }, + }, + { + name: 'mailchimpOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], webhooks: [ { @@ -50,6 +68,23 @@ export class MailchimpTrigger implements INodeType { } ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Key', + value: 'apiKey', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'apiKey', + description: 'Method of authentication.', + }, { displayName: 'List', name: 'list', diff --git a/packages/nodes-base/nodes/Mautic/ContactDescription.ts b/packages/nodes-base/nodes/Mautic/ContactDescription.ts index b9eb9b0f423..1ea81acbc89 100644 --- a/packages/nodes-base/nodes/Mautic/ContactDescription.ts +++ b/packages/nodes-base/nodes/Mautic/ContactDescription.ts @@ -226,6 +226,94 @@ export const contactFields = [ }, }, options: [ + { + displayName: 'Address', + name: 'addressUi', + placeholder: 'Address', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'addressValues', + displayName: 'Address', + values: [ + { + displayName: 'Address Line 1', + name: 'address1', + type: 'string', + default: '', + }, + { + displayName: 'Address Line 2', + name: 'address2', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'zipCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'B2B or B2C', + name: 'b2bOrb2c', + type: 'options', + options: [ + { + name: 'B2B', + value: 'B2B', + }, + { + name: 'B2C', + value: 'B2C', + }, + ], + default: '', + }, + { + displayName: 'CRM ID', + name: 'crmId', + type: 'string', + default: '', + }, + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + }, + { + displayName: 'Has Purchased', + name: 'hasPurchased', + type: 'boolean', + default: false, + }, { displayName: 'IP Address', name: 'ipAddress', @@ -240,6 +328,12 @@ export const contactFields = [ default: '', description: 'Date/time in UTC;', }, + { + displayName: 'Mobile', + name: 'mobile', + type: 'string', + default: '', + }, { displayName: 'Owner ID', name: 'ownerId', @@ -247,6 +341,112 @@ export const contactFields = [ default: '', description: 'ID of a Mautic user to assign this contact to', }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + }, + { + displayName: 'Prospect or Customer', + name: 'prospectOrCustomer', + type: 'options', + options: [ + { + name: 'Prospect', + value: 'Prospect', + }, + { + name: 'Customer', + value: 'Customer', + }, + ], + default: '', + }, + { + displayName: 'Sandbox', + name: 'sandbox', + type: 'boolean', + default: false, + }, + { + displayName: 'Stage', + name: 'stage', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getStages', + }, + default: '', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: '', + }, + { + displayName: 'Social Media', + name: 'socialMediaUi', + placeholder: 'Social Media', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'socialMediaValues', + displayName: 'Social Media', + values: [ + { + displayName: 'Facebook', + name: 'facebook', + type: 'string', + default: '', + }, + { + displayName: 'Foursquare', + name: 'foursquare', + type: 'string', + default: '', + }, + { + displayName: 'Instagram', + name: 'instagram', + type: 'string', + default: '', + }, + { + displayName: 'LinkedIn', + name: 'linkedIn', + type: 'string', + default: '', + }, + { + displayName: 'Skype', + name: 'skype', + type: 'string', + default: '', + }, + { + displayName: 'Twitter', + name: 'twitter', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + }, ], }, @@ -318,6 +518,103 @@ export const contactFields = [ default: '', description: 'Contact parameters', }, + { + displayName: 'Address', + name: 'addressUi', + placeholder: 'Address', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: {}, + options: [ + { + name: 'addressValues', + displayName: 'Address', + values: [ + { + displayName: 'Address Line 1', + name: 'address1', + type: 'string', + default: '', + }, + { + displayName: 'Address Line 2', + name: 'address2', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'zipCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'B2B or B2C', + name: 'b2bOrb2c', + type: 'options', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + options: [ + { + name: 'B2B', + value: 'B2B', + }, + { + name: 'B2C', + value: 'B2C', + }, + ], + default: '', + }, + { + displayName: 'CRM ID', + name: 'crmId', + type: 'string', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: '', + }, { displayName: 'Email', name: 'email', @@ -332,6 +629,19 @@ export const contactFields = [ default: '', description: 'Email address of the contact.', }, + { + displayName: 'Fax', + name: 'fax', + type: 'string', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: '', + }, { displayName: 'First Name', name: 'firstName', @@ -346,6 +656,47 @@ export const contactFields = [ default: '', description: 'First Name', }, + { + displayName: 'Has Purchased', + name: 'hasPurchased', + type: 'boolean', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: false, + }, + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: '', + description: 'IP address to associate with the contact', + }, + { + displayName: 'Last Active', + name: 'lastActive', + type: 'dateTime', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: '', + description: 'Date/time in UTC;', + }, { displayName: 'Last Name', name: 'lastName', @@ -360,6 +711,60 @@ export const contactFields = [ default: '', description: 'LastName', }, + { + displayName: 'Mobile', + name: 'mobile', + type: 'string', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: '', + }, + { + displayName: 'Owner ID', + name: 'ownerId', + type: 'string', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: '', + description: 'ID of a Mautic user to assign this contact to', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: '', + }, + { + displayName: 'Position', + name: 'position', + type: 'string', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: '', + description: 'Position', + }, { displayName: 'Primary Company', name: 'company', @@ -378,9 +783,9 @@ export const contactFields = [ description: 'Primary company', }, { - displayName: 'Position', - name: 'position', - type: 'string', + displayName: 'Prospect or Customer', + name: 'prospectOrCustomer', + type: 'options', displayOptions: { show: { '/jsonParameters': [ @@ -388,8 +793,62 @@ export const contactFields = [ ], }, }, + options: [ + { + name: 'Prospect', + value: 'Prospect', + }, + { + name: 'Customer', + value: 'Customer', + }, + ], + default: '', + }, + { + displayName: 'Sandbox', + name: 'sandbox', + type: 'boolean', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: false, + }, + { + displayName: 'Stage', + name: 'stage', + type: 'options', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getStages', + }, + default: '', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getTags', + }, default: '', - description: 'Position', }, { displayName: 'Title', @@ -405,27 +864,94 @@ export const contactFields = [ default: '', description: 'Title', }, + { + displayName: 'Social Media', + name: 'socialMediaUi', + placeholder: 'Social Media', + type: 'fixedCollection', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'socialMediaValues', + displayName: 'Social Media', + values: [ + { + displayName: 'Facebook', + name: 'facebook', + type: 'string', + default: '', + }, + { + displayName: 'Foursquare', + name: 'foursquare', + type: 'string', + default: '', + }, + { + displayName: 'Instagram', + name: 'instagram', + type: 'string', + default: '', + }, + { + displayName: 'LinkedIn', + name: 'linkedIn', + type: 'string', + default: '', + }, + { + displayName: 'Skype', + name: 'skype', + type: 'string', + default: '', + }, + { + displayName: 'Twitter', + name: 'twitter', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, + default: '', + }, { displayName: 'IP Address', name: 'ipAddress', type: 'string', + displayOptions: { + show: { + '/jsonParameters': [ + false, + ], + }, + }, default: '', description: 'IP address to associate with the contact', }, - { - displayName: 'Last Active', - name: 'lastActive', - type: 'dateTime', - default: '', - description: 'Date/time in UTC;', - }, - { - displayName: 'Owner ID', - name: 'ownerId', - type: 'string', - default: '', - description: 'ID of a Mautic user to assign this contact to', - }, ], }, diff --git a/packages/nodes-base/nodes/Mautic/GenericFunctions.ts b/packages/nodes-base/nodes/Mautic/GenericFunctions.ts index 6b179db7fd2..98616902548 100644 --- a/packages/nodes-base/nodes/Mautic/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Mautic/GenericFunctions.ts @@ -10,7 +10,6 @@ import { import { IDataObject, } from 'n8n-workflow'; -import { errors } from 'imap-simple'; interface OMauticErrorResponse { errors: Array<{ @@ -19,7 +18,7 @@ interface OMauticErrorResponse { }>; } -function getErrors(error: OMauticErrorResponse): string { +export function getErrors(error: OMauticErrorResponse): string { const returnErrors: string[] = []; for (const errorItem of error.errors) { @@ -31,23 +30,40 @@ function getErrors(error: OMauticErrorResponse): string { export async function mauticApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('mauticApi'); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - const base64Key = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64'); + const authenticationMethod = this.getNodeParameter('authentication', 0, 'credentials') as string; + const options: OptionsWithUri = { - headers: { Authorization: `Basic ${base64Key}` }, + headers: {}, method, qs: query, - uri: uri || `${credentials.url}/api${endpoint}`, + uri: uri || `/api${endpoint}`, body, json: true }; - try { - const returnData = await this.helpers.request!(options); - if (returnData.error) { + try { + + let returnData; + + if (authenticationMethod === 'credentials') { + const credentials = this.getCredentials('mauticApi') as IDataObject; + + const base64Key = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64'); + + options.headers!.Authorization = `Basic ${base64Key}`; + + options.uri = `${credentials.url}${options.uri}`; + //@ts-ignore + returnData = await this.helpers.request(options); + } else { + const credentials = this.getCredentials('mauticOAuth2Api') as IDataObject; + + options.uri = `${credentials.url}${options.uri}`; + //@ts-ignore + returnData = await this.helpers.requestOAuth2.call(this, 'mauticOAuth2Api', options); + } + + if (returnData.errors) { // They seem to to sometimes return 200 status but still error. throw new Error(getErrors(returnData)); } diff --git a/packages/nodes-base/nodes/Mautic/Mautic.node.ts b/packages/nodes-base/nodes/Mautic/Mautic.node.ts index 50abcda83ed..7bdafe7605b 100644 --- a/packages/nodes-base/nodes/Mautic/Mautic.node.ts +++ b/packages/nodes-base/nodes/Mautic/Mautic.node.ts @@ -1,5 +1,3 @@ -import { snakeCase } from 'change-case'; - import { IExecuteFunctions, } from 'n8n-core'; @@ -15,12 +13,18 @@ import { mauticApiRequest, mauticApiRequestAllItems, validateJSON, + getErrors, } from './GenericFunctions'; + import { contactFields, contactOperations, } from './ContactDescription'; +import { + snakeCase, + } from 'change-case'; + export class Mautic implements INodeType { description: INodeTypeDescription = { displayName: 'Mautic', @@ -40,9 +44,43 @@ export class Mautic implements INodeType { { name: 'mauticApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'credentials', + ], + }, + }, + }, + { + name: 'mauticOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Credentials', + value: 'credentials', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'credentials', + }, { displayName: 'Resource', name: 'resource', @@ -77,6 +115,32 @@ export class Mautic implements INodeType { } return returnData; }, + // Get all the available tags to display them to user so that he can + // select them easily + async getTags(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tags = await mauticApiRequestAllItems.call(this, 'tags', 'GET', '/tags'); + for (const tag of tags) { + returnData.push({ + name: tag.tag, + value: tag.tag, + }); + } + return returnData; + }, + // Get all the available stages to display them to user so that he can + // select them easily + async getStages(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const stages = await mauticApiRequestAllItems.call(this, 'stages', 'GET', '/stages'); + for (const stage of stages) { + returnData.push({ + name: stage.name, + value: stage.id, + }); + } + return returnData; + }, }, }; @@ -124,6 +188,62 @@ export class Mautic implements INodeType { if (additionalFields.ownerId) { body.ownerId = additionalFields.ownerId as string; } + if (additionalFields.addressUi) { + const addressValues = (additionalFields.addressUi as IDataObject).addressValues as IDataObject; + if (addressValues) { + body.address1 = addressValues.address1 as string; + body.address2 = addressValues.address2 as string; + body.city = addressValues.city as string; + body.state = addressValues.state as string; + body.country = addressValues.country as string; + body.zipcode = addressValues.zipCode as string; + } + } + if (additionalFields.socialMediaUi) { + const socialMediaValues = (additionalFields.socialMediaUi as IDataObject).socialMediaValues as IDataObject; + if (socialMediaValues) { + body.facebook = socialMediaValues.facebook as string; + body.foursquare = socialMediaValues.foursquare as string; + body.instagram = socialMediaValues.instagram as string; + body.linkedin = socialMediaValues.linkedIn as string; + body.skype = socialMediaValues.skype as string; + body.twitter = socialMediaValues.twitter as string; + } + } + if (additionalFields.b2bOrb2c) { + body.b2b_or_b2c = additionalFields.b2bOrb2c as string; + } + if (additionalFields.crmId) { + body.crm_id = additionalFields.crmId as string; + } + if (additionalFields.fax) { + body.fax = additionalFields.fax as string; + } + if (additionalFields.hasPurchased) { + body.haspurchased = additionalFields.hasPurchased as boolean; + } + if (additionalFields.mobile) { + body.mobile = additionalFields.mobile as string; + } + if (additionalFields.phone) { + body.phone = additionalFields.phone as string; + } + if (additionalFields.prospectOrCustomer) { + body.prospect_or_customer = additionalFields.prospectOrCustomer as string; + } + if (additionalFields.sandbox) { + body.sandbox = additionalFields.sandbox as boolean; + } + if (additionalFields.stage) { + body.stage = additionalFields.stage as string; + } + if (additionalFields.tags) { + body.tags = additionalFields.tags as string; + } + if (additionalFields.website) { + body.website = additionalFields.website as string; + } + responseData = await mauticApiRequest.call(this, 'POST', '/contacts/new', body); responseData = responseData.contact; } @@ -167,6 +287,61 @@ export class Mautic implements INodeType { if (updateFields.ownerId) { body.ownerId = updateFields.ownerId as string; } + if (updateFields.addressUi) { + const addressValues = (updateFields.addressUi as IDataObject).addressValues as IDataObject; + if (addressValues) { + body.address1 = addressValues.address1 as string; + body.address2 = addressValues.address2 as string; + body.city = addressValues.city as string; + body.state = addressValues.state as string; + body.country = addressValues.country as string; + body.zipcode = addressValues.zipCode as string; + } + } + if (updateFields.socialMediaUi) { + const socialMediaValues = (updateFields.socialMediaUi as IDataObject).socialMediaValues as IDataObject; + if (socialMediaValues) { + body.facebook = socialMediaValues.facebook as string; + body.foursquare = socialMediaValues.foursquare as string; + body.instagram = socialMediaValues.instagram as string; + body.linkedin = socialMediaValues.linkedIn as string; + body.skype = socialMediaValues.skype as string; + body.twitter = socialMediaValues.twitter as string; + } + } + if (updateFields.b2bOrb2c) { + body.b2b_or_b2c = updateFields.b2bOrb2c as string; + } + if (updateFields.crmId) { + body.crm_id = updateFields.crmId as string; + } + if (updateFields.fax) { + body.fax = updateFields.fax as string; + } + if (updateFields.hasPurchased) { + body.haspurchased = updateFields.hasPurchased as boolean; + } + if (updateFields.mobile) { + body.mobile = updateFields.mobile as string; + } + if (updateFields.phone) { + body.phone = updateFields.phone as string; + } + if (updateFields.prospectOrCustomer) { + body.prospect_or_customer = updateFields.prospectOrCustomer as string; + } + if (updateFields.sandbox) { + body.sandbox = updateFields.sandbox as boolean; + } + if (updateFields.stage) { + body.stage = updateFields.stage as string; + } + if (updateFields.tags) { + body.tags = updateFields.tags as string; + } + if (updateFields.website) { + body.website = updateFields.website as string; + } responseData = await mauticApiRequest.call(this, 'PATCH', `/contacts/${contactId}/edit`, body); responseData = responseData.contact; } @@ -193,6 +368,9 @@ export class Mautic implements INodeType { qs.limit = this.getNodeParameter('limit', i) as number; qs.start = 0; responseData = await mauticApiRequest.call(this, 'GET', '/contacts', {}, qs); + if (responseData.errors) { + throw new Error(getErrors(responseData)); + } responseData = responseData.contacts; responseData = Object.values(responseData); } diff --git a/packages/nodes-base/nodes/Mautic/MauticTrigger.node.ts b/packages/nodes-base/nodes/Mautic/MauticTrigger.node.ts index a3ba20f28a8..4f844e11882 100644 --- a/packages/nodes-base/nodes/Mautic/MauticTrigger.node.ts +++ b/packages/nodes-base/nodes/Mautic/MauticTrigger.node.ts @@ -38,7 +38,25 @@ export class MauticTrigger implements INodeType { { name: 'mauticApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'credentials', + ], + }, + }, + }, + { + name: 'mauticOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], webhooks: [ { @@ -49,6 +67,22 @@ export class MauticTrigger implements INodeType { }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Credentials', + value: 'credentials', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'credentials', + }, { displayName: 'Events', name: 'events', diff --git a/packages/nodes-base/nodes/MessageBird/GenericFunctions.ts b/packages/nodes-base/nodes/MessageBird/GenericFunctions.ts new file mode 100644 index 00000000000..5a7eda2210f --- /dev/null +++ b/packages/nodes-base/nodes/MessageBird/GenericFunctions.ts @@ -0,0 +1,64 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +/** + * Make an API request to Message Bird + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function messageBirdApiRequest( + this: IHookFunctions | IExecuteFunctions, + method: string, + resource: string, + body: IDataObject, + query: IDataObject = {}, +): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('messageBirdApi'); + if (credentials === undefined) { + throw new Error('No credentials returned!'); + } + + const options: OptionsWithUri = { + headers: { + Accept: 'application/json', + Authorization: `AccessKey ${credentials.accessKey}`, + }, + method, + qs: query, + body, + uri: `https://rest.messagebird.com${resource}`, + json: true, + }; + + try { + return await this.helpers.request(options); + } catch (error) { + if (error.statusCode === 401) { + throw new Error('The Message Bird credentials are not valid!'); + } + + if (error.response && error.response.body && error.response.body.errors) { + // Try to return the error prettier + const errorMessage = error.response.body.errors.map((e: IDataObject) => e.description); + + throw new Error(`MessageBird Error response [${error.statusCode}]: ${errorMessage.join('|')}`); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} diff --git a/packages/nodes-base/nodes/MessageBird/MessageBird.node.ts b/packages/nodes-base/nodes/MessageBird/MessageBird.node.ts new file mode 100644 index 00000000000..46f70c85d69 --- /dev/null +++ b/packages/nodes-base/nodes/MessageBird/MessageBird.node.ts @@ -0,0 +1,364 @@ +import { + IExecuteFunctions, + } from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, +} from 'n8n-workflow'; + +import { + messageBirdApiRequest, +} from './GenericFunctions'; + +export class MessageBird implements INodeType { + description: INodeTypeDescription = { + displayName: 'MessageBird', + name: 'messageBird', + icon: 'file:messagebird.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Sending SMS', + defaults: { + name: 'MessageBird', + color: '#2481d7', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'messageBirdApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'SMS', + value: 'sms', + }, + ], + default: 'sms', + description: 'The resource to operate on.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'sms', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send text messages (SMS)', + }, + ], + default: 'send', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // sms:send + // ---------------------------------- + { + displayName: 'From', + name: 'originator', + type: 'string', + default: '', + placeholder: '14155238886', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The number from which to send the message.', + }, + { + displayName: 'To', + name: 'recipients', + type: 'string', + default: '', + placeholder: '14155238886/+14155238886', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'All recipients separated by commas.', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The message to be send.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Fields', + default: {}, + options: [ + { + displayName: 'Created Date-time', + name: 'createdDatetime', + type: 'dateTime', + default: '', + description: 'The date and time of the creation of the message in RFC3339 format (Y-m-dTH:i:sP).', + }, + { + displayName: 'Datacoding', + name: 'datacoding', + type: 'options', + options: [ + { + name: 'Auto', + value: 'auto', + }, + { + name: 'Plain', + value: 'plain', + }, + { + name: 'Unicode', + value: 'unicode', + }, + ], + default: '', + description: 'Using unicode will limit the maximum number of characters to 70 instead of 160.', + }, + { + displayName: 'Gateway', + name: 'gateway', + type: 'number', + default: '', + description: 'The SMS route that is used to send the message.', + }, + { + displayName: 'Group IDs', + name: 'groupIds', + placeholder: '1,2', + type: 'string', + default: '', + description: 'Group IDs separated by commas, If provided recipients can be omitted.', + }, + { + displayName: 'Message Type', + name: 'mclass', + type: 'options', + placeholder: 'Permissible values from 0-3', + options: [ + { + name: 'Flash', + value: 1, + }, + { + name: 'Normal', + value: 0, + }, + ], + default: 1, + description: 'Indicated the message type. 1 is a normal message, 0 is a flash message.', + }, + { + displayName: 'Reference', + name: 'reference', + type: 'string', + default: '', + description: 'A client reference.', + }, + { + displayName: 'Report Url', + name: 'reportUrl', + type: 'string', + default: '', + description: 'The status report URL to be used on a per-message basis.
Reference is required for a status report webhook to be sent.', + }, + { + displayName: 'Scheduled Date-time', + name: 'scheduledDatetime', + type: 'dateTime', + default: '', + description: 'The scheduled date and time of the message in RFC3339 format (Y-m-dTH:i:sP).', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Binary', + value: 'binary', + }, + { + name: 'Flash', + value: 'flash', + }, + { + name: 'SMS', + value: 'sms', + }, + ], + default: '', + description: 'The type of message.
Values can be: sms, binary, or flash.', + }, + { + displayName: 'Type Details', + name: 'typeDetails', + type: 'string', + default: '', + description: 'A hash with extra information.
Is only used when a binary message is sent.', + }, + { + displayName: 'Validity', + name: 'validity', + type: 'number', + default: 1, + typeOptions: { + minValue: 1, + }, + description: 'The amount of seconds that the message is valid.', + }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + let operation: string; + let resource: string; + + // For POST + let bodyRequest: IDataObject; + // For Query string + let qs: IDataObject; + + let requestMethod; + + for (let i = 0; i < items.length; i++) { + qs = {}; + + resource = this.getNodeParameter('resource', i) as string; + operation = this.getNodeParameter('operation', i) as string; + + if (resource === 'sms') { + //https://developers.messagebird.com/api/sms-messaging/#sms-api + if (operation === 'send') { + // ---------------------------------- + // sms:send + // ---------------------------------- + + requestMethod = 'POST'; + const originator = this.getNodeParameter('originator', i) as string; + const body = this.getNodeParameter('message', i) as string; + + bodyRequest = { + recipients: [], + originator, + body + }; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + + if (additionalFields.groupIds) { + bodyRequest.groupIds = additionalFields.groupIds as string; + } + if (additionalFields.type) { + bodyRequest.type = additionalFields.type as string; + } + if (additionalFields.reference) { + bodyRequest.reference = additionalFields.reference as string; + } + if (additionalFields.reportUrl) { + bodyRequest.reportUrl = additionalFields.reportUrl as string; + } + if (additionalFields.validity) { + bodyRequest.validity = additionalFields.reportUrl as number; + } + if (additionalFields.gateway) { + bodyRequest.gateway = additionalFields.gateway as string; + } + if (additionalFields.typeDetails) { + bodyRequest.typeDetails = additionalFields.typeDetails as string; + } + if (additionalFields.datacoding) { + bodyRequest.datacoding = additionalFields.datacoding as string; + } + if (additionalFields.mclass) { + bodyRequest.mclass = additionalFields.mclass as number; + } + if (additionalFields.scheduledDatetime) { + bodyRequest.scheduledDatetime = additionalFields.scheduledDatetime as string; + } + if (additionalFields.createdDatetime) { + bodyRequest.createdDatetime = additionalFields.createdDatetime as string; + } + + const receivers = this.getNodeParameter('recipients', i) as string; + + bodyRequest.recipients = receivers.split(',').map(item => { + return parseInt(item, 10); + }); + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + + const responseData = await messageBirdApiRequest.call( + this, + requestMethod, + '/messages', + bodyRequest, + qs + ); + + returnData.push(responseData as IDataObject); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/MessageBird/messagebird.png b/packages/nodes-base/nodes/MessageBird/messagebird.png new file mode 100644 index 00000000000..006b762950d Binary files /dev/null and b/packages/nodes-base/nodes/MessageBird/messagebird.png differ diff --git a/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts index 33090090b35..4180b4336b0 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts @@ -24,7 +24,7 @@ export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSing options.headers = Object.assign({}, options.headers, headers); } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'microsoftExcelOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'microsoftExcelOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) { // Try to return the error prettier diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts index 1bd1bee202f..7040bb053ea 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts @@ -35,7 +35,7 @@ export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSing } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'microsoftOneDriveOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'microsoftOneDriveOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) { // Try to return the error prettier diff --git a/packages/nodes-base/nodes/MondayCom/BoardItemDescription.ts b/packages/nodes-base/nodes/MondayCom/BoardItemDescription.ts index c6725279edd..9537c325213 100644 --- a/packages/nodes-base/nodes/MondayCom/BoardItemDescription.ts +++ b/packages/nodes-base/nodes/MondayCom/BoardItemDescription.ts @@ -15,6 +15,21 @@ export const boardItemOperations = [ }, }, options: [ + { + name: 'Add Update', + value: 'addUpdate', + description: `Add an update to an item.`, + }, + { + name: 'Change Column Value', + value: 'changeColumnValue', + description: 'Change a column value for a board item', + }, + { + name: 'Change Multiple Column Values', + value: 'changeMultipleColumnValues', + description: 'Change multiple column values for a board item', + }, { name: 'Create', value: 'create', @@ -48,6 +63,192 @@ export const boardItemOperations = [ export const boardItemFields = [ +/* -------------------------------------------------------------------------- */ +/* boardItem:addUpdate */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Item ID', + name: 'itemId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'boardItem', + ], + operation: [ + 'addUpdate', + ], + }, + }, + description: 'The unique identifier of the item to add update to.', + }, + { + displayName: 'Update Text', + name: 'value', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'boardItem', + ], + operation: [ + 'addUpdate', + ], + }, + }, + description: 'The update text to add.', + }, +/* -------------------------------------------------------------------------- */ +/* boardItem:changeColumnValue */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Board ID', + name: 'boardId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getBoards', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'boardItem', + ], + operation: [ + 'changeColumnValue', + ], + }, + }, + description: 'The unique identifier of the board.', + }, + { + displayName: 'Item ID', + name: 'itemId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'boardItem', + ], + operation: [ + 'changeColumnValue', + ], + }, + }, + description: 'The unique identifier of the item to to change column of.', + }, + { + displayName: 'Column ID', + name: 'columnId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getColumns', + loadOptionsDependsOn: [ + 'boardId' + ], + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'boardItem', + ], + operation: [ + 'changeColumnValue', + ], + }, + }, + description: `The column's unique identifier.`, + }, + { + displayName: 'Value', + name: 'value', + type: 'json', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'boardItem', + ], + operation: [ + 'changeColumnValue', + ], + }, + }, + description: 'The column value in JSON format. Documentation can be found here.', + }, +/* -------------------------------------------------------------------------- */ +/* boardItem:changeMultipleColumnValues */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Board ID', + name: 'boardId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getBoards', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'boardItem', + ], + operation: [ + 'changeMultipleColumnValues', + ], + }, + }, + description: 'The unique identifier of the board.', + }, + { + displayName: 'Item ID', + name: 'itemId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'boardItem', + ], + operation: [ + 'changeMultipleColumnValues', + ], + }, + }, + description: `Item's ID` + }, + { + displayName: 'Column Values', + name: 'columnValues', + type: 'json', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'boardItem', + ], + operation: [ + 'changeMultipleColumnValues', + ], + }, + }, + description: 'The column fields and values in JSON format. Documentation can be found here.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, /* -------------------------------------------------------------------------- */ /* boardItem:create */ /* -------------------------------------------------------------------------- */ diff --git a/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts b/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts index f8e538e5cb1..5dc74570819 100644 --- a/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts +++ b/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts @@ -455,6 +455,84 @@ export class MondayCom implements INodeType { } } if (resource === 'boardItem') { + if (operation === 'addUpdate') { + const itemId = parseInt((this.getNodeParameter('itemId', i) as string), 10); + const value = this.getNodeParameter('value', i) as string; + + const body: IGraphqlBody = { + query: + `mutation ($itemId: Int!, $value: String!) { + create_update (item_id: $itemId, body: $value) { + id + } + }`, + variables: { + itemId, + value, + }, + }; + + responseData = await mondayComApiRequest.call(this, body); + responseData = responseData.data.create_update; + } + if (operation === 'changeColumnValue') { + const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const itemId = parseInt((this.getNodeParameter('itemId', i) as string), 10); + const columnId = this.getNodeParameter('columnId', i) as string; + const value = this.getNodeParameter('value', i) as string; + + const body: IGraphqlBody = { + query: + `mutation ($boardId: Int!, $itemId: Int!, $columnId: String!, $value: JSON!) { + change_column_value (board_id: $boardId, item_id: $itemId, column_id: $columnId, value: $value) { + id + } + }`, + variables: { + boardId, + itemId, + columnId, + }, + }; + + try { + JSON.parse(value); + } catch (e) { + throw new Error('Custom Values must be a valid JSON'); + } + body.variables.value = JSON.stringify(JSON.parse(value)); + + responseData = await mondayComApiRequest.call(this, body); + responseData = responseData.data.change_column_value; + } + if (operation === 'changeMultipleColumnValues') { + const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const itemId = parseInt((this.getNodeParameter('itemId', i) as string), 10); + const columnValues = this.getNodeParameter('columnValues', i) as string; + + const body: IGraphqlBody = { + query: + `mutation ($boardId: Int!, $itemId: Int!, $columnValues: JSON!) { + change_multiple_column_values (board_id: $boardId, item_id: $itemId, column_values: $columnValues) { + id + } + }`, + variables: { + boardId, + itemId, + }, + }; + + try { + JSON.parse(columnValues); + } catch (e) { + throw new Error('Custom Values must be a valid JSON'); + } + body.variables.columnValues = JSON.stringify(JSON.parse(columnValues)); + + responseData = await mondayComApiRequest.call(this, body); + responseData = responseData.data.change_multiple_column_values; + } if (operation === 'create') { const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); const groupId = this.getNodeParameter('groupId', i) as string; diff --git a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts index dbd7ab29295..d91a287b88f 100644 --- a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts +++ b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts @@ -32,7 +32,18 @@ export class MongoDb implements INodeType { const items = this.getInputData(); const operation = this.getNodeParameter('operation', 0) as string; - if (operation === 'find') { + if (operation === 'delete') { + // ---------------------------------- + // delete + // ---------------------------------- + + const { deletedCount } = await mdb + .collection(this.getNodeParameter('collection', 0) as string) + .deleteMany(JSON.parse(this.getNodeParameter('query', 0) as string)); + + returnItems = this.helpers.returnJsonArray([{ deletedCount }]); + + } else if (operation === 'find') { // ---------------------------------- // find // ---------------------------------- diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts index 190ff3de88d..953a45a6205 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts @@ -28,6 +28,11 @@ export const nodeDescription: INodeTypeDescription = { name: 'operation', type: 'options', options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete documents.' + }, { name: 'Find', value: 'find', @@ -57,13 +62,36 @@ export const nodeDescription: INodeTypeDescription = { description: 'MongoDB Collection' }, + // ---------------------------------- + // delete + // ---------------------------------- + { + displayName: 'Delete Query (JSON format)', + name: 'query', + type: 'json', + typeOptions: { + rows: 5 + }, + displayOptions: { + show: { + operation: [ + 'delete' + ], + }, + }, + default: '{}', + placeholder: `{ "birth": { "$gt": "1950-01-01" } }`, + required: true, + description: 'MongoDB Delete query.' + }, + // ---------------------------------- // find // ---------------------------------- { displayName: 'Query (JSON format)', name: 'query', - type: 'string', + type: 'json', typeOptions: { rows: 5 }, diff --git a/packages/nodes-base/nodes/OAuth.node.ts b/packages/nodes-base/nodes/OAuth.node.ts deleted file mode 100644 index 4f5ff5debb0..00000000000 --- a/packages/nodes-base/nodes/OAuth.node.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { OptionsWithUri } from 'request'; - -import { IExecuteFunctions } from 'n8n-core'; -import { - INodeExecutionData, - INodeType, - INodeTypeDescription, -} from 'n8n-workflow'; - -export class OAuth implements INodeType { - description: INodeTypeDescription = { - displayName: 'OAuth', - name: 'oauth', - icon: 'fa:code-branch', - group: ['input'], - version: 1, - description: 'Gets, sends data to Oauth API Endpoint and receives generic information.', - defaults: { - name: 'OAuth', - color: '#0033AA', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'oAuth2Api', - required: true, - } - ], - properties: [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - options: [ - { - name: 'Get', - value: 'get', - description: 'Returns the OAuth token data.', - }, - { - name: 'Request', - value: 'request', - description: 'Make an OAuth signed requ.', - }, - ], - default: 'get', - description: 'The operation to perform.', - }, - { - displayName: 'Request Method', - name: 'requestMethod', - type: 'options', - displayOptions: { - show: { - operation: [ - 'request', - ], - }, - }, - options: [ - { - name: 'DELETE', - value: 'DELETE' - }, - { - name: 'GET', - value: 'GET' - }, - { - name: 'HEAD', - value: 'HEAD' - }, - { - name: 'PATCH', - value: 'PATCH' - }, - { - name: 'POST', - value: 'POST' - }, - { - name: 'PUT', - value: 'PUT' - }, - ], - default: 'GET', - description: 'The request method to use.', - }, - { - displayName: 'URL', - name: 'url', - type: 'string', - displayOptions: { - show: { - operation: [ - 'request', - ], - }, - }, - default: '', - placeholder: 'http://example.com/index.html', - description: 'The URL to make the request to.', - required: true, - }, - ] - }; - - async execute(this: IExecuteFunctions): Promise { - const credentials = this.getCredentials('oAuth2Api'); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - if (credentials.oauthTokenData === undefined) { - throw new Error('OAuth credentials not connected'); - } - - const operation = this.getNodeParameter('operation', 0) as string; - if (operation === 'get') { - // credentials.oauthTokenData has the refreshToken and accessToken available - // it would be nice to have credentials.getOAuthToken() which returns the accessToken - // and also handles an error case where if the token is to be refreshed, it does so - // without knowledge of the node. - - return [this.helpers.returnJsonArray(JSON.parse(credentials.oauthTokenData as string))]; - } else if (operation === 'request') { - const url = this.getNodeParameter('url', 0) as string; - const requestMethod = this.getNodeParameter('requestMethod', 0) as string; - - // Authorization Code Grant - const requestOptions: OptionsWithUri = { - headers: { - 'User-Agent': 'some-user', - }, - method: requestMethod, - uri: url, - json: true, - }; - - const responseData = await this.helpers.requestOAuth.call(this, 'oAuth2Api', requestOptions); - return [this.helpers.returnJsonArray(responseData)]; - } else { - throw new Error('Unknown operation'); - } - } -} diff --git a/packages/nodes-base/nodes/OpenWeatherMap.node.ts b/packages/nodes-base/nodes/OpenWeatherMap.node.ts index e12e3840942..38e883e53f0 100644 --- a/packages/nodes-base/nodes/OpenWeatherMap.node.ts +++ b/packages/nodes-base/nodes/OpenWeatherMap.node.ts @@ -213,20 +213,20 @@ export class OpenWeatherMap implements INodeType { // Set base data qs = { APPID: credentials.accessToken, - units: this.getNodeParameter('format', 0) as string + units: this.getNodeParameter('format', i) as string }; // Get the location - locationSelection = this.getNodeParameter('locationSelection', 0) as string; + locationSelection = this.getNodeParameter('locationSelection', i) as string; if (locationSelection === 'cityName') { - qs.q = this.getNodeParameter('cityName', 0) as string; + qs.q = this.getNodeParameter('cityName', i) as string; } else if (locationSelection === 'cityId') { - qs.id = this.getNodeParameter('cityId', 0) as number; + qs.id = this.getNodeParameter('cityId', i) as number; } else if (locationSelection === 'coordinates') { - qs.lat = this.getNodeParameter('latitude', 0) as string; - qs.lon = this.getNodeParameter('longitude', 0) as string; + qs.lat = this.getNodeParameter('latitude', i) as string; + qs.lon = this.getNodeParameter('longitude', i) as string; } else if (locationSelection === 'zipCode') { - qs.zip = this.getNodeParameter('zipCode', 0) as string; + qs.zip = this.getNodeParameter('zipCode', i) as string; } else { throw new Error(`The locationSelection "${locationSelection}" is not known!`); } diff --git a/packages/nodes-base/nodes/PagerDuty/GenericFunctions.ts b/packages/nodes-base/nodes/PagerDuty/GenericFunctions.ts index c360114144e..f4832b923c7 100644 --- a/packages/nodes-base/nodes/PagerDuty/GenericFunctions.ts +++ b/packages/nodes-base/nodes/PagerDuty/GenericFunctions.ts @@ -19,16 +19,11 @@ import { export async function pagerDutyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('pagerDutyApi'); - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } + const authenticationMethod = this.getNodeParameter('authentication', 0); const options: OptionsWithUri = { headers: { - Accept: 'application/vnd.pagerduty+json;version=2', - Authorization: `Token token=${credentials.apiToken}`, + Accept: 'application/vnd.pagerduty+json;version=2' }, method, body, @@ -39,15 +34,30 @@ export async function pagerDutyApiRequest(this: IExecuteFunctions | IWebhookFunc arrayFormat: 'brackets', }, }; + if (!Object.keys(body).length) { delete options.form; } if (!Object.keys(query).length) { delete options.qs; } + options.headers = Object.assign({}, options.headers, headers); + try { - return await this.helpers.request!(options); + if (authenticationMethod === 'apiToken') { + const credentials = this.getCredentials('pagerDutyApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + options.headers!['Authorization'] = `Token token=${credentials.apiToken}`; + + return await this.helpers.request!(options); + } else { + return await this.helpers.requestOAuth2!.call(this, 'pagerDutyOAuth2Api', options); + } } catch (error) { if (error.response && error.response.body && error.response.body.error && error.response.body.error.errors) { // Try to return the error prettier diff --git a/packages/nodes-base/nodes/PagerDuty/PagerDuty.node.ts b/packages/nodes-base/nodes/PagerDuty/PagerDuty.node.ts index d53e5921b88..8d62b8fc6fe 100644 --- a/packages/nodes-base/nodes/PagerDuty/PagerDuty.node.ts +++ b/packages/nodes-base/nodes/PagerDuty/PagerDuty.node.ts @@ -66,9 +66,43 @@ export class PagerDuty implements INodeType { { name: 'pagerDutyApi', required: true, + displayOptions: { + show: { + authentication: [ + 'apiToken', + ], + }, + }, + }, + { + name: 'pagerDutyOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Token', + value: 'apiToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'apiToken', + }, { displayName: 'Resource', name: 'resource', diff --git a/packages/nodes-base/nodes/ReadPdf.node.ts b/packages/nodes-base/nodes/ReadPdf.node.ts index 52f149da97a..2d41583c874 100644 --- a/packages/nodes-base/nodes/ReadPdf.node.ts +++ b/packages/nodes-base/nodes/ReadPdf.node.ts @@ -51,6 +51,7 @@ export class ReadPdf implements INodeType { const binaryData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING); return { + binary: item.binary, json: await pdf(binaryData) }; } diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts index c8cd97d9fab..648735feb2b 100644 --- a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -20,7 +20,7 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin }; try { //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'salesforceOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'salesforceOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body[0] && error.response.body[0].message) { // Try to return the error prettier diff --git a/packages/nodes-base/nodes/Signl4/GenericFunctions.ts b/packages/nodes-base/nodes/Signl4/GenericFunctions.ts new file mode 100644 index 00000000000..5281ae8c25e --- /dev/null +++ b/packages/nodes-base/nodes/Signl4/GenericFunctions.ts @@ -0,0 +1,52 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import { + OptionsWithUri, + } from 'request'; + +/** + * Make an API request to SIGNL4 + * + * @param {IHookFunctions | IExecuteFunctions} this + * @param {object} message + * @returns {Promise} + */ + +export async function SIGNL4ApiRequest(this: IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + let options: OptionsWithUri = { + headers: { + 'Accept': '*/*', + }, + method, + body, + qs: query, + uri: uri || ``, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + if (!Object.keys(query).length) { + delete options.qs; + } + options = Object.assign({}, options, option); + + try { + return await this.helpers.request!(options); + } catch (error) { + + if (error.response && error.response.body && error.response.body.details) { + throw new Error(`SIGNL4 error response [${error.statusCode}]: ${error.response.body.details}`); + } + + throw error; + } +} diff --git a/packages/nodes-base/nodes/Signl4/Signl4.node.ts b/packages/nodes-base/nodes/Signl4/Signl4.node.ts new file mode 100644 index 00000000000..29d3917cf6c --- /dev/null +++ b/packages/nodes-base/nodes/Signl4/Signl4.node.ts @@ -0,0 +1,325 @@ +import { + IExecuteFunctions, + BINARY_ENCODING, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + IBinaryKeyData, +} from 'n8n-workflow'; + +import { + SIGNL4ApiRequest, +} from './GenericFunctions'; + +export class Signl4 implements INodeType { + description: INodeTypeDescription = { + displayName: 'SIGNL4', + name: 'signl4', + icon: 'file:signl4.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume SIGNL4 API.', + defaults: { + name: 'SIGNL4', + color: '#53afe8', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'signl4Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Alert', + value: 'alert', + }, + ], + default: 'alert', + description: 'The resource to operate on.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'alert', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send an alert.', + }, + ], + default: 'send', + description: 'The operation to perform.', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + required: false, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'alert', + ], + }, + }, + description: 'A more detailed description for the alert.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'alert', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Alerting Scenario', + name: 'alertingScenario', + type: 'options', + options: [ + { + name: 'Single ACK', + value: 'single_ack', + description: 'In case only one person needs to confirm this Signl.' + }, + { + name: 'Multi ACK', + value: 'multi_ack', + description: 'in case this alert must be confirmed by the number of people who are on duty at the time this Singl is raised', + }, + ], + default: 'single_ack', + required: false, + }, + { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachments', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + options: [ + { + name: 'attachmentsBinary', + displayName: 'Attachments Binary', + values: [ + { + displayName: 'Property Name', + name: 'property', + type: 'string', + placeholder: 'data', + default: '', + description: 'Name of the binary properties which contain data which should be added as attachment', + }, + ], + }, + ], + default: {}, + }, + { + displayName: 'External ID', + name: 'externalId', + type: 'string', + default: '', + description: `If the event originates from a record in a 3rd party system, use this parameter to pass
+ the unique ID of that record. That ID will be communicated in outbound webhook notifications from SIGNL4,
+ which is great for correlation/synchronization of that record with the alert.`, + }, + { + displayName: 'Filtering', + name: 'filtering', + type: 'boolean', + default: 'false', + description: `Specify a boolean value of true or false to apply event filtering for this event, or not.
+ If set to true, the event will only trigger a notification to the team, if it contains at least one keyword
+ from one of your services and system categories (i.e. it is whitelisted)`, + }, + { + displayName: 'Location', + name: 'locationFieldsUi', + type: 'fixedCollection', + placeholder: 'Add Location', + default: {}, + description: 'Transmit location information (\'latitude, longitude\') with your event and display a map in the mobile app.', + options: [ + { + name: 'locationFieldsValues', + displayName: 'Location', + values: [ + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + required: true, + description: 'The location latitude.', + default: '', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + required: true, + description: 'The location longitude.', + default: '', + }, + ], + } + ], + }, + { + displayName: 'Service', + name: 'service', + type: 'string', + default: '', + description: 'Assigns the alert to the service/system category with the specified name.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + 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++) { + if (resource === 'alert') { + //https://connect.signl4.com/webhook/docs/index.html + if (operation === 'send') { + const message = this.getNodeParameter('message', i) as string; + const additionalFields = this.getNodeParameter('additionalFields',i) as IDataObject; + + const data: IDataObject = { + message, + }; + + if (additionalFields.alertingScenario) { + data['X-S4-AlertingScenario'] = additionalFields.alertingScenario as string; + } + if (additionalFields.externalId) { + data['X-S4-ExternalID'] = additionalFields.externalId as string; + } + if (additionalFields.filtering) { + data['X-S4-Filtering'] = (additionalFields.filtering as boolean).toString(); + } + if (additionalFields.locationFieldsUi) { + const locationUi = (additionalFields.locationFieldsUi as IDataObject).locationFieldsValues as IDataObject; + if (locationUi) { + data['X-S4-Location'] = `${locationUi.latitude},${locationUi.longitude}`; + } + } + if (additionalFields.service) { + data['X-S4-Service'] = additionalFields.service as string; + } + if (additionalFields.title) { + data['title'] = additionalFields.title as string; + } + + const attachments = additionalFields.attachmentsUi as IDataObject; + + if (attachments) { + if (attachments.attachmentsBinary && items[i].binary) { + + const propertyName = (attachments.attachmentsBinary as IDataObject).property as string; + + const binaryProperty = (items[i].binary as IBinaryKeyData)[propertyName]; + + if (binaryProperty) { + + const supportedFileExtension = ['png', 'jpg', 'txt']; + + if (!supportedFileExtension.includes(binaryProperty.fileExtension as string)) { + + throw new Error(`Invalid extension, just ${supportedFileExtension.join(',')} are supported}`); + } + + data['file'] = { + value: Buffer.from(binaryProperty.data, BINARY_ENCODING), + options: { + filename: binaryProperty.fileName, + contentType: binaryProperty.mimeType, + }, + }; + + } else { + throw new Error(`Binary property ${propertyName} does not exist on input`); + } + } + } + + const credentials = this.getCredentials('signl4Api'); + + const endpoint = `https://connect.signl4.com/webhook/${credentials?.teamSecret}`; + + responseData = await SIGNL4ApiRequest.call( + this, + 'POST', + '', + {}, + {}, + endpoint, + { + formData: { + ...data, + }, + }, + ); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Signl4/signl4.png b/packages/nodes-base/nodes/Signl4/signl4.png new file mode 100644 index 00000000000..205c94a5d2b Binary files /dev/null and b/packages/nodes-base/nodes/Signl4/signl4.png differ diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts index 7d65e1a93a3..ad04536b36f 100644 --- a/packages/nodes-base/nodes/Slack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -43,7 +43,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu return await this.helpers.request(options); } else { //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'slackOAuth2Api', options, 'bearer', 'authed_user.access_token'); + return await this.helpers.requestOAuth2.call(this, 'slackOAuth2Api', options, 'bearer', 'authed_user.access_token'); } } catch (error) { if (error.statusCode === 401) { diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index d5170aba67e..8d91663155c 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -111,7 +111,7 @@ export const messageFields = [ ], }, }, - description: 'Set the bot\'s user name.', + description: 'Set the bot\'s user name. This field will be ignored if you are using a user token.', }, { displayName: 'JSON parameters', @@ -486,26 +486,6 @@ export const messageFields = [ }, description: `Timestamp of the message to be updated.`, }, - { - displayName: 'As User', - name: 'as_user', - type: 'boolean', - default: false, - displayOptions: { - show: { - authentication: [ - 'accessToken', - ], - operation: [ - 'update' - ], - resource: [ - 'message', - ], - }, - }, - description: 'Pass true to update the message as the authed user. Bot users in this context are considered authed users.', - }, { displayName: 'Update Fields', name: 'updateFields', diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 57fe569d228..befe931fb5a 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -454,6 +454,10 @@ export class Slack implements INodeType { body.username = this.getNodeParameter('username', i) as string; } + // ignore body.as_user as it's deprecated + + delete body.as_user; + if (!jsonParameters) { const attachments = this.getNodeParameter('attachments', i, []) as unknown as Attachment[]; const blocksUi = (this.getNodeParameter('blocksUi', i, []) as IDataObject).blocksValues as IDataObject[]; @@ -691,10 +695,6 @@ export class Slack implements INodeType { ts, }; - if (authentication === 'accessToken') { - body.as_user = this.getNodeParameter('as_user', i) as boolean; - } - // The node does save the fields data differently than the API // expects so fix the data befre we send the request for (const attachment of attachments) { diff --git a/packages/nodes-base/nodes/Spotify/GenericFunctions.ts b/packages/nodes-base/nodes/Spotify/GenericFunctions.ts new file mode 100644 index 00000000000..f4c86a9d2db --- /dev/null +++ b/packages/nodes-base/nodes/Spotify/GenericFunctions.ts @@ -0,0 +1,84 @@ +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +/** + * Make an API request to Spotify + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function spotifyApiRequest(this: IHookFunctions | IExecuteFunctions, + method: string, endpoint: string, body: object, query?: object, uri?: string): Promise { // tslint:disable-line:no-any + + const options: OptionsWithUri = { + method, + headers: { + 'User-Agent': 'n8n', + 'Content-Type': 'text/plain', + 'Accept': ' application/json', + }, + body, + qs: query, + uri: uri || `https://api.spotify.com/v1${endpoint}`, + json: true + }; + + try { + const credentials = this.getCredentials('spotifyOAuth2Api'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + if (Object.keys(body).length === 0) { + delete options.body; + } + + return await this.helpers.requestOAuth2.call(this, 'spotifyOAuth2Api', options); + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The Spotify credentials are not valid!'); + } + + if (error.error && error.error.error && error.error.error.message) { + // Try to return the error prettier + throw new Error(`Spotify error response [${error.error.error.status}]: ${error.error.error.message}`); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} + +export async function spotifyApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, + propertyName: string, method: string, endpoint: string, body: object, query?: object): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + let uri: string | undefined; + + do { + responseData = await spotifyApiRequest.call(this, method, endpoint, body, query, uri); + returnData.push.apply(returnData, responseData[propertyName]); + uri = responseData.next; + + } while ( + responseData['next'] !== null + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Spotify/Spotify.node.ts b/packages/nodes-base/nodes/Spotify/Spotify.node.ts new file mode 100644 index 00000000000..797020c392d --- /dev/null +++ b/packages/nodes-base/nodes/Spotify/Spotify.node.ts @@ -0,0 +1,816 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + spotifyApiRequest, + spotifyApiRequestAllItems, +} from './GenericFunctions'; + +export class Spotify implements INodeType { + description: INodeTypeDescription = { + displayName: 'Spotify', + name: 'spotify', + icon: 'file:spotify.png', + group: ['input'], + version: 1, + description: 'Access public song data via the Spotify API.', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + defaults: { + name: 'Spotify', + color: '#1DB954', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'spotifyOAuth2Api', + required: true, + }, + ], + properties: [ + // ---------------------------------------------------------- + // Resource to Operate on + // Player, Album, Artisits, Playlists, Tracks + // ---------------------------------------------------------- + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Album', + value: 'album', + }, + { + name: 'Artist', + value: 'artist', + }, + { + name: 'Player', + value: 'player', + }, + { + name: 'Playlist', + value: 'playlist', + }, + { + name: 'Track', + value: 'track', + }, + ], + default: 'player', + description: 'The resource to operate on.', + }, + // -------------------------------------------------------------------------------------------------------- + // Player Operations + // Pause, Play, Get Recently Played, Get Currently Playing, Next Song, Previous Song, Add to Queue + // -------------------------------------------------------------------------------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'player', + ], + }, + }, + options: [ + { + name: 'Add Song to Queue', + value: 'addSongToQueue', + description: 'Add a song to your queue.' + }, + { + name: 'Currently Playing', + value: 'currentlyPlaying', + description: 'Get your currently playing track.' + }, + { + name: 'Next Song', + value: 'nextSong', + description: 'Skip to your next track.' + }, + { + name: 'Pause', + value: 'pause', + description: 'Pause your music.', + }, + { + name: 'Previous Song', + value: 'previousSong', + description: 'Skip to your previous song.' + }, + { + name: 'Recently Played', + value: 'recentlyPlayed', + description: 'Get your recently played tracks.' + }, + { + name: 'Start Music', + value: 'startMusic', + description: 'Start playing a playlist, artist, or album.' + }, + ], + default: 'addSongToQueue', + description: 'The operation to perform.', + }, + { + displayName: 'Resource ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'player' + ], + operation: [ + 'startMusic', + ], + }, + }, + placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp', + description: `Enter a playlist, artist, or album URI or ID.`, + }, + { + displayName: 'Track ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'player' + ], + operation: [ + 'addSongToQueue', + ], + }, + }, + placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU', + description: `Enter a track URI or ID.`, + }, + // ----------------------------------------------- + // Album Operations + // Get an Album, Get an Album's Tracks + // ----------------------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'album', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an album by URI or ID.', + }, + { + name: `Get Tracks`, + value: 'getTracks', + description: `Get an album's tracks by URI or ID.`, + }, + ], + default: 'get', + description: 'The operation to perform.', + }, + { + displayName: 'Album ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'album', + ], + }, + }, + placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp', + description: `The album's Spotify URI or ID.`, + }, + // ------------------------------------------------------------------------------------------------------------- + // Artist Operations + // Get an Artist, Get an Artist's Related Artists, Get an Artist's Top Tracks, Get an Artist's Albums + // ------------------------------------------------------------------------------------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'artist', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an artist by URI or ID.', + }, + { + name: `Get Albums`, + value: 'getAlbums', + description: `Get an artist's albums by URI or ID.`, + }, + { + name: `Get Related Artists`, + value: 'getRelatedArtists', + description: `Get an artist's related artists by URI or ID.`, + }, + { + name: `Get Top Tracks`, + value: 'getTopTracks', + description: `Get an artist's top tracks by URI or ID.`, + }, + ], + default: 'get', + description: 'The operation to perform.', + }, + { + displayName: 'Artist ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'artist', + ], + }, + }, + placeholder: 'spotify:artist:4LLpKhyESsyAXpc4laK94U', + description: `The artist's Spotify URI or ID.`, + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: 'US', + required: true, + displayOptions: { + show: { + resource: [ + 'artist' + ], + operation: [ + 'getTopTracks', + ], + }, + }, + placeholder: 'US', + description: `Top tracks in which country? Enter the postal abbriviation.`, + }, + // ------------------------------------------------------------------------------------------------------------- + // Playlist Operations + // Get a Playlist, Get a Playlist's Tracks, Add/Remove a Song from a Playlist, Get a User's Playlists + // ------------------------------------------------------------------------------------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'playlist', + ], + }, + }, + options: [ + { + name: 'Add an Item', + value: 'add', + description: 'Add tracks from a playlist by track and playlist URI or ID.', + }, + { + name: 'Get', + value: 'get', + description: 'Get a playlist by URI or ID.', + }, + { + name: 'Get Tracks', + value: 'getTracks', + description: `Get a playlist's tracks by URI or ID.`, + }, + { + name: `Get the User's Playlists`, + value: 'getUserPlaylists', + description: `Get a user's playlists.`, + }, + { + name: 'Remove an Item', + value: 'delete', + description: 'Remove tracks from a playlist by track and playlist URI or ID.', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, + { + displayName: 'Playlist ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'playlist', + ], + operation: [ + 'add', + 'delete', + 'get', + 'getTracks', + ], + }, + }, + placeholder: 'spotify:playlist:37i9dQZF1DWUhI3iC1khPH', + description: `The playlist's Spotify URI or its ID.`, + }, + { + displayName: 'Track ID', + name: 'trackID', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'playlist', + ], + operation: [ + 'add', + 'delete', + ], + }, + }, + placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU', + description: `The track's Spotify URI or its ID. The track to add/delete from the playlist.`, + }, + // ----------------------------------------------------- + // Track Operations + // Get a Track, Get a Track's Audio Features + // ----------------------------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'track', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a track by its URI or ID.', + }, + { + name: 'Get Audio Features', + value: 'getAudioFeatures', + description: 'Get audio features for a track by URI or ID.', + }, + ], + default: 'track', + description: 'The operation to perform.', + }, + { + displayName: 'Track ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'track', + ], + }, + }, + placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU', + description: `The track's Spotify URI or ID.`, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + required: true, + displayOptions: { + show: { + resource: [ + 'album', + 'artist', + 'playlist', + ], + operation: [ + 'getTracks', + 'getAlbums', + 'getUserPlaylists', + ], + }, + }, + description: `The number of items to return.`, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + required: true, + displayOptions: { + show: { + resource: [ + 'album', + 'artist', + 'playlist' + ], + operation: [ + 'getTracks', + 'getAlbums', + 'getUserPlaylists', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + description: `The number of items to return.`, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + required: true, + displayOptions: { + show: { + resource: [ + 'player', + ], + operation: [ + 'recentlyPlayed', + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 50, + }, + description: `The number of items to return.`, + }, + ] + }; + + + async execute(this: IExecuteFunctions): Promise { + // Get all of the incoming input data to loop through + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + // For Post + let body: IDataObject; + // For Query string + let qs: IDataObject; + + let requestMethod: string; + let endpoint: string; + let returnAll: boolean; + let propertyName = ''; + let responseData; + + const operation = this.getNodeParameter('operation', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; + + // Set initial values + requestMethod = 'GET'; + endpoint = ''; + body = {}; + qs = {}; + returnAll = false; + + for(let i = 0; i < items.length; i++) { + // ----------------------------- + // Player Operations + // ----------------------------- + if( resource === 'player' ) { + if(operation === 'pause') { + requestMethod = 'PUT'; + + endpoint = `/me/player/pause`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + + } else if(operation === 'recentlyPlayed') { + requestMethod = 'GET'; + + endpoint = `/me/player/recently-played`; + + const limit = this.getNodeParameter('limit', i) as number; + + qs = { + limit, + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.items; + + } else if(operation === 'currentlyPlaying') { + requestMethod = 'GET'; + + endpoint = `/me/player/currently-playing`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + } else if(operation === 'nextSong') { + requestMethod = 'POST'; + + endpoint = `/me/player/next`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + + } else if(operation === 'previousSong') { + requestMethod = 'POST'; + + endpoint = `/me/player/previous`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + + } else if(operation === 'startMusic') { + requestMethod = 'PUT'; + + endpoint = `/me/player/play`; + + const id = this.getNodeParameter('id', i) as string; + + body.context_uri = id; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + + } else if(operation === 'addSongToQueue') { + requestMethod = 'POST'; + + endpoint = `/me/player/queue`; + + const id = this.getNodeParameter('id', i) as string; + + qs = { + uri: id + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + } + // ----------------------------- + // Album Operations + // ----------------------------- + } else if( resource === 'album') { + const uri = this.getNodeParameter('id', i) as string; + + const id = uri.replace('spotify:album:', ''); + + requestMethod = 'GET'; + + if(operation === 'get') { + endpoint = `/albums/${id}`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + } else if(operation === 'getTracks') { + endpoint = `/albums/${id}/tracks`; + + propertyName = 'tracks'; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + + propertyName = 'items'; + + if(!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + + qs = { + limit, + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.items; + } + } + // ----------------------------- + // Artist Operations + // ----------------------------- + } else if( resource === 'artist') { + const uri = this.getNodeParameter('id', i) as string; + + const id = uri.replace('spotify:artist:', ''); + + if(operation === 'getAlbums') { + + endpoint = `/artists/${id}/albums`; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + + propertyName = 'items'; + + if(!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + + qs = { + limit, + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.items; + } + } else if(operation === 'getRelatedArtists') { + + endpoint = `/artists/${id}/related-artists`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.artists; + + } else if(operation === 'getTopTracks'){ + const country = this.getNodeParameter('country', i) as string; + + qs = { + country, + }; + + endpoint = `/artists/${id}/top-tracks`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.tracks; + + } else if (operation === 'get') { + + requestMethod = 'GET'; + + endpoint = `/artists/${id}`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + } + // ----------------------------- + // Playlist Operations + // ----------------------------- + } else if( resource === 'playlist') { + if(['delete', 'get', 'getTracks', 'add'].includes(operation)) { + const uri = this.getNodeParameter('id', i) as string; + + const id = uri.replace('spotify:playlist:', ''); + + if(operation === 'delete') { + requestMethod = 'DELETE'; + const trackId = this.getNodeParameter('trackID', i) as string; + + body.tracks = [ + { + uri: `${trackId}`, + positions: [ 0 ], + }, + ]; + + endpoint = `/playlists/${id}/tracks`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + + } else if(operation === 'get') { + requestMethod = 'GET'; + + endpoint = `/playlists/${id}`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + } else if(operation === 'getTracks') { + requestMethod = 'GET'; + + endpoint = `/playlists/${id}/tracks`; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + + propertyName = 'items'; + + if(!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + + qs = { + 'limit': limit + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.items; + } + } else if(operation === 'add') { + requestMethod = 'POST'; + + const trackId = this.getNodeParameter('trackID', i) as string; + + qs = { + uris: trackId + }; + + endpoint = `/playlists/${id}/tracks`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + } + } else if(operation === 'getUserPlaylists') { + requestMethod = 'GET'; + + endpoint = '/me/playlists'; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + + propertyName = 'items'; + + if(!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + + qs = { + limit, + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.items; + } + } + // ----------------------------- + // Track Operations + // ----------------------------- + } else if( resource === 'track') { + const uri = this.getNodeParameter('id', i) as string; + + const id = uri.replace('spotify:track:', ''); + + requestMethod = 'GET'; + + if(operation === 'getAudioFeatures') { + endpoint = `/audio-features/${id}`; + } else if(operation === 'get') { + endpoint = `/tracks/${id}`; + } + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + } + + if(returnAll) { + responseData = await spotifyApiRequestAllItems.call(this, propertyName, requestMethod, endpoint, body, qs); + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Spotify/spotify.png b/packages/nodes-base/nodes/Spotify/spotify.png new file mode 100644 index 00000000000..2feeb78bbf2 Binary files /dev/null and b/packages/nodes-base/nodes/Spotify/spotify.png differ diff --git a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts index 86f999b578c..bfca4dde1b0 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts @@ -14,19 +14,13 @@ import { } from 'n8n-workflow'; export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - - const credentials = this.getCredentials('surveyMonkeyApi'); - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } + const authenticationMethod = this.getNodeParameter('authentication', 0); const endpoint = 'https://api.surveymonkey.com/v3'; let options: OptionsWithUri = { headers: { 'Content-Type': 'application/json', - 'Authorization': `bearer ${credentials.accessToken}`, }, method, body, @@ -34,6 +28,7 @@ export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookF uri: uri || `${endpoint}${resource}`, json: true }; + if (!Object.keys(body).length) { delete options.body; } @@ -41,8 +36,22 @@ export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookF delete options.qs; } options = Object.assign({}, options, option); + try { - return await this.helpers.request!(options); + if ( authenticationMethod === 'accessToken') { + const credentials = this.getCredentials('surveyMonkeyApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + // @ts-ignore + options.headers['Authorization'] = `bearer ${credentials.accessToken}`; + + return await this.helpers.request!(options); + + } else { + return await this.helpers.requestOAuth2?.call(this, 'surveyMonkeyOAuth2Api', options); + } } catch (error) { const errorMessage = error.response.body.error.message; if (errorMessage !== undefined) { diff --git a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts index efdc8dba5aa..c0f722dd8c9 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts @@ -49,6 +49,24 @@ export class SurveyMonkeyTrigger implements INodeType { { name: 'surveyMonkeyApi', required: true, + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + }, + }, + }, + { + name: 'surveyMonkeyOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, }, ], webhooks: [ @@ -66,6 +84,23 @@ export class SurveyMonkeyTrigger implements INodeType { }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'accessToken', + description: 'Method of authentication.', + }, { displayName: 'Type', name: 'objectType', @@ -453,11 +488,18 @@ export class SurveyMonkeyTrigger implements INodeType { async webhook(this: IWebhookFunctions): Promise { const event = this.getNodeParameter('event') as string; const objectType = this.getNodeParameter('objectType') as string; - const credentials = this.getCredentials('surveyMonkeyApi') as IDataObject; + const authenticationMethod = this.getNodeParameter('authentication') as string; + let credentials : IDataObject; const headerData = this.getHeaderData() as IDataObject; const req = this.getRequestObject(); const webhookName = this.getWebhookName(); + if (authenticationMethod === 'accessToken') { + credentials = this.getCredentials('surveyMonkeyApi') as IDataObject; + } else { + credentials = this.getCredentials('surveyMonkeyOAuth2Api') as IDataObject; + } + if (webhookName === 'setup') { // It is a create webhook confirmation request return {}; diff --git a/packages/nodes-base/nodes/Twitter/GenericFunctions.ts b/packages/nodes-base/nodes/Twitter/GenericFunctions.ts new file mode 100644 index 00000000000..1b793047c35 --- /dev/null +++ b/packages/nodes-base/nodes/Twitter/GenericFunctions.ts @@ -0,0 +1,76 @@ +import { + OptionsWithUrl, +} from 'request'; + +import { + IHookFunctions, + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function twitterApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + let options: OptionsWithUrl = { + method, + body, + qs, + url: uri || `https://api.twitter.com/1.1${resource}`, + json: true + }; + try { + if (Object.keys(option).length !== 0) { + options = Object.assign({}, options, option); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth1.call(this, 'twitterOAuth1Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.errors) { + // Try to return the error prettier + const errorMessages = error.response.body.errors.map((error: IDataObject) => { + return error.message; + }); + throw new Error(`Twitter error response [${error.statusCode}]: ${errorMessages.join(' | ')}`); + } + + throw error; + } +} + +export async function twitterApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.count = 100; + do { + responseData = await twitterApiRequest.call(this, method, endpoint, body, query); + query.since_id = responseData.search_metadata.max_id; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.search_metadata && + responseData.search_metadata.next_results + ); + + return returnData; +} + +export function chunks (buffer: Buffer, chunkSize: number) { + const result = []; + const len = buffer.length; + let i = 0; + + while (i < len) { + result.push(buffer.slice(i, i += chunkSize)); + } + + return result; +} + + diff --git a/packages/nodes-base/nodes/Twitter/TweetDescription.ts b/packages/nodes-base/nodes/Twitter/TweetDescription.ts new file mode 100644 index 00000000000..12cb87ebfee --- /dev/null +++ b/packages/nodes-base/nodes/Twitter/TweetDescription.ts @@ -0,0 +1,324 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const tweetOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'tweet', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new tweet', + }, + { + name: 'Search', + value: 'search', + description: 'Search tweets', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const tweetFields = [ +/* -------------------------------------------------------------------------- */ +/* tweet:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: true, + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'tweet', + ], + }, + }, + description: 'The text of the status update. URL encode as necessary. t.co link wrapping will affect character counts. ', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'tweet', + ], + }, + }, + options: [ + { + displayName: 'Attachments', + name: 'attachments', + type: 'string', + default: 'data', + description: 'Name of the binary properties which contain
data which should be added to tweet as attachment.
Multiple ones can be comma separated.', + }, + { + displayName: 'Display Coordinates', + name: 'displayCoordinates', + type: 'boolean', + default: false, + description: 'Whether or not to put a pin on the exact coordinates a Tweet has been sent from.', + }, + { + displayName: 'Location', + name: 'locationFieldsUi', + type: 'fixedCollection', + placeholder: 'Add Location', + default: {}, + description: `Subscriber location information.n`, + options: [ + { + name: 'locationFieldsValues', + displayName: 'Location', + values: [ + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + required: true, + description: 'The location latitude.', + default: '', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + required: true, + description: 'The location longitude.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Possibly Sensitive', + name: 'possiblySensitive', + type: 'boolean', + default: false, + description: 'If you upload Tweet media that might be considered sensitive content such as nudity, or medical procedures, you must set this value to true.', + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* tweet:search */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Search Text', + name: 'searchText', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: true, + default: '', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'tweet', + ], + }, + }, + description: `A UTF-8, URL-encoded search query of 500 characters maximum,
+ including operators. Queries may additionally be limited by complexity.
+ Check the searching examples here.`, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'tweet', + ], + }, + }, + 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: [ + 'search', + ], + resource: [ + 'tweet', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'tweet', + ], + }, + }, + options: [ + { + displayName: 'Include Entities', + name: 'includeEntities', + type: 'boolean', + default: false, + description: 'The entities node will not be included when set to false', + }, + { + displayName: 'Language', + name: 'lang', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLanguages', + }, + default: '', + description: 'Restricts tweets to the given language, given by an ISO 639-1 code. Language detection is best-effort.', + }, + { + displayName: 'Location', + name: 'locationFieldsUi', + type: 'fixedCollection', + placeholder: 'Add Location', + default: {}, + description: `Subscriber location information.n`, + options: [ + { + name: 'locationFieldsValues', + displayName: 'Location', + values: [ + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + required: true, + description: 'The location latitude.', + default: '', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + required: true, + description: 'The location longitude.', + default: '', + }, + { + displayName: 'Radius', + name: 'radius', + type: 'options', + options: [ + { + name: 'Milles', + value: 'mi', + }, + { + name: 'Kilometers', + value: 'km', + }, + ], + required: true, + description: 'Returns tweets by users located within a given radius of the given latitude/longitude.', + default: '', + }, + { + displayName: 'Distance', + name: 'distance', + type: 'number', + typeOptions: { + minValue: 0, + }, + required: true, + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Result Type', + name: 'resultType', + type: 'options', + options: [ + { + name: 'Mixed', + value: 'mixed', + description: 'Include both popular and real time results in the response.', + }, + { + name: 'Recent', + value: 'recent', + description: 'Return only the most recent results in the response', + }, + { + name: 'Popular', + value: 'popular', + description: 'Return only the most popular results in the response.' + }, + ], + default: 'mixed', + description: 'Specifies what type of search results you would prefer to receive', + }, + { + displayName: 'Until', + name: 'until', + type: 'dateTime', + default: '', + description: 'Returns tweets created before the given date', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Twitter/TweetInterface.ts b/packages/nodes-base/nodes/Twitter/TweetInterface.ts new file mode 100644 index 00000000000..10b16d6a2dd --- /dev/null +++ b/packages/nodes-base/nodes/Twitter/TweetInterface.ts @@ -0,0 +1,8 @@ +export interface ITweet { + display_coordinates?: boolean; + lat?: number; + long?: number; + media_ids?: string; + possibly_sensitive?: boolean; + status: string; +} diff --git a/packages/nodes-base/nodes/Twitter/Twitter.node.ts b/packages/nodes-base/nodes/Twitter/Twitter.node.ts new file mode 100644 index 00000000000..a62f9fd95bd --- /dev/null +++ b/packages/nodes-base/nodes/Twitter/Twitter.node.ts @@ -0,0 +1,280 @@ + +import { + IExecuteFunctions, + ILoadOptionsFunctions, + BINARY_ENCODING, +} from 'n8n-core'; + +import { + IBinaryKeyData, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + tweetFields, + tweetOperations, +} from './TweetDescription'; + +import { + chunks, + twitterApiRequest, + twitterApiRequestAllItems, +} from './GenericFunctions'; + +import { + ITweet, +} from './TweetInterface'; + +const ISO6391 = require('iso-639-1'); + +export class Twitter implements INodeType { + description: INodeTypeDescription = { + displayName: 'Twitter ', + name: 'twitter', + icon: 'file:twitter.png', + group: ['input', 'output'], + version: 1, + description: 'Consume Twitter API', + subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}', + defaults: { + name: 'Twitter', + color: '#1DA1F2', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'twitterOAuth1Api', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Tweet', + value: 'tweet', + }, + ], + default: 'tweet', + description: 'The resource to operate on.', + }, + // TWEET + ...tweetOperations, + ...tweetFields, + ], + }; + + methods = { + loadOptions: { + // Get all the available languages to display them to user so that he can + // select them easily + async getLanguages(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const languages = ISO6391.getAllNames(); + for (const language of languages) { + const languageName = language; + const languageId = ISO6391.getCode(language); + returnData.push({ + name: languageName, + value: languageId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + 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++) { + if (resource === 'tweet') { + // https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update + if (operation === 'create') { + const text = this.getNodeParameter('text', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: ITweet = { + status: text, + }; + + if (additionalFields.attachments) { + const mediaIds = []; + const attachments = additionalFields.attachments as string; + const uploadUri = 'https://upload.twitter.com/1.1/media/upload.json'; + + const attachmentProperties: string[] = attachments.split(',').map((propertyName) => { + return propertyName.trim(); + }); + + for (const binaryPropertyName of attachmentProperties) { + + const binaryData = items[i].binary as IBinaryKeyData; + + if (binaryData === undefined) { + throw new Error('No binary data set. So file can not be written!'); + } + + if (!binaryData[binaryPropertyName]) { + continue; + } + + let attachmentBody = {}; + let response: IDataObject = {}; + + const isAnimatedWebp = (Buffer.from(binaryData[binaryPropertyName].data, 'base64').toString().indexOf('ANMF') !== -1); + + const isImage = binaryData[binaryPropertyName].mimeType.includes('image'); + + if (isImage && isAnimatedWebp) { + throw new Error('Animated .webp images are not supported use .gif instead'); + } + + if (isImage) { + + const attachmentBody = { + media_data: binaryData[binaryPropertyName].data, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + } else { + + // https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-init + + attachmentBody = { + command: 'INIT', + total_bytes: Buffer.from(binaryData[binaryPropertyName].data, BINARY_ENCODING).byteLength, + media_type: binaryData[binaryPropertyName].mimeType, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + const mediaId = response.media_id_string; + + // break the data on 5mb chunks (max size that can be uploaded at once) + + const binaryParts = chunks(Buffer.from(binaryData[binaryPropertyName].data, BINARY_ENCODING), 5242880); + + let index = 0; + + for (const binaryPart of binaryParts) { + + //https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-append + + attachmentBody = { + name: binaryData[binaryPropertyName].fileName, + command: 'APPEND', + media_id: mediaId, + media_data: Buffer.from(binaryPart).toString('base64'), + segment_index: index, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + index++; + } + + //https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-finalize + + attachmentBody = { + command: 'FINALIZE', + media_id: mediaId, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + // data has not been uploaded yet, so wait for it to be ready + if (response.processing_info) { + const { check_after_secs } = (response.processing_info as IDataObject); + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve(); + }, (check_after_secs as number) * 1000); + }); + } + } + + mediaIds.push(response.media_id_string); + } + + body.media_ids = mediaIds.join(','); + } + + if (additionalFields.possiblySensitive) { + body.possibly_sensitive = additionalFields.possibly_sensitive as boolean; + } + + if (additionalFields.locationFieldsUi) { + const locationUi = additionalFields.locationFieldsUi as IDataObject; + if (locationUi.locationFieldsValues) { + const values = locationUi.locationFieldsValues as IDataObject; + body.lat = parseFloat(values.lalatitude as string); + body.long = parseFloat(values.lalatitude as string); + } + } + + responseData = await twitterApiRequest.call(this, 'POST', '/statuses/update.json', body); + } + // https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets + if (operation === 'search') { + const q = this.getNodeParameter('searchText', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const qs: IDataObject = { + q + }; + + if (additionalFields.includeEntities) { + qs.include_entities = additionalFields.includeEntities as boolean; + } + + if (additionalFields.resultType) { + qs.response_type = additionalFields.resultType as string; + } + + if (additionalFields.until) { + qs.until = additionalFields.until as string; + } + + if (additionalFields.lang) { + qs.lang = additionalFields.lang as string; + } + + if (additionalFields.locationFieldsUi) { + const locationUi = additionalFields.locationFieldsUi as IDataObject; + if (locationUi.locationFieldsValues) { + const values = locationUi.locationFieldsValues as IDataObject; + qs.geocode = `${values.latitude as string},${values.longitude as string},${values.distance}${values.radius}`; + } + } + + if (returnAll) { + responseData = await twitterApiRequestAllItems.call(this, 'statuses', 'GET', '/search/tweets.json', {}, qs); + } else { + qs.count = this.getNodeParameter('limit', 0) as number; + responseData = await twitterApiRequest.call(this, 'GET', '/search/tweets.json', {}, qs); + responseData = responseData.statuses; + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Twitter/twitter.png b/packages/nodes-base/nodes/Twitter/twitter.png new file mode 100644 index 00000000000..6f4436e4414 Binary files /dev/null and b/packages/nodes-base/nodes/Twitter/twitter.png differ diff --git a/packages/nodes-base/nodes/Typeform/GenericFunctions.ts b/packages/nodes-base/nodes/Typeform/GenericFunctions.ts index c5e9242465d..83ca713afe6 100644 --- a/packages/nodes-base/nodes/Typeform/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Typeform/GenericFunctions.ts @@ -45,18 +45,10 @@ export interface ITypeformAnswerField { * @returns {Promise} */ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('typeformApi'); - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - query = query || {}; + const authenticationMethod = this.getNodeParameter('authentication', 0); const options: OptionsWithUri = { - headers: { - 'Authorization': `bearer ${credentials.accessToken}`, - }, + headers: {}, method, body, qs: query, @@ -64,8 +56,23 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa json: true, }; + query = query || {}; + try { - return await this.helpers.request!(options); + if (authenticationMethod === 'accessToken') { + + const credentials = this.getCredentials('typeformApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + options.headers!['Authorization'] = `bearer ${credentials.accessToken}`; + + return await this.helpers.request!(options); + } else { + return await this.helpers.requestOAuth2!.call(this, 'typeformOAuth2Api', options); + } } catch (error) { if (error.statusCode === 401) { // Return a clear error diff --git a/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts b/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts index b127a1f5943..9c8f6e16ffa 100644 --- a/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts +++ b/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts @@ -37,7 +37,25 @@ export class TypeformTrigger implements INodeType { { name: 'typeformApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + }, + }, + }, + { + name: 'typeformOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], webhooks: [ { @@ -48,6 +66,23 @@ export class TypeformTrigger implements INodeType { }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'accessToken', + description: 'The resource to operate on.', + }, { displayName: 'Form', name: 'formId', diff --git a/packages/nodes-base/nodes/Webflow/GenericFunctions.ts b/packages/nodes-base/nodes/Webflow/GenericFunctions.ts index 030e47f9039..783b6748085 100644 --- a/packages/nodes-base/nodes/Webflow/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Webflow/GenericFunctions.ts @@ -1,4 +1,7 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; + import { IExecuteFunctions, IExecuteSingleFunctions, @@ -6,17 +9,16 @@ import { ILoadOptionsFunctions, IWebhookFunctions, } from 'n8n-core'; -import { IDataObject } from 'n8n-workflow'; + +import { + IDataObject, + } from 'n8n-workflow'; export async function webflowApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('webflowApi'); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } + const authenticationMethod = this.getNodeParameter('authentication', 0); let options: OptionsWithUri = { headers: { - authorization: `Bearer ${credentials.accessToken}`, 'accept-version': '1.0.0', }, method, @@ -31,14 +33,22 @@ export async function webflowApiRequest(this: IHookFunctions | IExecuteFunctions } try { - return await this.helpers.request!(options); - } catch (error) { + if (authenticationMethod === 'accessToken') { + const credentials = this.getCredentials('webflowApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } - let errorMessage = error.message; - if (error.response.body && error.response.body.err) { - errorMessage = error.response.body.err; + options.headers!['authorization'] = `Bearer ${credentials.accessToken}`; + + return await this.helpers.request!(options); + } else { + return await this.helpers.requestOAuth2!.call(this, 'webflowOAuth2Api', options); } - - throw new Error('Webflow Error: ' + errorMessage); + } catch (error) { + if (error.response.body.err) { + throw new Error(`Webflow Error: [${error.statusCode}]: ${error.response.body.err}`); + } + return error; } } diff --git a/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts b/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts index 709b9858cd5..73b2efef61d 100644 --- a/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts +++ b/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts @@ -34,7 +34,25 @@ export class WebflowTrigger implements INodeType { { name: 'webflowApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + }, + }, + }, + { + name: 'webflowOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], webhooks: [ { @@ -45,6 +63,23 @@ export class WebflowTrigger implements INodeType { }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'accessToken', + description: 'Method of authentication.', + }, { displayName: 'Site', name: 'site', diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index ffe371e1608..271c10259f3 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -14,26 +14,48 @@ import { } from 'n8n-workflow'; export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('zendeskApi'); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64'); + const authenticationMethod = this.getNodeParameter('authentication', 0); + let options: OptionsWithUri = { - headers: { 'Authorization': `Basic ${base64Key}`}, + headers: {}, method, qs, body, - uri: uri ||`${credentials.url}/api/v2${resource}.json`, + //@ts-ignore + uri, json: true }; + options = Object.assign({}, options, option); if (Object.keys(options.body).length === 0) { delete options.body; } + try { - return await this.helpers.request!(options); - } catch (err) { + if (authenticationMethod === 'apiToken') { + const credentials = this.getCredentials('zendeskApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64'); + options.uri = `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`; + options.headers!['Authorization'] = `Basic ${base64Key}`; + + return await this.helpers.request!(options); + } else { + const credentials = this.getCredentials('zendeskOAuth2Api'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + options.uri = `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`; + + return await this.helpers.requestOAuth2!.call(this, 'zendeskOAuth2Api', options); + } + } catch(err) { let errorMessage = err.message; if (err.response && err.response.body && err.response.body.error) { errorMessage = err.response.body.error; diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts index 8a2586d8d8c..d0c72c335f7 100644 --- a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -52,9 +52,44 @@ export class Zendesk implements INodeType { { name: 'zendeskApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'apiToken', + ], + }, + }, + }, + { + name: 'zendeskOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Token', + value: 'apiToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'apiToken', + description: 'The resource to operate on.', + }, { displayName: 'Resource', name: 'resource', diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index cc4e26dbd40..421d9e4b32f 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -42,7 +42,25 @@ export class ZendeskTrigger implements INodeType { { name: 'zendeskApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'apiToken', + ], + }, + }, + }, + { + name: 'zendeskOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], webhooks: [ { @@ -53,6 +71,23 @@ export class ZendeskTrigger implements INodeType { }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Token', + value: 'apiToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'apiToken', + description: 'The resource to operate on.', + }, { displayName: 'Service', name: 'service', diff --git a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts index 4deea5f6f58..13ebecd4c32 100644 --- a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts @@ -25,7 +25,7 @@ export async function zohoApiRequest(this: IExecuteFunctions | IExecuteSingleFun }; try { //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'zohoOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'zohoOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body.message) { // Try to return the error prettier diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e86d2041a96..cc7f61827b8 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,360 +1,381 @@ { - "name": "n8n-nodes-base", - "version": "0.62.1", - "description": "Base nodes 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": { - "dev": "npm run watch", - "build": "tsc && gulp", - "tslint": "tslint -p tsconfig.json -c tslint.json", - "watch": "tsc --watch", - "test": "jest" - }, - "files": [ - "dist" + "name": "n8n-nodes-base", + "version": "0.65.0", + "description": "Base nodes 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": { + "dev": "npm run watch", + "build": "tsc && gulp", + "tslint": "tslint -p tsconfig.json -c tslint.json", + "watch": "tsc --watch", + "test": "jest" + }, + "files": [ + "dist" + ], + "n8n": { + "credentials": [ + "dist/credentials/ActiveCampaignApi.credentials.js", + "dist/credentials/AgileCrmApi.credentials.js", + "dist/credentials/AcuitySchedulingApi.credentials.js", + "dist/credentials/AirtableApi.credentials.js", + "dist/credentials/Amqp.credentials.js", + "dist/credentials/AsanaApi.credentials.js", + "dist/credentials/Aws.credentials.js", + "dist/credentials/AffinityApi.credentials.js", + "dist/credentials/BannerbearApi.credentials.js", + "dist/credentials/BitbucketApi.credentials.js", + "dist/credentials/BitlyApi.credentials.js", + "dist/credentials/ChargebeeApi.credentials.js", + "dist/credentials/ClearbitApi.credentials.js", + "dist/credentials/ClickUpApi.credentials.js", + "dist/credentials/ClockifyApi.credentials.js", + "dist/credentials/CockpitApi.credentials.js", + "dist/credentials/CodaApi.credentials.js", + "dist/credentials/CopperApi.credentials.js", + "dist/credentials/CalendlyApi.credentials.js", + "dist/credentials/DisqusApi.credentials.js", + "dist/credentials/DriftApi.credentials.js", + "dist/credentials/DriftOAuth2Api.credentials.js", + "dist/credentials/DropboxApi.credentials.js", + "dist/credentials/EventbriteApi.credentials.js", + "dist/credentials/EventbriteOAuth2Api.credentials.js", + "dist/credentials/FacebookGraphApi.credentials.js", + "dist/credentials/FreshdeskApi.credentials.js", + "dist/credentials/FileMaker.credentials.js", + "dist/credentials/FlowApi.credentials.js", + "dist/credentials/GithubApi.credentials.js", + "dist/credentials/GithubOAuth2Api.credentials.js", + "dist/credentials/GitlabApi.credentials.js", + "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/GoogleCalendarOAuth2Api.credentials.js", + "dist/credentials/GoogleDriveOAuth2Api.credentials.js", + "dist/credentials/GoogleOAuth2Api.credentials.js", + "dist/credentials/GoogleSheetsOAuth2Api.credentials.js", + "dist/credentials/GoogleTasksOAuth2Api.credentials.js", + "dist/credentials/GumroadApi.credentials.js", + "dist/credentials/HarvestApi.credentials.js", + "dist/credentials/HelpScoutOAuth2Api.credentials.js", + "dist/credentials/HttpBasicAuth.credentials.js", + "dist/credentials/HttpDigestAuth.credentials.js", + "dist/credentials/HttpHeaderAuth.credentials.js", + "dist/credentials/HubspotApi.credentials.js", + "dist/credentials/HubspotDeveloperApi.credentials.js", + "dist/credentials/HubspotOAuth2Api.credentials.js", + "dist/credentials/HunterApi.credentials.js", + "dist/credentials/Imap.credentials.js", + "dist/credentials/IntercomApi.credentials.js", + "dist/credentials/InvoiceNinjaApi.credentials.js", + "dist/credentials/JiraSoftwareCloudApi.credentials.js", + "dist/credentials/JiraSoftwareServerApi.credentials.js", + "dist/credentials/JotFormApi.credentials.js", + "dist/credentials/KeapOAuth2Api.credentials.js", + "dist/credentials/LinkFishApi.credentials.js", + "dist/credentials/MailchimpApi.credentials.js", + "dist/credentials/MailchimpOAuth2Api.credentials.js", + "dist/credentials/MailgunApi.credentials.js", + "dist/credentials/MailjetEmailApi.credentials.js", + "dist/credentials/MailjetSmsApi.credentials.js", + "dist/credentials/MandrillApi.credentials.js", + "dist/credentials/MattermostApi.credentials.js", + "dist/credentials/MauticApi.credentials.js", + "dist/credentials/MauticOAuth2Api.credentials.js", + "dist/credentials/MessageBirdApi.credentials.js", + "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js", + "dist/credentials/MicrosoftOAuth2Api.credentials.js", + "dist/credentials/MicrosoftOneDriveOAuth2Api.credentials.js", + "dist/credentials/MoceanApi.credentials.js", + "dist/credentials/MondayComApi.credentials.js", + "dist/credentials/MongoDb.credentials.js", + "dist/credentials/Msg91Api.credentials.js", + "dist/credentials/MySql.credentials.js", + "dist/credentials/NextCloudApi.credentials.js", + "dist/credentials/OAuth1Api.credentials.js", + "dist/credentials/OAuth2Api.credentials.js", + "dist/credentials/OpenWeatherMapApi.credentials.js", + "dist/credentials/PagerDutyApi.credentials.js", + "dist/credentials/PagerDutyOAuth2Api.credentials.js", + "dist/credentials/PayPalApi.credentials.js", + "dist/credentials/PipedriveApi.credentials.js", + "dist/credentials/Postgres.credentials.js", + "dist/credentials/Redis.credentials.js", + "dist/credentials/RocketchatApi.credentials.js", + "dist/credentials/RundeckApi.credentials.js", + "dist/credentials/ShopifyApi.credentials.js", + "dist/credentials/SalesforceOAuth2Api.credentials.js", + "dist/credentials/SlackApi.credentials.js", + "dist/credentials/SlackOAuth2Api.credentials.js", + "dist/credentials/Sms77Api.credentials.js", + "dist/credentials/Smtp.credentials.js", + "dist/credentials/StripeApi.credentials.js", + "dist/credentials/SalesmateApi.credentials.js", + "dist/credentials/SegmentApi.credentials.js", + "dist/credentials/Signl4Api.credentials.js", + "dist/credentials/SpotifyOAuth2Api.credentials.js", + "dist/credentials/SurveyMonkeyApi.credentials.js", + "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js", + "dist/credentials/TelegramApi.credentials.js", + "dist/credentials/TodoistApi.credentials.js", + "dist/credentials/TrelloApi.credentials.js", + "dist/credentials/TwilioApi.credentials.js", + "dist/credentials/TwitterOAuth1Api.credentials.js", + "dist/credentials/TypeformApi.credentials.js", + "dist/credentials/TypeformOAuth2Api.credentials.js", + "dist/credentials/TogglApi.credentials.js", + "dist/credentials/UpleadApi.credentials.js", + "dist/credentials/VeroApi.credentials.js", + "dist/credentials/WebflowApi.credentials.js", + "dist/credentials/WebflowOAuth2Api.credentials.js", + "dist/credentials/WooCommerceApi.credentials.js", + "dist/credentials/WordpressApi.credentials.js", + "dist/credentials/ZendeskApi.credentials.js", + "dist/credentials/ZendeskOAuth2Api.credentials.js", + "dist/credentials/ZohoOAuth2Api.credentials.js", + "dist/credentials/ZulipApi.credentials.js" ], - "n8n": { - "credentials": [ - "dist/credentials/ActiveCampaignApi.credentials.js", - "dist/credentials/AgileCrmApi.credentials.js", - "dist/credentials/AcuitySchedulingApi.credentials.js", - "dist/credentials/AirtableApi.credentials.js", - "dist/credentials/Amqp.credentials.js", - "dist/credentials/AsanaApi.credentials.js", - "dist/credentials/Aws.credentials.js", - "dist/credentials/AffinityApi.credentials.js", - "dist/credentials/BannerbearApi.credentials.js", - "dist/credentials/BitbucketApi.credentials.js", - "dist/credentials/BitlyApi.credentials.js", - "dist/credentials/ChargebeeApi.credentials.js", - "dist/credentials/ClearbitApi.credentials.js", - "dist/credentials/ClickUpApi.credentials.js", - "dist/credentials/ClockifyApi.credentials.js", - "dist/credentials/CockpitApi.credentials.js", - "dist/credentials/CodaApi.credentials.js", - "dist/credentials/CopperApi.credentials.js", - "dist/credentials/CalendlyApi.credentials.js", - "dist/credentials/DisqusApi.credentials.js", - "dist/credentials/DriftApi.credentials.js", - "dist/credentials/DropboxApi.credentials.js", - "dist/credentials/DropboxOAuth2Api.credentials.js", - "dist/credentials/EventbriteApi.credentials.js", - "dist/credentials/FacebookGraphApi.credentials.js", - "dist/credentials/FreshdeskApi.credentials.js", - "dist/credentials/FileMaker.credentials.js", - "dist/credentials/FlowApi.credentials.js", - "dist/credentials/GithubApi.credentials.js", - "dist/credentials/GithubOAuth2Api.credentials.js", - "dist/credentials/GitlabApi.credentials.js", - "dist/credentials/GoogleApi.credentials.js", - "dist/credentials/GoogleCalendarOAuth2Api.credentials.js", - "dist/credentials/GoogleOAuth2Api.credentials.js", - "dist/credentials/GoogleSheetsOAuth2Api.credentials.js", - "dist/credentials/GumroadApi.credentials.js", - "dist/credentials/HarvestApi.credentials.js", - "dist/credentials/HelpScoutOAuth2Api.credentials.js", - "dist/credentials/HttpBasicAuth.credentials.js", - "dist/credentials/HttpDigestAuth.credentials.js", - "dist/credentials/HttpHeaderAuth.credentials.js", - "dist/credentials/HubspotApi.credentials.js", - "dist/credentials/HubspotDeveloperApi.credentials.js", - "dist/credentials/HunterApi.credentials.js", - "dist/credentials/Imap.credentials.js", - "dist/credentials/IntercomApi.credentials.js", - "dist/credentials/InvoiceNinjaApi.credentials.js", - "dist/credentials/JiraSoftwareCloudApi.credentials.js", - "dist/credentials/JiraSoftwareServerApi.credentials.js", - "dist/credentials/JotFormApi.credentials.js", - "dist/credentials/KeapOAuth2Api.credentials.js", - "dist/credentials/LinkFishApi.credentials.js", - "dist/credentials/MailchimpApi.credentials.js", - "dist/credentials/MailgunApi.credentials.js", - "dist/credentials/MailjetEmailApi.credentials.js", - "dist/credentials/MailjetSmsApi.credentials.js", - "dist/credentials/MandrillApi.credentials.js", - "dist/credentials/MattermostApi.credentials.js", - "dist/credentials/MauticApi.credentials.js", - "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js", - "dist/credentials/MicrosoftOAuth2Api.credentials.js", - "dist/credentials/MicrosoftOneDriveOAuth2Api.credentials.js", - "dist/credentials/MoceanApi.credentials.js", - "dist/credentials/MondayComApi.credentials.js", - "dist/credentials/MongoDb.credentials.js", - "dist/credentials/Msg91Api.credentials.js", - "dist/credentials/MySql.credentials.js", - "dist/credentials/NextCloudApi.credentials.js", - "dist/credentials/OAuth2Api.credentials.js", - "dist/credentials/OpenWeatherMapApi.credentials.js", - "dist/credentials/PagerDutyApi.credentials.js", - "dist/credentials/PayPalApi.credentials.js", - "dist/credentials/PipedriveApi.credentials.js", - "dist/credentials/Postgres.credentials.js", - "dist/credentials/Redis.credentials.js", - "dist/credentials/RocketchatApi.credentials.js", - "dist/credentials/RundeckApi.credentials.js", - "dist/credentials/ShopifyApi.credentials.js", - "dist/credentials/SalesforceOAuth2Api.credentials.js", - "dist/credentials/SlackApi.credentials.js", - "dist/credentials/SlackOAuth2Api.credentials.js", - "dist/credentials/Sms77Api.credentials.js", - "dist/credentials/Smtp.credentials.js", - "dist/credentials/StripeApi.credentials.js", - "dist/credentials/SalesmateApi.credentials.js", - "dist/credentials/SegmentApi.credentials.js", - "dist/credentials/SurveyMonkeyApi.credentials.js", - "dist/credentials/TelegramApi.credentials.js", - "dist/credentials/TodoistApi.credentials.js", - "dist/credentials/TrelloApi.credentials.js", - "dist/credentials/TwilioApi.credentials.js", - "dist/credentials/TypeformApi.credentials.js", - "dist/credentials/TogglApi.credentials.js", - "dist/credentials/UpleadApi.credentials.js", - "dist/credentials/VeroApi.credentials.js", - "dist/credentials/WebflowApi.credentials.js", - "dist/credentials/WooCommerceApi.credentials.js", - "dist/credentials/WordpressApi.credentials.js", - "dist/credentials/ZendeskApi.credentials.js", - "dist/credentials/ZohoOAuth2Api.credentials.js", - "dist/credentials/ZulipApi.credentials.js" - ], - "nodes": [ - "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", - "dist/nodes/ActiveCampaign/ActiveCampaignTrigger.node.js", - "dist/nodes/AgileCrm/AgileCrm.node.js", - "dist/nodes/Airtable/Airtable.node.js", - "dist/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.js", - "dist/nodes/Amqp/Amqp.node.js", - "dist/nodes/Amqp/AmqpTrigger.node.js", - "dist/nodes/Asana/Asana.node.js", - "dist/nodes/Asana/AsanaTrigger.node.js", - "dist/nodes/Affinity/Affinity.node.js", - "dist/nodes/Affinity/AffinityTrigger.node.js", - "dist/nodes/Aws/AwsLambda.node.js", - "dist/nodes/Aws/S3/AwsS3.node.js", - "dist/nodes/Aws/AwsSes.node.js", - "dist/nodes/Aws/AwsSns.node.js", - "dist/nodes/Aws/AwsSnsTrigger.node.js", - "dist/nodes/Bannerbear/Bannerbear.node.js", - "dist/nodes/Bitbucket/BitbucketTrigger.node.js", - "dist/nodes/Bitly/Bitly.node.js", - "dist/nodes/Calendly/CalendlyTrigger.node.js", - "dist/nodes/Chargebee/Chargebee.node.js", - "dist/nodes/Chargebee/ChargebeeTrigger.node.js", - "dist/nodes/Clearbit/Clearbit.node.js", - "dist/nodes/ClickUp/ClickUp.node.js", - "dist/nodes/ClickUp/ClickUpTrigger.node.js", - "dist/nodes/Clockify/ClockifyTrigger.node.js", - "dist/nodes/Cockpit/Cockpit.node.js", - "dist/nodes/Coda/Coda.node.js", - "dist/nodes/Copper/CopperTrigger.node.js", - "dist/nodes/Cron.node.js", - "dist/nodes/Crypto.node.js", - "dist/nodes/DateTime.node.js", - "dist/nodes/Discord/Discord.node.js", - "dist/nodes/Disqus/Disqus.node.js", - "dist/nodes/Drift/Drift.node.js", - "dist/nodes/Dropbox/Dropbox.node.js", - "dist/nodes/EditImage.node.js", - "dist/nodes/EmailReadImap.node.js", - "dist/nodes/EmailSend.node.js", - "dist/nodes/ErrorTrigger.node.js", - "dist/nodes/Eventbrite/EventbriteTrigger.node.js", - "dist/nodes/ExecuteCommand.node.js", - "dist/nodes/ExecuteWorkflow.node.js", - "dist/nodes/Facebook/FacebookGraphApi.node.js", - "dist/nodes/FileMaker/FileMaker.node.js", - "dist/nodes/Freshdesk/Freshdesk.node.js", - "dist/nodes/Flow/Flow.node.js", - "dist/nodes/Flow/FlowTrigger.node.js", - "dist/nodes/Function.node.js", - "dist/nodes/FunctionItem.node.js", - "dist/nodes/Github/Github.node.js", - "dist/nodes/Github/GithubTrigger.node.js", - "dist/nodes/Gitlab/Gitlab.node.js", - "dist/nodes/Gitlab/GitlabTrigger.node.js", - "dist/nodes/Google/Calendar/GoogleCalendar.node.js", - "dist/nodes/Google/Drive/GoogleDrive.node.js", - "dist/nodes/Google/Sheet/GoogleSheets.node.js", - "dist/nodes/GraphQL/GraphQL.node.js", - "dist/nodes/Gumroad/GumroadTrigger.node.js", - "dist/nodes/Harvest/Harvest.node.js", - "dist/nodes/HelpScout/HelpScout.node.js", - "dist/nodes/HelpScout/HelpScoutTrigger.node.js", - "dist/nodes/HtmlExtract/HtmlExtract.node.js", - "dist/nodes/HttpRequest.node.js", - "dist/nodes/Hubspot/Hubspot.node.js", - "dist/nodes/Hubspot/HubspotTrigger.node.js", - "dist/nodes/Hunter/Hunter.node.js", - "dist/nodes/If.node.js", - "dist/nodes/Intercom/Intercom.node.js", - "dist/nodes/Interval.node.js", - "dist/nodes/InvoiceNinja/InvoiceNinja.node.js", - "dist/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.js", - "dist/nodes/Jira/Jira.node.js", - "dist/nodes/JotForm/JotFormTrigger.node.js", - "dist/nodes/Keap/Keap.node.js", - "dist/nodes/Keap/KeapTrigger.node.js", - "dist/nodes/LinkFish/LinkFish.node.js", - "dist/nodes/Mailchimp/Mailchimp.node.js", - "dist/nodes/Mailchimp/MailchimpTrigger.node.js", - "dist/nodes/Mailgun/Mailgun.node.js", - "dist/nodes/Mailjet/Mailjet.node.js", - "dist/nodes/Mailjet/MailjetTrigger.node.js", - "dist/nodes/Mandrill/Mandrill.node.js", - "dist/nodes/Mattermost/Mattermost.node.js", - "dist/nodes/Mautic/Mautic.node.js", - "dist/nodes/Mautic/MauticTrigger.node.js", - "dist/nodes/Merge.node.js", - "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", - "dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js", - "dist/nodes/MoveBinaryData.node.js", - "dist/nodes/Mocean/Mocean.node.js", - "dist/nodes/MondayCom/MondayCom.node.js", - "dist/nodes/MongoDb/MongoDb.node.js", - "dist/nodes/MoveBinaryData.node.js", - "dist/nodes/Msg91/Msg91.node.js", - "dist/nodes/MySql/MySql.node.js", - "dist/nodes/NextCloud/NextCloud.node.js", - "dist/nodes/NoOp.node.js", - "dist/nodes/OpenWeatherMap.node.js", - "dist/nodes/PagerDuty/PagerDuty.node.js", - "dist/nodes/PayPal/PayPal.node.js", - "dist/nodes/PayPal/PayPalTrigger.node.js", - "dist/nodes/Pipedrive/Pipedrive.node.js", - "dist/nodes/Pipedrive/PipedriveTrigger.node.js", - "dist/nodes/Postgres/Postgres.node.js", - "dist/nodes/ReadBinaryFile.node.js", - "dist/nodes/ReadBinaryFiles.node.js", - "dist/nodes/ReadPdf.node.js", - "dist/nodes/Redis/Redis.node.js", - "dist/nodes/RenameKeys.node.js", - "dist/nodes/Rocketchat/Rocketchat.node.js", - "dist/nodes/RssFeedRead.node.js", - "dist/nodes/Rundeck/Rundeck.node.js", - "dist/nodes/Salesforce/Salesforce.node.js", - "dist/nodes/Set.node.js", - "dist/nodes/Shopify/Shopify.node.js", - "dist/nodes/Shopify/ShopifyTrigger.node.js", - "dist/nodes/Slack/Slack.node.js", - "dist/nodes/Sms77/Sms77.node.js", - "dist/nodes/SplitInBatches.node.js", - "dist/nodes/SpreadsheetFile.node.js", - "dist/nodes/SseTrigger.node.js", - "dist/nodes/Start.node.js", - "dist/nodes/Stripe/StripeTrigger.node.js", - "dist/nodes/Switch.node.js", - "dist/nodes/Salesmate/Salesmate.node.js", - "dist/nodes/Segment/Segment.node.js", - "dist/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.js", - "dist/nodes/Telegram/Telegram.node.js", - "dist/nodes/Telegram/TelegramTrigger.node.js", - "dist/nodes/Todoist/Todoist.node.js", - "dist/nodes/Toggl/TogglTrigger.node.js", - "dist/nodes/Trello/Trello.node.js", - "dist/nodes/Trello/TrelloTrigger.node.js", - "dist/nodes/Twilio/Twilio.node.js", - "dist/nodes/Typeform/TypeformTrigger.node.js", - "dist/nodes/Uplead/Uplead.node.js", - "dist/nodes/Vero/Vero.node.js", - "dist/nodes/Webflow/WebflowTrigger.node.js", - "dist/nodes/Webhook.node.js", - "dist/nodes/Wordpress/Wordpress.node.js", - "dist/nodes/WooCommerce/WooCommerce.node.js", - "dist/nodes/WooCommerce/WooCommerceTrigger.node.js", - "dist/nodes/WriteBinaryFile.node.js", - "dist/nodes/Xml.node.js", - "dist/nodes/Zendesk/Zendesk.node.js", - "dist/nodes/Zendesk/ZendeskTrigger.node.js", - "dist/nodes/Zoho/ZohoCrm.node.js", - "dist/nodes/Zulip/Zulip.node.js" - ] + "nodes": [ + "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", + "dist/nodes/ActiveCampaign/ActiveCampaignTrigger.node.js", + "dist/nodes/AgileCrm/AgileCrm.node.js", + "dist/nodes/Airtable/Airtable.node.js", + "dist/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.js", + "dist/nodes/Amqp/Amqp.node.js", + "dist/nodes/Amqp/AmqpTrigger.node.js", + "dist/nodes/Asana/Asana.node.js", + "dist/nodes/Asana/AsanaTrigger.node.js", + "dist/nodes/Affinity/Affinity.node.js", + "dist/nodes/Affinity/AffinityTrigger.node.js", + "dist/nodes/Aws/AwsLambda.node.js", + "dist/nodes/Aws/S3/AwsS3.node.js", + "dist/nodes/Aws/AwsSes.node.js", + "dist/nodes/Aws/AwsSns.node.js", + "dist/nodes/Aws/AwsSnsTrigger.node.js", + "dist/nodes/Bannerbear/Bannerbear.node.js", + "dist/nodes/Bitbucket/BitbucketTrigger.node.js", + "dist/nodes/Bitly/Bitly.node.js", + "dist/nodes/Calendly/CalendlyTrigger.node.js", + "dist/nodes/Chargebee/Chargebee.node.js", + "dist/nodes/Chargebee/ChargebeeTrigger.node.js", + "dist/nodes/Clearbit/Clearbit.node.js", + "dist/nodes/ClickUp/ClickUp.node.js", + "dist/nodes/ClickUp/ClickUpTrigger.node.js", + "dist/nodes/Clockify/ClockifyTrigger.node.js", + "dist/nodes/Cockpit/Cockpit.node.js", + "dist/nodes/Coda/Coda.node.js", + "dist/nodes/Copper/CopperTrigger.node.js", + "dist/nodes/Cron.node.js", + "dist/nodes/Crypto.node.js", + "dist/nodes/DateTime.node.js", + "dist/nodes/Discord/Discord.node.js", + "dist/nodes/Disqus/Disqus.node.js", + "dist/nodes/Drift/Drift.node.js", + "dist/nodes/Dropbox/Dropbox.node.js", + "dist/nodes/EditImage.node.js", + "dist/nodes/EmailReadImap.node.js", + "dist/nodes/EmailSend.node.js", + "dist/nodes/ErrorTrigger.node.js", + "dist/nodes/Eventbrite/EventbriteTrigger.node.js", + "dist/nodes/ExecuteCommand.node.js", + "dist/nodes/ExecuteWorkflow.node.js", + "dist/nodes/Facebook/FacebookGraphApi.node.js", + "dist/nodes/FileMaker/FileMaker.node.js", + "dist/nodes/Freshdesk/Freshdesk.node.js", + "dist/nodes/Flow/Flow.node.js", + "dist/nodes/Flow/FlowTrigger.node.js", + "dist/nodes/Function.node.js", + "dist/nodes/FunctionItem.node.js", + "dist/nodes/Github/Github.node.js", + "dist/nodes/Github/GithubTrigger.node.js", + "dist/nodes/Gitlab/Gitlab.node.js", + "dist/nodes/Gitlab/GitlabTrigger.node.js", + "dist/nodes/Google/Calendar/GoogleCalendar.node.js", + "dist/nodes/Google/Drive/GoogleDrive.node.js", + "dist/nodes/Google/Sheet/GoogleSheets.node.js", + "dist/nodes/Google/Task/GoogleTasks.node.js", + "dist/nodes/GraphQL/GraphQL.node.js", + "dist/nodes/Gumroad/GumroadTrigger.node.js", + "dist/nodes/Harvest/Harvest.node.js", + "dist/nodes/HelpScout/HelpScout.node.js", + "dist/nodes/HelpScout/HelpScoutTrigger.node.js", + "dist/nodes/HtmlExtract/HtmlExtract.node.js", + "dist/nodes/HttpRequest.node.js", + "dist/nodes/Hubspot/Hubspot.node.js", + "dist/nodes/Hubspot/HubspotTrigger.node.js", + "dist/nodes/Hunter/Hunter.node.js", + "dist/nodes/If.node.js", + "dist/nodes/Intercom/Intercom.node.js", + "dist/nodes/Interval.node.js", + "dist/nodes/InvoiceNinja/InvoiceNinja.node.js", + "dist/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.js", + "dist/nodes/Jira/Jira.node.js", + "dist/nodes/JotForm/JotFormTrigger.node.js", + "dist/nodes/Keap/Keap.node.js", + "dist/nodes/Keap/KeapTrigger.node.js", + "dist/nodes/LinkFish/LinkFish.node.js", + "dist/nodes/Mailchimp/Mailchimp.node.js", + "dist/nodes/Mailchimp/MailchimpTrigger.node.js", + "dist/nodes/Mailgun/Mailgun.node.js", + "dist/nodes/Mailjet/Mailjet.node.js", + "dist/nodes/Mailjet/MailjetTrigger.node.js", + "dist/nodes/Mandrill/Mandrill.node.js", + "dist/nodes/Mattermost/Mattermost.node.js", + "dist/nodes/Mautic/Mautic.node.js", + "dist/nodes/Mautic/MauticTrigger.node.js", + "dist/nodes/Merge.node.js", + "dist/nodes/MessageBird/MessageBird.node.js", + "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", + "dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js", + "dist/nodes/MoveBinaryData.node.js", + "dist/nodes/Mocean/Mocean.node.js", + "dist/nodes/MondayCom/MondayCom.node.js", + "dist/nodes/MongoDb/MongoDb.node.js", + "dist/nodes/MoveBinaryData.node.js", + "dist/nodes/Msg91/Msg91.node.js", + "dist/nodes/MySql/MySql.node.js", + "dist/nodes/NextCloud/NextCloud.node.js", + "dist/nodes/NoOp.node.js", + "dist/nodes/OpenWeatherMap.node.js", + "dist/nodes/PagerDuty/PagerDuty.node.js", + "dist/nodes/PayPal/PayPal.node.js", + "dist/nodes/PayPal/PayPalTrigger.node.js", + "dist/nodes/Pipedrive/Pipedrive.node.js", + "dist/nodes/Pipedrive/PipedriveTrigger.node.js", + "dist/nodes/Postgres/Postgres.node.js", + "dist/nodes/ReadBinaryFile.node.js", + "dist/nodes/ReadBinaryFiles.node.js", + "dist/nodes/ReadPdf.node.js", + "dist/nodes/Redis/Redis.node.js", + "dist/nodes/RenameKeys.node.js", + "dist/nodes/Rocketchat/Rocketchat.node.js", + "dist/nodes/RssFeedRead.node.js", + "dist/nodes/Rundeck/Rundeck.node.js", + "dist/nodes/Salesforce/Salesforce.node.js", + "dist/nodes/Set.node.js", + "dist/nodes/Shopify/Shopify.node.js", + "dist/nodes/Shopify/ShopifyTrigger.node.js", + "dist/nodes/Signl4/Signl4.node.js", + "dist/nodes/Slack/Slack.node.js", + "dist/nodes/Sms77/Sms77.node.js", + "dist/nodes/SplitInBatches.node.js", + "dist/nodes/Spotify/Spotify.node.js", + "dist/nodes/SpreadsheetFile.node.js", + "dist/nodes/SseTrigger.node.js", + "dist/nodes/Start.node.js", + "dist/nodes/Stripe/StripeTrigger.node.js", + "dist/nodes/Switch.node.js", + "dist/nodes/Salesmate/Salesmate.node.js", + "dist/nodes/Segment/Segment.node.js", + "dist/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.js", + "dist/nodes/Telegram/Telegram.node.js", + "dist/nodes/Telegram/TelegramTrigger.node.js", + "dist/nodes/Todoist/Todoist.node.js", + "dist/nodes/Toggl/TogglTrigger.node.js", + "dist/nodes/Trello/Trello.node.js", + "dist/nodes/Trello/TrelloTrigger.node.js", + "dist/nodes/Twilio/Twilio.node.js", + "dist/nodes/Twitter/Twitter.node.js", + "dist/nodes/Typeform/TypeformTrigger.node.js", + "dist/nodes/Uplead/Uplead.node.js", + "dist/nodes/Vero/Vero.node.js", + "dist/nodes/Webflow/WebflowTrigger.node.js", + "dist/nodes/Webhook.node.js", + "dist/nodes/Wordpress/Wordpress.node.js", + "dist/nodes/WooCommerce/WooCommerce.node.js", + "dist/nodes/WooCommerce/WooCommerceTrigger.node.js", + "dist/nodes/WriteBinaryFile.node.js", + "dist/nodes/Xml.node.js", + "dist/nodes/Zendesk/Zendesk.node.js", + "dist/nodes/Zendesk/ZendeskTrigger.node.js", + "dist/nodes/Zoho/ZohoCrm.node.js", + "dist/nodes/Zulip/Zulip.node.js" + ] + }, + "devDependencies": { + "@types/aws4": "^1.5.1", + "@types/basic-auth": "^1.1.2", + "@types/cheerio": "^0.22.15", + "@types/cron": "^1.6.1", + "@types/eventsource": "^1.1.2", + "@types/express": "^4.16.1", + "@types/formidable": "^1.0.31", + "@types/gm": "^1.18.2", + "@types/imap-simple": "^4.2.0", + "@types/jest": "^24.0.18", + "@types/lodash.set": "^4.3.6", + "@types/moment-timezone": "^0.5.12", + "@types/mongodb": "^3.5.4", + "@types/node": "^10.10.1", + "@types/nodemailer": "^6.4.0", + "@types/redis": "^2.8.11", + "@types/request-promise-native": "~1.0.15", + "@types/uuid": "^3.4.6", + "@types/xml2js": "^0.4.3", + "gulp": "^4.0.0", + "jest": "^24.9.0", + "n8n-workflow": "~0.32.0", + "ts-jest": "^24.0.2", + "tslint": "^5.17.0", + "typescript": "~3.7.4" + }, + "dependencies": { + "aws4": "^1.8.0", + "basic-auth": "^2.0.1", + "change-case": "^4.1.1", + "cheerio": "^1.0.0-rc.3", + "cron": "^1.7.2", + "eventsource": "^1.0.7", + "formidable": "^1.2.1", + "glob-promise": "^3.4.0", + "gm": "^1.23.1", + "imap-simple": "^4.3.0", + "iso-639-1": "^2.1.3", + "jsonwebtoken": "^8.5.1", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2", + "lodash.unset": "^4.5.2", + "moment": "2.24.0", + "moment-timezone": "^0.5.28", + "mongodb": "^3.5.5", + "mysql2": "^2.0.1", + "n8n-core": "~0.36.0", + "nodemailer": "^6.4.6", + "pdf-parse": "^1.1.1", + "pg-promise": "^9.0.3", + "redis": "^2.8.0", + "request": "^2.88.2", + "rhea": "^1.0.11", + "rss-parser": "^3.7.0", + "uuid": "^3.4.0", + "vm2": "^3.6.10", + "xlsx": "^0.14.3", + "xml2js": "^0.4.22" + }, + "jest": { + "transform": { + "^.+\\.tsx?$": "ts-jest" }, - "devDependencies": { - "@types/aws4": "^1.5.1", - "@types/basic-auth": "^1.1.2", - "@types/cheerio": "^0.22.15", - "@types/cron": "^1.6.1", - "@types/eventsource": "^1.1.2", - "@types/express": "^4.16.1", - "@types/formidable": "^1.0.31", - "@types/gm": "^1.18.2", - "@types/imap-simple": "^4.2.0", - "@types/jest": "^24.0.18", - "@types/lodash.set": "^4.3.6", - "@types/moment-timezone": "^0.5.12", - "@types/mongodb": "^3.5.4", - "@types/node": "^10.10.1", - "@types/nodemailer": "^6.4.0", - "@types/redis": "^2.8.11", - "@types/request-promise-native": "~1.0.15", - "@types/uuid": "^3.4.6", - "@types/xml2js": "^0.4.3", - "gulp": "^4.0.0", - "jest": "^24.9.0", - "n8n-workflow": "~0.31.0", - "ts-jest": "^24.0.2", - "tslint": "^5.17.0", - "typescript": "~3.7.4" - }, - "dependencies": { - "aws4": "^1.8.0", - "basic-auth": "^2.0.1", - "change-case": "^4.1.1", - "cheerio": "^1.0.0-rc.3", - "cron": "^1.7.2", - "eventsource": "^1.0.7", - "formidable": "^1.2.1", - "glob-promise": "^3.4.0", - "gm": "^1.23.1", - "googleapis": "~50.0.0", - "imap-simple": "^4.3.0", - "jsonwebtoken": "^8.5.1", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "lodash.unset": "^4.5.2", - "moment": "2.24.0", - "moment-timezone": "^0.5.28", - "mongodb": "^3.5.5", - "mysql2": "^2.0.1", - "n8n-core": "~0.34.0", - "nodemailer": "^6.4.6", - "pdf-parse": "^1.1.1", - "pg-promise": "^9.0.3", - "redis": "^2.8.0", - "request": "^2.88.2", - "rhea": "^1.0.11", - "rss-parser": "^3.7.0", - "uuid": "^3.4.0", - "vm2": "^3.6.10", - "xlsx": "^0.14.3", - "xml2js": "^0.4.22" - }, - "jest": { - "transform": { - "^.+\\.tsx?$": "ts-jest" - }, - "testURL": "http://localhost/", - "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", - "testPathIgnorePatterns": [ - "/dist/", - "/node_modules/" - ], - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "json" - ] - } + "testURL": "http://localhost/", + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + "testPathIgnorePatterns": [ + "/dist/", + "/node_modules/" + ], + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "json" + ] + } } diff --git a/packages/workflow/LICENSE.md b/packages/workflow/LICENSE.md index aac54547eb9..24a7d38fc94 100644 --- a/packages/workflow/LICENSE.md +++ b/packages/workflow/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/workflow/package.json b/packages/workflow/package.json index fb851ce40b4..068dc6d108b 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "0.31.0", + "version": "0.33.0", "description": "Workflow base code of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io",