Merge branch 'master' into static-stateless-webhooks

This commit is contained in:
ricardo 2020-06-22 16:16:50 -04:00
commit 494b1de93f
217 changed files with 32466 additions and 2487 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,50 @@
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?
To make it easier to use the data which the Slack-Node outputs we no longer return the whole
object the Slack-API returns if the only other property is `"ok": true`. In this case it returns
now directly the data under "channel".
### When is action necessary?
When you currently use the Slack-Node with Operations Channel -> Create and you use
any of the data the node outputs.
### How to upgrade:
All values that get referenced which were before under the property "channel" are now on the main level.
This means that these expressions have to get adjusted.
Meaning if the expression used before was:
```
{{ $node["Slack"].data["channel"]["id"] }}
```
it has to get changed to:
```
{{ $node["Slack"].data["id"] }}
```
## 0.67.0
### What changed?

View file

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

View file

@ -9,7 +9,9 @@ import {
import {
ActiveExecutions,
CredentialsOverwrites,
Db,
ExternalHooks,
GenericHelpers,
IWorkflowBase,
IWorkflowExecutionDataProcess,
@ -103,6 +105,14 @@ export class Execute extends Command {
// Wait till the n8n-packages have been read
await loadNodesAndCredentialsPromise;
// Load the credentials overwrites if any exist
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);

View file

@ -5,13 +5,14 @@ import {
} from 'n8n-core';
import { Command, flags } from '@oclif/command';
const open = require('open');
// import { dirname } from 'path';
import * as config from '../config';
import {
ActiveWorkflowRunner,
CredentialTypes,
CredentialsOverwrites,
Db,
ExternalHooks,
GenericHelpers,
LoadNodesAndCredentials,
NodeTypes,
@ -108,6 +109,14 @@ export class Start extends Command {
const loadNodesAndCredentials = LoadNodesAndCredentials();
await loadNodesAndCredentials.init();
// Load the credentials overwrites if any exist
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);

View file

@ -98,6 +98,19 @@ const config = convict({
},
},
credentials: {
overwrite: {
// Allows to set default values for credentials which
// get automatically prefilled and the user does not get
// displayed and can not change.
// Format: { CREDENTIAL_NAME: { PARAMTER: VALUE }}
doc: 'Overwrites for credentials',
format: '*',
default: '{}',
env: 'CREDENTIALS_OVERWRITE'
}
},
executions: {
// By default workflows get always executed in their own process.
@ -169,6 +182,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',
@ -252,6 +271,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',

View file

@ -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"
],
@ -82,9 +83,11 @@
"basic-auth": "^2.0.1",
"body-parser": "^1.18.3",
"body-parser-xml": "^1.1.0",
"client-oauth2": "^4.2.5",
"compression": "^1.7.4",
"connect-history-api-fallback": "^1.6.0",
"convict": "^5.0.0",
"csrf": "^3.1.0",
"dotenv": "^8.0.0",
"express": "^4.16.4",
"flatted": "^2.0.0",
@ -97,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",

View file

@ -3,16 +3,31 @@ import {
ICredentialTypes as ICredentialTypesInterface,
} from 'n8n-workflow';
import {
CredentialsOverwrites,
ICredentialsTypeData,
} from './';
class CredentialTypesClass implements ICredentialTypesInterface {
credentialTypes: {
[key: string]: ICredentialType
} = {};
credentialTypes: ICredentialsTypeData = {};
async init(credentialTypes: { [key: string]: ICredentialType }): Promise<void> {
async init(credentialTypes: ICredentialsTypeData): Promise<void> {
this.credentialTypes = credentialTypes;
// Load the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites().getAll();
for (const credentialType of Object.keys(credentialsOverwrites)) {
if (credentialTypes[credentialType] === undefined) {
continue;
}
// Add which properties got overwritten that the Editor-UI knows
// which properties it should hide
credentialTypes[credentialType].__overwrittenProperties = Object.keys(credentialsOverwrites[credentialType]);
}
}
getAll(): ICredentialType[] {

View file

@ -0,0 +1,159 @@
import {
Credentials,
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
ICredentialsHelper,
INodeParameters,
INodeProperties,
NodeHelpers,
} from 'n8n-workflow';
import {
CredentialsOverwrites,
CredentialTypes,
Db,
ICredentialsDb,
} from './';
export class CredentialsHelper extends ICredentialsHelper {
/**
* Returns the credentials instance
*
* @param {string} name Name of the credentials to return instance of
* @param {string} type Type of the credentials to return instance of
* @returns {Credentials}
* @memberof CredentialsHelper
*/
getCredentials(name: string, type: string): Credentials {
if (!this.workflowCredentials[type]) {
throw new Error(`No credentials of type "${type}" exist.`);
}
if (!this.workflowCredentials[type][name]) {
throw new Error(`No credentials with name "${name}" exist for type "${type}".`);
}
const credentialData = this.workflowCredentials[type][name];
return new Credentials(credentialData.name, credentialData.type, credentialData.nodesAccess, credentialData.data);
}
/**
* Returns all the properties of the credentials with the given name
*
* @param {string} type The name of the type to return credentials off
* @returns {INodeProperties[]}
* @memberof CredentialsHelper
*/
getCredentialsProperties(type: string): INodeProperties[] {
const credentialTypes = CredentialTypes();
const credentialTypeData = credentialTypes.getByName(type);
if (credentialTypeData === undefined) {
throw new Error(`The credentials of type "${type}" are not known.`);
}
if (credentialTypeData.extends === undefined) {
return credentialTypeData.properties;
}
const combineProperties = [] as INodeProperties[];
for (const credentialsTypeName of credentialTypeData.extends) {
const mergeCredentialProperties = this.getCredentialsProperties(credentialsTypeName);
NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties);
}
// The properties defined on the parent credentials take presidence
NodeHelpers.mergeNodeProperties(combineProperties, credentialTypeData.properties);
return combineProperties;
}
/**
* Returns the decrypted credential data with applied overwrites
*
* @param {string} name Name of the credentials to return data of
* @param {string} type Type of the credentials to return data of
* @param {boolean} [raw] Return the data as supplied without defaults or overwrites
* @returns {ICredentialDataDecryptedObject}
* @memberof CredentialsHelper
*/
getDecrypted(name: string, type: string, raw?: boolean): ICredentialDataDecryptedObject {
const credentials = this.getCredentials(name, type);
const decryptedDataOriginal = credentials.getData(this.encryptionKey);
if (raw === true) {
return decryptedDataOriginal;
}
return this.applyDefaultsAndOverwrites(decryptedDataOriginal, type);
}
/**
* Applies credential default data and overwrites
*
* @param {ICredentialDataDecryptedObject} decryptedDataOriginal The credential data to overwrite data on
* @param {string} type Type of the credentials to overwrite data of
* @returns {ICredentialDataDecryptedObject}
* @memberof CredentialsHelper
*/
applyDefaultsAndOverwrites(decryptedDataOriginal: ICredentialDataDecryptedObject, type: string): ICredentialDataDecryptedObject {
const credentialsProperties = this.getCredentialsProperties(type);
// Add the default credential values
const decryptedData = NodeHelpers.getNodeParameters(credentialsProperties, decryptedDataOriginal as INodeParameters, true, false) as ICredentialDataDecryptedObject;
if (decryptedDataOriginal.oauthTokenData !== undefined) {
// The OAuth data gets removed as it is not defined specifically as a parameter
// on the credentials so add it back in case it was set
decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData;
}
// Load and apply the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites();
return credentialsOverwrites.applyOverwrite(type, decryptedData);
}
/**
* Updates credentials in the database
*
* @param {string} name Name of the credentials to set data of
* @param {string} type Type of the credentials to set data of
* @param {ICredentialDataDecryptedObject} data The data to set
* @returns {Promise<void>}
* @memberof CredentialsHelper
*/
async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise<void> {
const credentials = await this.getCredentials(name, type);
if (Db.collections!.Credentials === null) {
// The first time executeWorkflow gets called the Database has
// to get initialized first
await Db.init();
}
credentials.setData(data, this.encryptionKey);
const newCredentialsData = credentials.getDataToSave() as ICredentialsDb;
// Add special database related data
newCredentialsData.updatedAt = new Date();
// TODO: also add user automatically depending on who is logged in, if anybody is logged in
// Save the credentials in DB
const findQuery = {
name,
type,
};
await Db.collections.Credentials!.update(findQuery, newCredentialsData);
}
}

View file

@ -0,0 +1,63 @@
import {
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
import {
ICredentialsOverwrite,
GenericHelpers,
} from './';
class CredentialsOverwritesClass {
private overwriteData: ICredentialsOverwrite = {};
async init(overwriteData?: ICredentialsOverwrite) {
if (overwriteData !== undefined) {
// If data is already given it can directly be set instead of
// loaded from environment
this.overwriteData = overwriteData;
return;
}
const data = await GenericHelpers.getConfigValue('credentials.overwrite') as string;
try {
this.overwriteData = JSON.parse(data);
} catch (error) {
throw new Error(`The credentials-overwrite is not valid JSON.`);
}
}
applyOverwrite(type: string, data: ICredentialDataDecryptedObject) {
const overwrites = this.get(type);
if (overwrites === undefined) {
return data;
}
const returnData = JSON.parse(JSON.stringify(data));
Object.assign(returnData, overwrites);
return returnData;
}
get(type: string): ICredentialDataDecryptedObject | undefined {
return this.overwriteData[type];
}
getAll(): ICredentialsOverwrite {
return this.overwriteData;
}
}
let credentialsOverwritesInstance: CredentialsOverwritesClass | undefined;
export function CredentialsOverwrites(): CredentialsOverwritesClass {
if (credentialsOverwritesInstance === undefined) {
credentialsOverwritesInstance = new CredentialsOverwritesClass();
}
return credentialsOverwritesInstance;
}

View file

@ -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<void> {
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<void> { // 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;
}

View file

@ -1,6 +1,8 @@
import {
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialsEncrypted,
ICredentialType,
IDataObject,
IExecutionError,
IRun,
@ -35,6 +37,13 @@ export interface ICustomRequest extends Request {
parsedUrl: Url | undefined;
}
export interface ICredentialsTypeData {
[key: string]: ICredentialType;
}
export interface ICredentialsOverwrite {
[key: string]: ICredentialDataDecryptedObject;
}
export interface IDatabaseCollections {
Credentials: Repository<ICredentialsDb> | null;
@ -78,7 +87,7 @@ export interface ICredentialsBase {
updatedAt: Date;
}
export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted{
export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted {
id: number | string | ObjectID;
}
@ -195,6 +204,30 @@ export interface IExecutingWorkflowData {
workflowExecution?: PCancelable<IRun>;
}
export interface IExternalHooks {
credentials?: {
create?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise<void>; }>
delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise<void>; }>
update?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise<void>; }>
};
workflow?: {
activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }>
create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise<void>; }>
delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise<void>; }>
execute?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb, mode: WorkflowExecuteMode): Promise<void>; }>
update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }>
};
}
export interface IExternalHooksFunctions {
dbCollections: IDatabaseCollections;
}
export interface IExternalHooksClass {
init(): Promise<void>;
run(hookName: string, hookParameters?: any[]): Promise<void>; // tslint:disable-line:no-any
}
export interface IN8nConfig {
database: IN8nConfigDatabase;
endpoints: IN8nConfigEndpoints;
@ -353,7 +386,10 @@ export interface IWorkflowExecutionDataProcess {
workflowData: IWorkflowBase;
}
export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExecutionDataProcess {
credentialsOverwrite: ICredentialsOverwrite;
credentialsTypeData: ICredentialsTypeData;
executionId: string;
nodeTypeData: ITransferNodeTypes;
}

View file

@ -97,24 +97,28 @@ class LoadNodesAndCredentialsClass {
* @memberof LoadNodesAndCredentialsClass
*/
async getN8nNodePackages(): Promise<string[]> {
const packages: string[] = [];
for (const file of await fsReaddirAsync(this.nodeModulesPath)) {
if (file.indexOf('n8n-nodes-') !== 0) {
continue;
const getN8nNodePackagesRecursive = async (relativePath: string): Promise<string[]> => {
const results: string[] = [];
const nodeModulesPath = `${this.nodeModulesPath}/${relativePath}`;
for (const file of await fsReaddirAsync(nodeModulesPath)) {
const isN8nNodesPackage = file.indexOf('n8n-nodes-') === 0;
const isNpmScopedPackage = file.indexOf('@') === 0;
if (!isN8nNodesPackage && !isNpmScopedPackage) {
continue;
}
if (!(await fsStatAsync(nodeModulesPath)).isDirectory()) {
continue;
}
if (isN8nNodesPackage) { results.push(`${relativePath}${file}`); }
if (isNpmScopedPackage) {
results.push(...await getN8nNodePackagesRecursive(`${relativePath}${file}/`));
}
}
// Check if it is really a folder
if (!(await fsStatAsync(path.join(this.nodeModulesPath, file))).isDirectory()) {
continue;
}
packages.push(file);
}
return packages;
return results;
};
return getN8nNodePackagesRecursive('');
}
/**
* Loads credentials from a file
*
@ -137,7 +141,7 @@ class LoadNodesAndCredentialsClass {
}
}
this.credentialTypes[credentialName] = tempCredential;
this.credentialTypes[tempCredential.name] = tempCredential;
}

View file

@ -5,6 +5,7 @@ import {
import {
dirname as pathDirname,
join as pathJoin,
resolve as pathResolve,
} from 'path';
import {
getConnectionManager,
@ -12,13 +13,21 @@ 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,
ActiveWorkflowRunner,
CredentialsHelper,
CredentialTypes,
Db,
ExternalHooks,
IActivationError,
ICustomRequest,
ICredentialsDb,
@ -33,6 +42,7 @@ import {
IExecutionsListResponse,
IExecutionsStopData,
IExecutionsSummary,
IExternalHooksClass,
IN8nUISettings,
IPackageVersions,
IWorkflowBase,
@ -57,6 +67,7 @@ import {
} from 'n8n-core';
import {
ICredentialsEncrypted,
ICredentialType,
IDataObject,
INodeCredentials,
@ -64,6 +75,7 @@ import {
INodeParameters,
INodePropertyOptions,
IRunData,
IWorkflowCredentials,
Workflow,
} from 'n8n-workflow';
@ -83,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 {
@ -92,6 +105,7 @@ class App {
testWebhooks: TestWebhooks.TestWebhooks;
endpointWebhook: string;
endpointWebhookTest: string;
externalHooks: IExternalHooksClass;
saveDataErrorExecution: string;
saveDataSuccessExecution: string;
saveManualExecutions: boolean;
@ -123,6 +137,8 @@ class App {
this.protocol = config.get('protocol');
this.sslKey = config.get('ssl_key');
this.sslCert = config.get('ssl_cert');
this.externalHooks = ExternalHooks();
}
@ -340,7 +356,7 @@ class App {
// Creates a new workflow
this.app.post('/rest/workflows', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowResponse> => {
const newWorkflowData = req.body;
const newWorkflowData = req.body as IWorkflowBase;
newWorkflowData.name = newWorkflowData.name.trim();
newWorkflowData.createdAt = this.getCurrentDate();
@ -348,6 +364,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);
@ -423,9 +441,11 @@ class App {
// Updates an existing workflow
this.app.patch('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowResponse> => {
const newWorkflowData = req.body;
const newWorkflowData = req.body as IWorkflowBase;
const id = req.params.id;
await this.externalHooks.run('workflow.update', [newWorkflowData]);
const isActive = await this.activeWorkflowRunner.isActive(id);
if (isActive) {
@ -469,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
@ -493,6 +515,8 @@ class App {
this.app.delete('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
const id = req.params.id;
await this.externalHooks.run('workflow.delete', [id]);
const isActive = await this.activeWorkflowRunner.isActive(id);
if (isActive) {
@ -567,7 +591,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);
@ -601,8 +625,8 @@ class App {
// Returns the node icon
this.app.get('/rest/node-icon/:nodeType', async (req: express.Request, res: express.Response): Promise<void> => {
const nodeTypeName = req.params.nodeType;
this.app.get(['/rest/node-icon/:nodeType', '/rest/node-icon/:scope/:nodeType'], async (req: express.Request, res: express.Response): Promise<void> => {
const nodeTypeName = `${req.params.scope ? `${req.params.scope}/` : ''}${req.params.nodeType}`;
const nodeTypes = NodeTypes();
const nodeType = nodeTypes.getByName(nodeTypeName);
@ -658,6 +682,8 @@ class App {
this.app.delete('/rest/credentials/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
const id = req.params.id;
await this.externalHooks.run('credentials.delete', [id]);
await Db.collections.Credentials!.delete({ id });
return true;
@ -667,6 +693,10 @@ class App {
this.app.post('/rest/credentials', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<ICredentialsResponse> => {
const incomingData = req.body;
if (!incomingData.name || incomingData.name.length < 3) {
throw new ResponseHelper.ResponseError(`Credentials name must be at least 3 characters long.`, undefined, 400);
}
// Add the added date for node access permissions
for (const nodeAccess of incomingData.nodesAccess) {
nodeAccess.date = this.getCurrentDate();
@ -699,6 +729,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();
@ -707,6 +739,7 @@ class App {
// Save the credentials in DB
const result = await Db.collections.Credentials!.save(newCredentialsData);
result.data = incomingData.data;
// Convert to response format in which the id is a string
(result as unknown as ICredentialsResponse).id = result.id.toString();
@ -750,6 +783,21 @@ class App {
throw new Error('No encryption key got found to encrypt the credentials!');
}
// Load the currently saved credentials to be able to persist some of the data if
const result = await Db.collections.Credentials!.findOne(id);
if (result === undefined) {
throw new ResponseHelper.ResponseError(`Credentials with the id "${id}" do not exist.`, undefined, 400);
}
const currentlySavedCredentials = new Credentials(result.name, result.type, result.nodesAccess, result.data);
const decryptedData = currentlySavedCredentials.getData(encryptionKey!);
// Do not overwrite the oauth data else data like the access or refresh token would get lost
// everytime anybody changes anything on the credentials even if it is just the name.
if (decryptedData.oauthTokenData) {
incomingData.data.oauthTokenData = decryptedData.oauthTokenData;
}
// Encrypt the data
const credentials = new Credentials(incomingData.name, incomingData.type, incomingData.nodesAccess);
credentials.setData(incomingData.data, encryptionKey);
@ -758,6 +806,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);
@ -869,6 +919,331 @@ class App {
return returnData;
}));
// ----------------------------------------
// OAuth1-Credential/Auth
// ----------------------------------------
// Authorize OAuth Data
this.app.get('/rest/oauth1-credential/auth', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
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()}rest/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('/rest/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
// ----------------------------------------
// Authorize OAuth Data
this.app.get('/rest/oauth2-credential/auth', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
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 token = new csrf();
// Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR
const csrfSecret = token.secretSync();
const state = {
token: token.create(csrfSecret),
cid: req.query.id
};
const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64') as string;
const oAuthObj = new clientOAuth2({
clientId: _.get(oauthCredentials, 'clientId') as string,
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`,
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','),
state: stateEncodedStr,
});
// Encrypt the data
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
decryptedDataOriginal.csrfSecret = csrfSecret;
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);
const authQueryParameters = _.get(oauthCredentials, 'authQueryParameters', '') as string;
let returnUri = oAuthObj.code.getUri();
if (authQueryParameters) {
returnUri += '&' + authQueryParameters;
}
return returnUri;
}));
// ----------------------------------------
// OAuth2-Credential/Callback
// ----------------------------------------
// 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) => {
const {code, state: stateEncoded } = req.query;
if (code === undefined || stateEncoded === undefined) {
throw new Error('Insufficient parameters for OAuth2 callback');
}
let state;
try {
state = JSON.parse(Buffer.from(stateEncoded as string, 'base64').toString());
} catch (error) {
const errorResponse = new ResponseHelper.ResponseError('Invalid state format returned', undefined, 503);
return ResponseHelper.sendErrorResponse(res, errorResponse);
}
const result = await Db.collections.Credentials!.findOne(state.cid);
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 token = new csrf();
if (decryptedDataOriginal.csrfSecret === undefined || !token.verify(decryptedDataOriginal.csrfSecret as string, state.token)) {
const errorResponse = new ResponseHelper.ResponseError('The OAuth2 callback state is invalid!', undefined, 404);
return ResponseHelper.sendErrorResponse(res, errorResponse);
}
let options = {};
if (_.get(oauthCredentials, 'authentication', 'header') as string === 'body') {
options = {
body: {
client_id: _.get(oauthCredentials, 'clientId') as string,
client_secret: _.get(oauthCredentials, 'clientSecret', '') as string,
},
};
}
const oAuthObj = new clientOAuth2({
clientId: _.get(oauthCredentials, 'clientId') as string,
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`,
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',')
});
const oauthToken = await oAuthObj.code.getToken(req.originalUrl, options);
if (oauthToken === undefined) {
const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404);
return ResponseHelper.sendErrorResponse(res, errorResponse);
}
if (decryptedDataOriginal.oauthTokenData) {
// Only overwrite supplied data as some providers do for example just return the
// refresh_token on the very first request and not on subsequent ones.
Object.assign(decryptedDataOriginal.oauthTokenData, oauthToken.data);
} else {
// No data exists so simply set
decryptedDataOriginal.oauthTokenData = oauthToken.data;
}
_.unset(decryptedDataOriginal, 'csrfSecret');
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(state.cid, newCredentialsData);
res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html'));
});
// ----------------------------------------
@ -1299,6 +1674,7 @@ class App {
export async function start(): Promise<void> {
const PORT = config.get('port');
const ADDRESS = config.get('listen_address');
const app = new App();
@ -1317,9 +1693,9 @@ export async function start(): Promise<void> {
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}`);
});
}

View file

@ -176,6 +176,9 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
};
}
// 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<{

View file

@ -1,5 +1,7 @@
import {
CredentialsHelper,
Db,
ExternalHooks,
IExecutionDb,
IExecutionFlattedDb,
IPushDataExecutionFinished,
@ -302,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;
@ -404,6 +410,7 @@ export async function getBase(credentials: IWorkflowCredentials, currentNodePara
return {
credentials,
credentialsHelper: new CredentialsHelper(credentials, encryptionKey),
encryptionKey,
executeWorkflow,
restApiUrl: urlBaseWebhook + config.get('endpoints.rest') as string,

View file

@ -1,5 +1,7 @@
import {
CredentialTypes,
Db,
ICredentialsTypeData,
ITransferNodeTypes,
IWorkflowExecutionDataProcess,
IWorkflowErrorData,
@ -15,6 +17,7 @@ import {
IRun,
IRunExecutionData,
ITaskData,
IWorkflowCredentials,
Workflow,
} from 'n8n-workflow';
@ -217,6 +220,63 @@ export function getNodeTypeData(nodes: INode[]): ITransferNodeTypes {
/**
* Returns the credentials data of the given type and its parent types
* it extends
*
* @export
* @param {string} type The credential type to return data off
* @returns {ICredentialsTypeData}
*/
export function getCredentialsDataWithParents(type: string): ICredentialsTypeData {
const credentialTypes = CredentialTypes();
const credentialType = credentialTypes.getByName(type);
const credentialTypeData: ICredentialsTypeData = {};
credentialTypeData[type] = credentialType;
if (credentialType === undefined || credentialType.extends === undefined) {
return credentialTypeData;
}
for (const typeName of credentialType.extends) {
if (credentialTypeData[typeName] !== undefined) {
continue;
}
credentialTypeData[typeName] = credentialTypes.getByName(typeName);
Object.assign(credentialTypeData, getCredentialsDataWithParents(typeName));
}
return credentialTypeData;
}
/**
* Returns all the credentialTypes which are needed to resolve
* the given workflow credentials
*
* @export
* @param {IWorkflowCredentials} credentials The credentials which have to be able to be resolved
* @returns {ICredentialsTypeData}
*/
export function getCredentialsData(credentials: IWorkflowCredentials): ICredentialsTypeData {
const credentialTypeData: ICredentialsTypeData = {};
for (const credentialType of Object.keys(credentials)) {
if (credentialTypeData[credentialType] !== undefined) {
continue;
}
Object.assign(credentialTypeData, getCredentialsDataWithParents(credentialType));
}
return credentialTypeData;
}
/**
* Returns the names of the NodeTypes which are are needed
* to execute the gives nodes

View file

@ -1,5 +1,10 @@
import {
ActiveExecutions,
CredentialsOverwrites,
CredentialTypes,
ExternalHooks,
ICredentialsOverwrite,
ICredentialsTypeData,
IProcessMessageDataHook,
ITransferNodeTypes,
IWorkflowExecutionDataProcess,
@ -31,12 +36,14 @@ import { fork } from 'child_process';
export class WorkflowRunner {
activeExecutions: ActiveExecutions.ActiveExecutions;
credentialsOverwrites: ICredentialsOverwrite;
push: Push.Push;
constructor() {
this.push = Push.getInstance();
this.activeExecutions = ActiveExecutions.getInstance();
this.credentialsOverwrites = CredentialsOverwrites().getAll();
}
@ -94,6 +101,9 @@ export class WorkflowRunner {
* @memberof WorkflowRunner
*/
async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
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);
@ -173,8 +183,8 @@ export class WorkflowRunner {
const executionId = this.activeExecutions.add(data, subprocess);
// Check if workflow contains a "executeWorkflow" Node as in this
// case we can not know which nodeTypes will be needed and so have
// to load all of them in the workflowRunnerProcess
// case we can not know which nodeTypes and credentialTypes will
// be needed and so have to load all of them in the workflowRunnerProcess
let loadAllNodeTypes = false;
for (const node of data.workflowData.nodes) {
if (node.type === 'n8n-nodes-base.executeWorkflow') {
@ -184,16 +194,24 @@ export class WorkflowRunner {
}
let nodeTypeData: ITransferNodeTypes;
let credentialTypeData: ICredentialsTypeData;
if (loadAllNodeTypes === true) {
// Supply all nodeTypes
// Supply all nodeTypes and credentialTypes
nodeTypeData = WorkflowHelpers.getAllNodeTypeData();
const credentialTypes = CredentialTypes();
credentialTypeData = credentialTypes.credentialTypes;
} else {
// Supply only nodeTypes which the workflow needs
// Supply only nodeTypes and credentialTypes which the workflow needs
nodeTypeData = WorkflowHelpers.getNodeTypeData(data.workflowData.nodes);
credentialTypeData = WorkflowHelpers.getCredentialsData(data.credentials);
}
(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = this.credentialsOverwrites;
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData = credentialTypeData; // TODO: Still needs correct value
const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);

View file

@ -1,5 +1,7 @@
import {
CredentialsOverwrites,
CredentialTypes,
IWorkflowExecutionDataProcessWithExecution,
NodeTypes,
WorkflowExecuteAdditionalData,
@ -58,6 +60,14 @@ export class WorkflowRunnerProcess {
const nodeTypes = NodeTypes();
await nodeTypes.init(nodeTypesData);
// Init credential types the workflow uses (is needed to apply default values to credentials)
const credentialTypes = CredentialTypes();
await credentialTypes.init(inputData.credentialsTypeData);
// Load the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites();
await credentialsOverwrites.init();
this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings});
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials);
additionalData.hooks = this.getProcessForwardHooks();

View file

@ -8,8 +8,8 @@ export class InitialMigration1588157391238 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
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);
}

View file

@ -8,29 +8,31 @@ export class InitialMigration1587669153312 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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);
}

View file

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

View file

@ -1,4 +1,7 @@
export * from './CredentialsHelper';
export * from './CredentialTypes';
export * from './CredentialsOverwrites';
export * from './ExternalHooks';
export * from './Interfaces';
export * from './LoadNodesAndCredentials';
export * from './NodeTypes';

View file

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

View file

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

View file

@ -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",
@ -40,11 +40,12 @@
"typescript": "~3.7.4"
},
"dependencies": {
"client-oauth2": "^4.2.5",
"cron": "^1.7.2",
"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"

View file

@ -1,25 +1,14 @@
import {
ICredentialDataDecryptedObject,
CredentialInformation,
ICredentialDataDecryptedObject,
ICredentials,
ICredentialsEncrypted,
ICredentialNodeAccess,
} from 'n8n-workflow';
import { enc, AES } from 'crypto-js';
export class Credentials implements ICredentialsEncrypted {
name: string;
type: string;
data: string | undefined;
nodesAccess: ICredentialNodeAccess[];
constructor(name: string, type: string, nodesAccess: ICredentialNodeAccess[], data?: string) {
this.name = name;
this.type = type;
this.nodesAccess = nodesAccess;
this.data = data;
}
export class Credentials extends ICredentials {
/**

View file

@ -1,4 +1,5 @@
import {
IAllExecuteFunctions,
IBinaryData,
ICredentialType,
IDataObject,
@ -17,6 +18,7 @@ import {
} from 'n8n-workflow';
import { OptionsWithUri, OptionsWithUrl } from 'request';
import * as requestPromise from 'request-promise-native';
interface Constructable<T> {
@ -34,6 +36,8 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase {
helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}
@ -43,6 +47,8 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
};
}
@ -51,15 +57,24 @@ export interface IPollFunctions extends IPollFunctionsBase {
helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}
export interface IResponseError extends Error {
statusCode?: number;
}
export interface ITriggerFunctions extends ITriggerFunctionsBase {
helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}
@ -83,6 +98,8 @@ export interface IUserSettings {
export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
helpers: {
request?: requestPromise.RequestPromiseAPI,
requestOAuth2?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions) => Promise<any>, // tslint:disable-line:no-any
requestOAuth1?(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
};
}
@ -90,6 +107,8 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
export interface IHookFunctions extends IHookFunctionsBase {
helpers: {
request: requestPromise.RequestPromiseAPI,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
};
}
@ -98,6 +117,8 @@ export interface IWebhookFunctions extends IWebhookFunctionsBase {
helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}

View file

@ -1,6 +1,7 @@
import {
INode,
INodeCredentials,
INodeParameters,
INodePropertyOptions,
INodeTypes,
IWorkflowExecuteAdditionalData,
@ -20,7 +21,7 @@ export class LoadNodeParameterOptions {
workflow: Workflow;
constructor(nodeTypeName: string, nodeTypes: INodeTypes, credentials?: INodeCredentials) {
constructor(nodeTypeName: string, nodeTypes: INodeTypes, currentNodeParameters: INodeParameters, credentials?: INodeCredentials) {
const nodeType = nodeTypes.getByName(nodeTypeName);
if (nodeType === undefined) {
@ -28,8 +29,7 @@ export class LoadNodeParameterOptions {
}
const nodeData: INode = {
parameters: {
},
parameters: currentNodeParameters,
name: TEMP_NODE_NAME,
type: nodeTypeName,
typeVersion: 1,

View file

@ -1,12 +1,13 @@
import {
Credentials,
IHookFunctions,
ILoadOptionsFunctions,
IResponseError,
IWorkflowSettings,
BINARY_ENCODING,
} from './';
import {
IAllExecuteFunctions,
IBinaryData,
IContextObject,
ICredentialDataDecryptedObject,
@ -35,13 +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 express from 'express';
import * as path from 'path';
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);
@ -102,6 +110,135 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m
/**
* Makes a request using OAuth data for authentication
*
* @export
* @param {IAllExecuteFunctions} this
* @param {string} credentialsType
* @param {(OptionsWithUri | requestPromise.RequestPromiseOptions)} requestOptions
* @param {INode} node
* @param {IWorkflowExecuteAdditionalData} additionalData
* @returns
*/
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) {
throw new Error('No credentials got returned!');
}
if (credentials.oauthTokenData === undefined) {
throw new Error('OAuth credentials not connected!');
}
const oAuthClient = new clientOAuth2({
clientId: credentials.clientId as string,
clientSecret: credentials.clientSecret as string,
accessTokenUri: credentials.accessTokenUrl as string,
});
const oauthTokenData = credentials.oauthTokenData as clientOAuth2.Data;
const token = oAuthClient.createToken(get(oauthTokenData, property as string) || oauthTokenData.accessToken, oauthTokenData.refreshToken, tokenType || oauthTokenData.tokenType, oauthTokenData);
// Signs the request by adding authorization headers or query parameters depending
// on the token-type used.
const newRequestOptions = token.sign(requestOptions as clientOAuth2.RequestObject);
return this.helpers.request!(newRequestOptions)
.catch(async (error: IResponseError) => {
// TODO: Check if also other codes are possible
if (error.statusCode === 401) {
// TODO: Whole refresh process is not tested yet
// Token is probably not valid anymore. So try refresh it.
const newToken = await token.refresh();
credentials.oauthTokenData = newToken.data;
// Find the name of the credentials
if (!node.credentials || !node.credentials[credentialsType]) {
throw new Error(`The node "${node.name}" does not have credentials of type "${credentialsType}"!`);
}
const name = node.credentials[credentialsType];
// Save the refreshed token
await additionalData.credentialsHelper.updateCredentials(name, credentialsType, credentials);
// Make the request again with the new token
const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject);
return this.helpers.request!(newRequestOptions);
}
// Unknown error so simply throw it
throw error;
});
}
/* 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;
});
}
/**
* Takes generic input data and brings it into the json format n8n uses.
*
@ -177,20 +314,7 @@ export function getCredentials(workflow: Workflow, node: INode, type: string, ad
const name = node.credentials[type];
if (!additionalData.credentials[type]) {
throw new Error(`No credentials of type "${type}" exist.`);
}
if (!additionalData.credentials[type][name]) {
throw new Error(`No credentials with name "${name}" exist for type "${type}".`);
}
const credentialData = additionalData.credentials[type][name];
const credentials = new Credentials(name, type, credentialData.nodesAccess, credentialData.data);
const decryptedDataObject = credentials.getData(additionalData.encryptionKey, node.type);
if (decryptedDataObject === null) {
throw new Error('Could not get the credentials');
}
const decryptedDataObject = additionalData.credentialsHelper.getDecrypted(name, type);
return decryptedDataObject;
}
@ -406,6 +530,12 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // 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<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
returnJsonArray,
},
};
@ -463,6 +593,12 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // 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<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
returnJsonArray,
},
};
@ -553,6 +689,12 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // 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<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
returnJsonArray,
},
};
@ -645,6 +787,12 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // 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<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
},
};
})(workflow, runExecutionData, connectionInputData, inputData, node, itemIndex);
@ -695,6 +843,12 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio
},
helpers: {
request: requestPromise,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // 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<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
},
};
return that;
@ -756,6 +910,12 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio
},
helpers: {
request: requestPromise,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // 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<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
},
};
return that;
@ -844,6 +1004,12 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // 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<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
returnJsonArray,
},
};

View file

@ -143,7 +143,6 @@ export async function writeUserSettings(userSettings: IUserSettings, settingsPat
*/
export async function getUserSettings(settingsPath?: string, ignoreCache?: boolean): Promise<IUserSettings | undefined> {
if (settingsCache !== undefined && ignoreCache !== true) {
return settingsCache;
}

View file

@ -1,6 +1,8 @@
import { set } from 'lodash';
import {
ICredentialDataDecryptedObject,
ICredentialsHelper,
IExecuteWorkflowInfo,
INodeExecutionData,
INodeParameters,
@ -15,11 +17,25 @@ import {
} from 'n8n-workflow';
import {
Credentials,
IDeferredPromise,
IExecuteFunctions,
} from '../src';
export class CredentialsHelper extends ICredentialsHelper {
getDecrypted(name: string, type: string): ICredentialDataDecryptedObject {
return {};
}
getCredentials(name: string, type: string): Credentials {
return new Credentials('', '', [], '');
}
async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise<void> {}
}
class NodeTypesClass implements INodeTypes {
nodeTypes: INodeTypeData = {
@ -275,6 +291,7 @@ export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise<IRun
return {
credentials: {},
credentialsHelper: new CredentialsHelper({}, ''),
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {}, // tslint:disable-line:no-any
restApiUrl: '',

View file

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

View file

@ -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",
@ -66,7 +66,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",

View file

@ -145,6 +145,9 @@ export interface IRestApi {
deleteExecutions(sendData: IExecutionDeleteFilter): Promise<void>;
retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean>;
getTimezones(): Promise<IDataObject>;
oAuth1CredentialAuthorize(sendData: ICredentialsResponse): Promise<string>;
oAuth2CredentialAuthorize(sendData: ICredentialsResponse): Promise<string>;
oAuth2Callback(code: string, state: string): Promise<string>;
}
export interface IBinaryDisplayData {
@ -155,6 +158,13 @@ export interface IBinaryDisplayData {
runIndex: number;
}
export interface ICredentialsCreatedEvent {
data: ICredentialsDecryptedResponse;
options: {
closeDialog: boolean,
};
}
export interface IStartRunData {
workflowData: IWorkflowData;
startNodes?: string[];

View file

@ -8,7 +8,7 @@
Credential type:
</el-col>
<el-col :span="18">
<el-select v-model="credentialType" placeholder="Select Type" size="small">
<el-select v-model="credentialType" filterable placeholder="Select Type" size="small">
<el-option
v-for="item in credentialTypes"
:key="item.name"
@ -31,10 +31,15 @@ import Vue from 'vue';
import { restApi } from '@/components/mixins/restApi';
import { showMessage } from '@/components/mixins/showMessage';
import CredentialsInput from '@/components/CredentialsInput.vue';
import { ICredentialsDecryptedResponse } from '@/Interface';
import {
ICredentialsCreatedEvent,
ICredentialsDecryptedResponse,
} from '@/Interface';
import {
NodeHelpers,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
import mixins from 'vue-typed-mixins';
@ -168,36 +173,67 @@ export default mixins(
},
},
methods: {
getCredentialTypeData (name: string): ICredentialType | null {
for (const credentialData of this.credentialTypes) {
if (credentialData.name === name) {
return credentialData;
}
getCredentialProperties (name: string): INodeProperties[] {
const credentialsData = this.$store.getters.credentialType(name);
if (credentialsData === null) {
throw new Error(`Could not find credentials of type: ${name}`);
}
return null;
if (credentialsData.extends === undefined) {
return credentialsData.properties;
}
const combineProperties = [] as INodeProperties[];
for (const credentialsTypeName of credentialsData.extends) {
const mergeCredentialProperties = this.getCredentialProperties(credentialsTypeName);
NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties);
}
// The properties defined on the parent credentials take presidence
NodeHelpers.mergeNodeProperties(combineProperties, credentialsData.properties);
return combineProperties;
},
credentialsCreated (data: ICredentialsDecryptedResponse): void {
this.$emit('credentialsCreated', data);
getCredentialTypeData (name: string): ICredentialType | null {
let credentialData = this.$store.getters.credentialType(name);
if (credentialData === null || credentialData.extends === undefined) {
return credentialData;
}
// Credentials extends another one. So get the properties of the one it
// extends and add them.
credentialData = JSON.parse(JSON.stringify(credentialData));
credentialData.properties = this.getCredentialProperties(credentialData.name);
return credentialData;
},
credentialsCreated (eventData: ICredentialsCreatedEvent): void {
this.$emit('credentialsCreated', eventData);
this.$showMessage({
title: 'Credentials created',
message: `The credential "${data.name}" got created!`,
message: `The credential "${eventData.data.name}" got created!`,
type: 'success',
});
this.closeDialog();
if (eventData.options.closeDialog === true) {
this.closeDialog();
}
},
credentialsUpdated (data: ICredentialsDecryptedResponse): void {
this.$emit('credentialsUpdated', data);
credentialsUpdated (eventData: ICredentialsCreatedEvent): void {
this.$emit('credentialsUpdated', eventData);
this.$showMessage({
title: 'Credentials updated',
message: `The credential "${data.name}" got updated!`,
message: `The credential "${eventData.data.name}" got updated!`,
type: 'success',
});
this.closeDialog();
if (eventData.options.closeDialog === true) {
this.closeDialog();
}
},
closeDialog (): void {
// Handle the close externally as the visible parameter is an external prop

View file

@ -12,17 +12,16 @@
<el-input size="small" type="text" v-model="name"></el-input>
</el-col>
</el-row>
<br />
<div class="headline">
<div class="headline" v-if="credentialProperties.length">
Credential Data:
<el-tooltip class="credentials-info" placement="top" effect="light">
<div slot="content" v-html="helpTexts.credentialsData"></div>
<font-awesome-icon icon="question-circle" />
</el-tooltip>
</div>
<span v-for="parameter in credentialTypeData.properties" :key="parameter.name">
<el-row v-if="displayCredentialParameter(parameter)" class="parameter-wrapper">
<div v-for="parameter in credentialProperties" :key="parameter.name">
<el-row class="parameter-wrapper">
<el-col :span="6" class="parameter-name">
{{parameter.displayName}}:
<el-tooltip placement="top" class="parameter-info" v-if="parameter.description" effect="light">
@ -34,7 +33,46 @@
<parameter-input :parameter="parameter" :value="propertyValue[parameter.name]" :path="parameter.name" :isCredential="true" @valueChanged="valueChanged" />
</el-col>
</el-row>
</span>
</div>
<el-row v-if="isOAuthType" class="oauth-information">
<el-col :span="6" class="headline">
OAuth
</el-col>
<el-col :span="18">
<span v-if="requiredPropertiesFilled === false">
<el-button title="Connect OAuth Credentials" circle :disabled="true">
<font-awesome-icon icon="redo" />
</el-button>
Not all required credential properties are filled
</span>
<span v-else-if="isOAuthConnected === true">
<el-button title="Reconnect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" circle>
<font-awesome-icon icon="redo" />
</el-button>
Is connected
</span>
<span v-else>
<el-button title="Connect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" circle>
<font-awesome-icon icon="sign-in-alt" />
</el-button>
Is NOT connected
</span>
<div v-if="credentialProperties.length">
<div class="clickable oauth-callback-headline" :class="{expanded: !isMinimized}" @click="isMinimized=!isMinimized" :title="isMinimized ? 'Click to display Webhook URLs' : 'Click to hide Webhook URLs'">
<font-awesome-icon icon="angle-up" class="minimize-button minimize-icon" />
OAuth Callback URL
</div>
<el-tooltip v-if="!isMinimized" class="item" effect="light" content="Click to copy Callback URL" placement="right">
<div class="callback-url left-ellipsis clickable" @click="copyCallbackUrl">
{{oAuthCallbackUrl}}
</div>
</el-tooltip>
</div>
</el-col>
</el-row>
<el-row class="nodes-access-wrapper">
<el-col :span="6" class="headline">
@ -61,10 +99,10 @@
</el-row>
<div class="action-buttons">
<el-button type="success" @click="updateCredentials" v-if="credentialData">
<el-button type="success" @click="updateCredentials(true)" v-if="credentialDataDynamic">
Save
</el-button>
<el-button type="success" @click="createCredentials" v-else>
<el-button type="success" @click="createCredentials(true)" v-else>
Create
</el-button>
</div>
@ -75,11 +113,16 @@
<script lang="ts">
import Vue from 'vue';
import { copyPaste } from '@/components/mixins/copyPaste';
import { restApi } from '@/components/mixins/restApi';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { showMessage } from '@/components/mixins/showMessage';
import { ICredentialsDecryptedResponse, IUpdateInformation } from '@/Interface';
import {
ICredentialsDecryptedResponse,
ICredentialsResponse,
IUpdateInformation,
} from '@/Interface';
import {
CredentialInformation,
ICredentialDataDecryptedObject,
@ -87,8 +130,10 @@ import {
ICredentialType,
ICredentialNodeAccess,
INodeCredentialDescription,
INodeParameters,
INodeProperties,
INodeTypeDescription,
NodeHelpers,
} from 'n8n-workflow';
import ParameterInput from '@/components/ParameterInput.vue';
@ -96,6 +141,7 @@ import ParameterInput from '@/components/ParameterInput.vue';
import mixins from 'vue-typed-mixins';
export default mixins(
copyPaste,
nodeHelpers,
restApi,
showMessage,
@ -114,11 +160,13 @@ export default mixins(
},
data () {
return {
isMinimized: true,
helpTexts: {
credentialsData: 'The credentials to set.',
credentialsName: 'The name the credentials should be saved as. Use a name<br />which makes it clear to what exactly they give access to.<br />For credentials of an Email account that could be the Email address itself.',
nodesWithAccess: 'The nodes which allowed to use this credentials.',
},
credentialDataTemp: null as ICredentialsDecryptedResponse | null,
nodesAccess: [] as string[],
name: '',
propertyValue: {} as ICredentialDataDecryptedObject,
@ -155,8 +203,78 @@ export default mixins(
};
});
},
credentialProperties (): INodeProperties[] {
return this.credentialTypeData.properties.filter((propertyData: INodeProperties) => {
if (!this.displayCredentialParameter(propertyData)) {
return false;
}
return !this.credentialTypeData.__overwrittenProperties || !this.credentialTypeData.__overwrittenProperties.includes(propertyData.name);
});
},
credentialDataDynamic (): ICredentialsDecryptedResponse | null {
if (this.credentialData) {
return this.credentialData;
}
return this.credentialDataTemp;
},
isOAuthType (): boolean {
if (['oAuth1Api', 'oAuth2Api'].includes(this.credentialTypeData.name)) {
return true;
}
const types = this.parentTypes(this.credentialTypeData.name);
return types.includes('oAuth1Api') || types.includes('oAuth2Api');
},
isOAuthConnected (): boolean {
if (this.isOAuthType === false) {
return false;
}
return this.credentialDataDynamic !== null && !!this.credentialDataDynamic.data!.oauthTokenData;
},
oAuthCallbackUrl (): string {
const types = this.parentTypes(this.credentialTypeData.name);
const oauthType = (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) ? 'oauth2' : 'oauth1';
return this.$store.getters.getWebhookBaseUrl + `rest/${oauthType}-credential/callback`;
},
requiredPropertiesFilled (): boolean {
for (const property of this.credentialProperties) {
if (property.required !== true) {
continue;
}
if (!this.propertyValue[property.name]) {
return false;
}
}
return true;
},
},
methods: {
copyCallbackUrl (): void {
this.copyToClipboard(this.oAuthCallbackUrl);
this.$showMessage({
title: 'Copied',
message: `The callback URL got copied!`,
type: 'success',
});
},
parentTypes (name: string): string[] {
const credentialType = this.$store.getters.credentialType(name);
if (credentialType === undefined || credentialType.extends === undefined) {
return [];
}
const types: string[] = [];
for (const typeName of credentialType.extends) {
types.push(typeName);
types.push.apply(types, this.parentTypes(typeName));
}
return types;
},
valueChanged (parameterData: IUpdateInformation) {
const name = parameterData.name.split('.').pop() as string;
// For a currently for me unknown reason can In not simply just
@ -166,14 +284,18 @@ export default mixins(
Vue.set(this, 'propertyValue', tempValue);
},
displayCredentialParameter (parameter: INodeProperties): boolean {
if (parameter.type === 'hidden') {
return false;
}
if (parameter.displayOptions === undefined) {
// If it is not defined no need to do a proper check
return true;
}
return this.displayParameter(this.propertyValue, parameter, '');
return this.displayParameter(this.propertyValue as INodeParameters, parameter, '');
},
async createCredentials (): Promise<void> {
async createCredentials (closeDialog: boolean): Promise<ICredentialsResponse | null> {
const nodesAccess = this.nodesAccess.map((nodeType) => {
return {
nodeType,
@ -184,7 +306,8 @@ export default mixins(
name: this.name,
type: (this.credentialTypeData as ICredentialType).name,
nodesAccess,
data: this.propertyValue,
// Save only the none default data
data: NodeHelpers.getNodeParameters(this.credentialTypeData.properties as INodeProperties[], this.propertyValue as INodeParameters, false, false),
} as ICredentialsDecrypted;
let result;
@ -192,21 +315,110 @@ export default mixins(
result = await this.restApi().createNewCredentials(newCredentials);
} catch (error) {
this.$showError(error, 'Problem Creating Credentials', 'There was a problem creating the credentials:');
return;
return null;
}
// Add also to local store
this.$store.commit('addCredentials', result);
this.$emit('credentialsCreated', result);
this.$emit('credentialsCreated', {data: result, options: { closeDialog }});
return result;
},
async updateCredentials () {
async oAuthCredentialAuthorize () {
let url;
let credentialData = this.credentialDataDynamic;
let newCredentials = false;
if (!credentialData) {
// Credentials did not get created yet. So create first before
// doing oauth authorize
credentialData = await this.createCredentials(false) as ICredentialsDecryptedResponse;
newCredentials = true;
if (credentialData === null) {
return;
}
// Set the internal data directly so that even if it fails it displays a "Save" instead
// of the "Create" button. If that would not be done, people could not retry after a
// connect issue as it woult try to create credentials again which would fail as they
// exist already.
Vue.set(this, 'credentialDataTemp', credentialData);
} else {
// Exists already but got maybe changed. So save first
credentialData = await this.updateCredentials(false) as ICredentialsDecryptedResponse;
if (credentialData === null) {
return;
}
}
const types = this.parentTypes(this.credentialTypeData.name);
try {
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;
}
const params = `scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700`;
const oauthPopup = window.open(url, 'OAuth2 Authorization', params);
const receiveMessage = (event: MessageEvent) => {
// // TODO: Add check that it came from n8n
// if (event.origin !== 'http://example.org:8080') {
// return;
// }
if (event.data === 'success') {
// Set some kind of data that status changes.
// As data does not get displayed directly it does not matter what data.
if (this.credentialData === null) {
// Are new credentials so did not get send via "credentialData"
Vue.set(this, 'credentialDataTemp', credentialData);
Vue.set(this.credentialDataTemp!.data!, 'oauthTokenData', {});
} else {
// Credentials did already exist so can be set directly
Vue.set(this.credentialData.data, 'oauthTokenData', {});
}
// Save that OAuth got authorized locally
this.$store.commit('updateCredentials', this.credentialDataDynamic);
// Close the window
if (oauthPopup) {
oauthPopup.close();
}
if (newCredentials === true) {
this.$emit('credentialsCreated', {data: credentialData, options: { closeDialog: false }});
}
this.$showMessage({
title: 'Connected',
message: 'Got connected!',
type: 'success',
});
}
// Make sure that the event gets removed again
window.removeEventListener('message', receiveMessage, false);
};
window.addEventListener('message', receiveMessage, false);
},
async updateCredentials (closeDialog: boolean): Promise<ICredentialsResponse | null> {
const nodesAccess: ICredentialNodeAccess[] = [];
const addedNodeTypes: string[] = [];
// Add Node-type which already had access to keep the original added date
let nodeAccessData: ICredentialNodeAccess;
for (nodeAccessData of (this.credentialData as ICredentialsDecryptedResponse).nodesAccess) {
for (nodeAccessData of (this.credentialDataDynamic as ICredentialsDecryptedResponse).nodesAccess) {
if (this.nodesAccess.includes((nodeAccessData.nodeType))) {
nodesAccess.push(nodeAccessData);
addedNodeTypes.push(nodeAccessData.nodeType);
@ -226,15 +438,16 @@ export default mixins(
name: this.name,
type: (this.credentialTypeData as ICredentialType).name,
nodesAccess,
data: this.propertyValue,
// Save only the none default data
data: NodeHelpers.getNodeParameters(this.credentialTypeData.properties as INodeProperties[], this.propertyValue as INodeParameters, false, false),
} as ICredentialsDecrypted;
let result;
try {
result = await this.restApi().updateCredentials((this.credentialData as ICredentialsDecryptedResponse).id as string, newCredentials);
result = await this.restApi().updateCredentials((this.credentialDataDynamic as ICredentialsDecryptedResponse).id as string, newCredentials);
} catch (error) {
this.$showError(error, 'Problem Updating Credentials', 'There was a problem updating the credentials:');
return;
return null;
}
// Update also in local store
@ -244,7 +457,9 @@ export default mixins(
// which have now a different name
this.updateNodesCredentialsIssues();
this.$emit('credentialsUpdated', result);
this.$emit('credentialsUpdated', {data: result, options: { closeDialog }});
return result;
},
init () {
if (this.credentialData) {
@ -312,6 +527,11 @@ export default mixins(
line-height: 1.75em;
}
.oauth-information {
line-height: 2.5em;
margin: 2em 0;
}
.parameter-wrapper {
line-height: 3em;
@ -334,6 +554,20 @@ export default mixins(
display: none;
}
.callback-url {
position: relative;
top: 0;
width: 100%;
font-size: 0.9em;
white-space: normal;
overflow: visible;
text-overflow: initial;
color: #404040;
text-align: left;
direction: ltr;
word-break: break-all;
}
.headline:hover,
.headline-regular:hover {
.credentials-info {
@ -341,6 +575,17 @@ export default mixins(
}
}
.expanded .minimize-button {
-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);
}
.oauth-callback-headline {
padding-top: 1em;
font-weight: 500;
}
}
</style>

View file

@ -45,6 +45,7 @@ import Vue from 'vue';
import { restApi } from '@/components/mixins/restApi';
import {
ICredentialsCreatedEvent,
ICredentialsResponse,
INodeUi,
INodeUpdatePropertiesInformation,
@ -134,21 +135,23 @@ export default mixins(
closeCredentialNewDialog () {
this.credentialNewDialogVisible = false;
},
async credentialsCreated (data: ICredentialsResponse) {
await this.credentialsUpdated(data);
async credentialsCreated (eventData: ICredentialsCreatedEvent) {
await this.credentialsUpdated(eventData);
},
credentialsUpdated (data: ICredentialsResponse) {
if (!this.credentialTypesNode.includes(data.type)) {
credentialsUpdated (eventData: ICredentialsCreatedEvent) {
if (!this.credentialTypesNode.includes(eventData.data.type)) {
return;
}
this.init();
Vue.set(this.credentials, data.type, data.name);
Vue.set(this.credentials, eventData.data.type, eventData.data.name);
// Makes sure that it does also get set correctly on the node not just the UI
this.credentialSelected(data.type);
this.credentialSelected(eventData.data.type);
this.closeCredentialNewDialog();
if (eventData.options.closeDialog === true) {
this.closeCredentialNewDialog();
}
},
credentialInputWrapperStyle (credentialType: string) {
let deductWidth = 0;

View file

@ -149,6 +149,10 @@ export default mixins(
this.$emit('valueChanged', parameterData);
},
displayNodeParameter (parameter: INodeProperties): boolean {
if (parameter.type === 'hidden') {
return false;
}
if (parameter.displayOptions === undefined) {
// If it is not defined no need to do a proper check
return true;

View file

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

View file

@ -252,6 +252,26 @@ export const restApi = Vue.extend({
return self.restApi().makeRestApiRequest('GET', `/credential-types`);
},
// Get OAuth1 Authorization URL using the stored credentials
oAuth1CredentialAuthorize: (sendData: ICredentialsResponse): Promise<string> => {
return self.restApi().makeRestApiRequest('GET', `/oauth1-credential/auth`, sendData);
},
// Get OAuth2 Authorization URL using the stored credentials
oAuth2CredentialAuthorize: (sendData: ICredentialsResponse): Promise<string> => {
return self.restApi().makeRestApiRequest('GET', `/oauth2-credential/auth`, sendData);
},
// Verify OAuth2 provider callback and kick off token generation
oAuth2Callback: (code: string, state: string): Promise<string> => {
const sendData = {
'code': code,
'state': state,
};
return self.restApi().makeRestApiRequest('POST', `/oauth2-credential/callback`, sendData);
},
// Returns the execution with the given name
getExecution: async (id: string): Promise<IExecutionResponse> => {
const response = await self.restApi().makeRestApiRequest('GET', `/executions/${id}`);

View file

@ -71,6 +71,7 @@ import {
faSave,
faSearchMinus,
faSearchPlus,
faSignInAlt,
faSlidersH,
faSpinner,
faStop,
@ -145,6 +146,7 @@ library.add(faRss);
library.add(faSave);
library.add(faSearchMinus);
library.add(faSearchPlus);
library.add(faSignInAlt);
library.add(faSlidersH);
library.add(faSpinner);
library.add(faStop);

View file

@ -19,6 +19,12 @@ export default new Router({
sidebar: MainSidebar,
},
},
{
path: '/oauth2/callback',
name: 'oAuth2Callback',
components: {
},
},
{
path: '/workflow',
name: 'NodeViewNew',

View file

@ -571,6 +571,9 @@ export const store = new Vuex.Store({
}
return `${state.baseUrl}${endpoint}`;
},
getWebhookBaseUrl: (state): string => {
return state.urlBaseWebhook;
},
getWebhookUrl: (state): string => {
return `${state.urlBaseWebhook}${state.endpointWebhook}`;
},

View file

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

View file

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "0.6.0",
"version": "0.9.0",
"description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -58,8 +58,8 @@
"change-case": "^4.1.1",
"copyfiles": "^2.1.1",
"inquirer": "^7.0.0",
"n8n-core": "^0.21.0",
"n8n-workflow": "^0.20.0",
"n8n-core": "^0.31.0",
"n8n-workflow": "^0.28.0",
"replace-in-file": "^6.0.0",
"request": "^2.88.2",
"tmp-promise": "^2.0.2",

View file

@ -105,10 +105,10 @@ export async function buildFiles (options?: IBuildOptions): Promise<string> {
}
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));
});
});
}

View file

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

View file

@ -0,0 +1,54 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class GithubOAuth2Api implements ICredentialType {
name = 'githubOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Github OAuth2 API';
properties = [
{
displayName: 'Github Server',
name: 'server',
type: 'string' as NodePropertyTypes,
default: 'https://api.github.com',
description: 'The server to connect to. Does only have to get changed if Github Enterprise gets used.',
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://github.com/login/oauth/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://github.com/login/oauth/access_token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: 'repo,admin:repo_hook,admin:org,admin:org_hook,gist,notifications,user,write:packages,read:packages,delete:packages,worfklow',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'header',
},
];
}

View file

@ -0,0 +1,25 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events',
];
export class GoogleCalendarOAuth2Api implements ICredentialType {
name = 'googleCalendarOAuth2Api';
extends = [
'googleOAuth2Api',
];
displayName = 'Google Calendar OAuth2 API';
properties = [
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(' '),
},
];
}

View file

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

View file

@ -0,0 +1,38 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class GoogleOAuth2Api implements ICredentialType {
name = 'googleOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Google OAuth2 API';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://accounts.google.com/o/oauth2/v2/auth',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://oauth2.googleapis.com/token',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: 'access_type=offline',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'body',
},
];
}

View file

@ -0,0 +1,26 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/spreadsheets',
];
export class GoogleSheetsOAuth2Api implements ICredentialType {
name = 'googleSheetsOAuth2Api';
extends = [
'googleOAuth2Api',
];
displayName = 'Google Sheets OAuth2 API';
properties = [
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(' '),
},
];
}

View file

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

View file

@ -0,0 +1,46 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class HelpScoutOAuth2Api implements ICredentialType {
name = 'helpScoutOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'HelpScout OAuth2 API';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://secure.helpscout.net/authentication/authorizeClientApplication',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.helpscout.net/v2/oauth2/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',
},
];
}

View file

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

View file

@ -0,0 +1,48 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'full',
];
export class KeapOAuth2Api implements ICredentialType {
name = 'keapOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Keap OAuth2 API';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://signin.infusionsoft.com/app/oauth/authorize',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.infusionsoft.com/token',
},
{
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',
},
];
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,21 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MicrosoftExcelOAuth2Api implements ICredentialType {
name = 'microsoftExcelOAuth2Api';
extends = [
'microsoftOAuth2Api',
];
displayName = 'Microsoft OAuth2 API';
properties = [
//https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: 'openid offline_access Files.ReadWrite',
},
];
}

View file

@ -0,0 +1,38 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MicrosoftOAuth2Api implements ICredentialType {
name = 'microsoftOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Microsoft OAuth2 API';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'string' as NodePropertyTypes,
default: 'https://login.microsoftonline.com/{yourtenantid}/oauth2/v2.0/authorize',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'string' as NodePropertyTypes,
default: 'https://login.microsoftonline.com/{yourtenantid}/oauth2/v2.0/token',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: 'response_mode=query',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'body',
},
];
}

View file

@ -0,0 +1,21 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MicrosoftOneDriveOAuth2Api implements ICredentialType {
name = 'microsoftOneDriveOAuth2Api';
extends = [
'microsoftOAuth2Api',
];
displayName = 'Microsoft OAuth2 API';
properties = [
//https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: 'openid offline_access Files.ReadWrite.All',
},
];
}

View file

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

View file

@ -0,0 +1,76 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class OAuth2Api implements ICredentialType {
name = 'oAuth2Api';
displayName = 'OAuth2 API';
properties = [
{
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: 'Client ID',
name: 'clientId',
type: 'string' as NodePropertyTypes,
default: '',
required: true,
},
{
displayName: 'Client Secret',
name: 'clientSecret',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
default: '',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'string' as NodePropertyTypes,
default: '',
description: 'For some services additional query parameters have to be set which can be defined here.',
placeholder: 'access_type=offline',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'options' as NodePropertyTypes,
options: [
{
name: 'Body',
value: 'body',
description: 'Send credentials in body',
},
{
name: 'Header',
value: 'header',
description: 'Send credentials as Basic Auth header',
},
],
default: 'header',
description: 'Resource to consume.',
},
];
}

View file

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

View file

@ -0,0 +1,40 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class SalesforceOAuth2Api implements ICredentialType {
name = 'salesforceOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Salesforce OAuth2 API';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://login.salesforce.com/services/oauth2/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'string' as NodePropertyTypes,
default: 'https://yourcompany.salesforce.com/services/oauth2/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: 'full refresh_token',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -0,0 +1,54 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
//https://api.slack.com/authentication/oauth-v2
const userScopes = [
'chat:write',
'files:read',
'files:write',
'stars:read',
'stars:write',
];
export class SlackOAuth2Api implements ICredentialType {
name = 'slackOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Slack OAuth2 API';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://slack.com/oauth/v2/authorize',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://slack.com/api/oauth.v2.access',
},
//https://api.slack.com/scopes
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: 'chat:write',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: `user_scope=${userScopes.join(' ')}`,
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'body',
},
];
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,80 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class ZohoOAuth2Api implements ICredentialType {
name = 'zohoOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Zoho OAuth2 API';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'options' as NodePropertyTypes,
options: [
{
name: 'https://accounts.zoho.com/oauth/v2/auth',
value: 'https://accounts.zoho.com/oauth/v2/auth',
description: 'For the EU, AU, and IN domains',
},
{
name: 'https://accounts.zoho.com.cn/oauth/v2/auth',
value: 'https://accounts.zoho.com.cn/oauth/v2/auth',
description: 'For the CN domain',
},
],
default: 'https://accounts.zoho.com/oauth/v2/auth',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'options' as NodePropertyTypes,
options: [
{
name: 'US - https://accounts.zoho.com/oauth/v2/token',
value: 'https://accounts.zoho.com/oauth/v2/token',
},
{
name: 'AU - https://accounts.zoho.com.au/oauth/v2/token',
value: 'https://accounts.zoho.com.au/oauth/v2/token',
},
{
name: 'EU - https://accounts.zoho.eu/oauth/v2/token',
value: 'https://accounts.zoho.eu/oauth/v2/token',
},
{
name: 'IN - https://accounts.zoho.in/oauth/v2/token',
value: 'https://accounts.zoho.in/oauth/v2/token',
},
{
name: 'CN - https://accounts.zoho.com.cn/oauth/v2/token',
value: ' https://accounts.zoho.com.cn/oauth/v2/token',
},
],
default: 'https://accounts.zoho.com/oauth/v2/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: 'ZohoCRM.modules.ALL,ZohoCRM.settings.all,ZohoCRM.users.all',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: 'access_type=offline',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'body',
},
];
}

View file

@ -1,3 +1,5 @@
import { OptionsWithUri } from 'request';
import {
IExecuteFunctions,
IHookFunctions,
@ -17,26 +19,40 @@ import {
* @returns {Promise<any>}
*/
export async function githubApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query?: object): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('githubApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const baseUrl = credentials!.server || 'https://api.github.com';
const options = {
const options: OptionsWithUri = {
method,
headers: {
'Authorization': `token ${credentials.accessToken}`,
'User-Agent': credentials.user,
'User-Agent': 'n8n',
},
body,
qs: query,
uri: `${baseUrl}${endpoint}`,
uri: '',
json: true
};
try {
return await this.helpers.request(options);
const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken') as string;
if (authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('githubApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const baseUrl = credentials!.server || 'https://api.github.com';
options.uri = `${baseUrl}${endpoint}`;
options.headers!.Authorization = `token ${credentials.accessToken}`;
return await this.helpers.request(options);
} else {
const credentials = this.getCredentials('githubOAuth2Api');
const baseUrl = credentials!.server || 'https://api.github.com';
options.uri = `${baseUrl}${endpoint}`;
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'githubOAuth2Api', options);
}
} catch (error) {
if (error.statusCode === 401) {
// Return a clear error

View file

@ -33,9 +33,44 @@ export class Github implements INodeType {
{
name: 'githubApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'githubOAuth2Api',
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',
@ -209,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',
@ -1093,12 +1123,6 @@ export class Github implements INodeType {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const credentials = this.getCredentials('githubApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
// Operations which overwrite the returned data
const overwriteDataOperations = [
'file:create',

View file

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

View file

@ -1,6 +1,7 @@
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
@ -16,7 +17,7 @@ import {
* @param {object} body
* @returns {Promise<any>}
*/
export async function gitlabApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query?: object): Promise<any> { // tslint:disable-line:no-any
export async function gitlabApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: object): Promise<any> { // 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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
import {
IDataObject,
} from 'n8n-workflow';
export interface IReminder {
useDefault?: boolean;
overrides?: IDataObject[];
}
export interface IEvent {
attendees?: IDataObject[];
colorId?: string;
description?: string;
end?: IDataObject;
guestsCanInviteOthers?: boolean;
guestsCanModify?: boolean;
guestsCanSeeOtherGuests?: boolean;
id?: string;
location?: string;
maxAttendees?: number;
recurrence?: string[];
reminders?: IReminder;
sendUpdates?: string;
start?: IDataObject;
summary?: string;
transparency?: string;
visibility?: string;
}

View file

@ -0,0 +1,67 @@
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<any> { // tslint:disable-line:no-any
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, 'googleCalendarOAuth2Api', 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 Calendar 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<any> { // 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;
}

View file

@ -0,0 +1,510 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeTypeDescription,
INodeType,
ILoadOptionsFunctions,
INodePropertyOptions,
} from 'n8n-workflow';
import {
googleApiRequest,
googleApiRequestAllItems,
} from './GenericFunctions';
import {
eventOperations,
eventFields,
} from './EventDescription';
import {
IEvent,
} from './EventInterface';
import * as moment from 'moment-timezone';
export class GoogleCalendar implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Calendar',
name: 'googleCalendar',
icon: 'file:googleCalendar.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Google Calendar API.',
defaults: {
name: 'Google Calendar',
color: '#3E87E4',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'googleCalendarOAuth2Api',
required: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Event',
value: 'event',
},
],
default: 'event',
description: 'The resource to operate on.'
},
...eventOperations,
...eventFields,
],
};
methods = {
loadOptions: {
// Get all the calendars to display them to user so that he can
// select them easily
async getCalendars(
this: ILoadOptionsFunctions
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
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
});
}
return returnData;
},
// Get all the colors to display them to user so that he can
// select them easily
async getColors(
this: ILoadOptionsFunctions
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
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}`,
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<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
for (const timezone of moment.tz.names()) {
const timezoneName = timezone;
const timezoneId = timezone;
returnData.push({
name: timezoneName,
value: timezoneId
});
}
return returnData;
}
}
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = (items.length as unknown) as number;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'event') {
//https://developers.google.com/calendar/v3/reference/events/insert
if (operation === 'create') {
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;
if (additionalFields.maxAttendees) {
qs.maxAttendees = additionalFields.maxAttendees as number;
}
if (additionalFields.sendNotifications) {
qs.sendNotifications = additionalFields.sendNotifications as boolean;
}
if (additionalFields.sendUpdates) {
qs.sendUpdates = additionalFields.sendUpdates as string;
}
const body: IEvent = {
start: {
dateTime: start,
timeZone: additionalFields.timeZone || this.getTimezone()
},
end: {
dateTime: end,
timeZone: additionalFields.timeZone || this.getTimezone()
}
};
if (additionalFields.attendees) {
body.attendees = (additionalFields.attendees as string[]).map(
attendee => {
return { email: attendee };
}
);
}
if (additionalFields.color) {
body.colorId = additionalFields.color as string;
}
if (additionalFields.description) {
body.description = additionalFields.description as string;
}
if (additionalFields.guestsCanInviteOthers) {
body.guestsCanInviteOthers = additionalFields.guestsCanInviteOthers as boolean;
}
if (additionalFields.guestsCanModify) {
body.guestsCanModify = additionalFields.guestsCanModify as boolean;
}
if (additionalFields.guestsCanSeeOtherGuests) {
body.guestsCanSeeOtherGuests = additionalFields.guestsCanSeeOtherGuests as boolean;
}
if (additionalFields.id) {
body.id = additionalFields.id as string;
}
if (additionalFields.location) {
body.location = additionalFields.location as string;
}
if (additionalFields.summary) {
body.summary = additionalFields.summary as string;
}
if (additionalFields.showMeAs) {
body.transparency = additionalFields.showMeAs as string;
}
if (additionalFields.visibility) {
body.visibility = additionalFields.visibility as string;
}
if (!useDefaultReminders) {
const reminders = (this.getNodeParameter(
'remindersUi',
i
) as IDataObject).remindersValues as IDataObject[];
body.reminders = {
useDefault: false
};
if (reminders) {
body.reminders.overrides = reminders;
}
}
if (additionalFields.allday) {
body.start = {
date: moment(start)
.utc()
.format('YYYY-MM-DD')
};
body.end = {
date: moment(end)
.utc()
.format('YYYY-MM-DD')
};
}
//exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z
//https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
body.recurrence = [];
if (
additionalFields.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()};`
);
}
if (additionalFields.repeatHowManyTimes) {
body.recurrence?.push(
`COUNT=${additionalFields.repeatHowManyTimes};`
);
}
if (additionalFields.repeatUntil) {
body.recurrence?.push(
`UNTIL=${moment(additionalFields.repeatUntil as string)
.utc()
.format('YYYYMMDDTHHmmss')}Z`
);
}
if (body.recurrence.length !== 0) {
body.recurrence = [`RRULE:${body.recurrence.join('')}`];
}
responseData = await googleApiRequest.call(
this,
'POST',
`/calendar/v3/calendars/${calendarId}/events`,
body,
qs
);
}
//https://developers.google.com/calendar/v3/reference/events/delete
if (operation === 'delete') {
const calendarId = this.getNodeParameter('calendar', i) as string;
const eventId = this.getNodeParameter('eventId', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
if (options.sendUpdates) {
qs.sendUpdates = options.sendUpdates as number;
}
responseData = await googleApiRequest.call(
this,
'DELETE',
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
{}
);
responseData = { success: true };
}
//https://developers.google.com/calendar/v3/reference/events/get
if (operation === 'get') {
const calendarId = this.getNodeParameter('calendar', i) as string;
const eventId = this.getNodeParameter('eventId', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
if (options.maxAttendees) {
qs.maxAttendees = options.maxAttendees as number;
}
if (options.timeZone) {
qs.timeZone = options.timeZone as string;
}
responseData = await googleApiRequest.call(
this,
'GET',
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
{},
qs
);
}
//https://developers.google.com/calendar/v3/reference/events/list
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const calendarId = this.getNodeParameter('calendar', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
if (options.iCalUID) {
qs.iCalUID = options.iCalUID as string;
}
if (options.maxAttendees) {
qs.maxAttendees = options.maxAttendees as number;
}
if (options.orderBy) {
qs.orderBy = options.orderBy as number;
}
if (options.query) {
qs.q = options.query as number;
}
if (options.showDeleted) {
qs.showDeleted = options.showDeleted as boolean;
}
if (options.showHiddenInvitations) {
qs.showHiddenInvitations = options.showHiddenInvitations as boolean;
}
if (options.singleEvents) {
qs.singleEvents = options.singleEvents as boolean;
}
if (options.timeMax) {
qs.timeMax = options.timeMax as string;
}
if (options.timeMin) {
qs.timeMin = options.timeMin as string;
}
if (options.timeZone) {
qs.timeZone = options.timeZone as string;
}
if (options.updatedMin) {
qs.updatedMin = options.updatedMin as string;
}
if (returnAll) {
responseData = await googleApiRequestAllItems.call(
this,
'items',
'GET',
`/calendar/v3/calendars/${calendarId}/events`,
{},
qs
);
} else {
qs.maxResults = this.getNodeParameter('limit', i) as number;
responseData = await googleApiRequest.call(
this,
'GET',
`/calendar/v3/calendars/${calendarId}/events`,
{},
qs
);
responseData = responseData.items;
}
}
//https://developers.google.com/calendar/v3/reference/events/patch
if (operation === 'update') {
const calendarId = this.getNodeParameter('calendar', i) as string;
const eventId = this.getNodeParameter('eventId', i) as string;
const useDefaultReminders = this.getNodeParameter(
'useDefaultReminders',
i
) as boolean;
const updateFields = this.getNodeParameter(
'updateFields',
i
) as IDataObject;
if (updateFields.maxAttendees) {
qs.maxAttendees = updateFields.maxAttendees as number;
}
if (updateFields.sendNotifications) {
qs.sendNotifications = updateFields.sendNotifications as boolean;
}
if (updateFields.sendUpdates) {
qs.sendUpdates = updateFields.sendUpdates as string;
}
const body: IEvent = {};
if (updateFields.start) {
body.start = {
dateTime: updateFields.start,
timeZone: updateFields.timeZone || this.getTimezone()
};
}
if (updateFields.end) {
body.end = {
dateTime: updateFields.end,
timeZone: updateFields.timeZone || this.getTimezone()
};
}
if (updateFields.attendees) {
body.attendees = (updateFields.attendees as string[]).map(
attendee => {
return { email: attendee };
}
);
}
if (updateFields.color) {
body.colorId = updateFields.color as string;
}
if (updateFields.description) {
body.description = updateFields.description as string;
}
if (updateFields.guestsCanInviteOthers) {
body.guestsCanInviteOthers = updateFields.guestsCanInviteOthers as boolean;
}
if (updateFields.guestsCanModify) {
body.guestsCanModify = updateFields.guestsCanModify as boolean;
}
if (updateFields.guestsCanSeeOtherGuests) {
body.guestsCanSeeOtherGuests = updateFields.guestsCanSeeOtherGuests as boolean;
}
if (updateFields.id) {
body.id = updateFields.id as string;
}
if (updateFields.location) {
body.location = updateFields.location as string;
}
if (updateFields.summary) {
body.summary = updateFields.summary as string;
}
if (updateFields.showMeAs) {
body.transparency = updateFields.showMeAs as string;
}
if (updateFields.visibility) {
body.visibility = updateFields.visibility as string;
}
if (!useDefaultReminders) {
const reminders = (this.getNodeParameter(
'remindersUi',
i
) as IDataObject).remindersValues as IDataObject[];
body.reminders = {
useDefault: false
};
if (reminders) {
body.reminders.overrides = reminders;
}
}
if (updateFields.allday && updateFields.start && updateFields.end) {
body.start = {
date: moment(updateFields.start as string)
.utc()
.format('YYYY-MM-DD')
};
body.end = {
date: moment(updateFields.end as string)
.utc()
.format('YYYY-MM-DD')
};
}
//exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z
//https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
body.recurrence = [];
if (updateFields.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()};`
);
}
if (updateFields.repeatHowManyTimes) {
body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`);
}
if (updateFields.repeatUntil) {
body.recurrence?.push(
`UNTIL=${moment(updateFields.repeatUntil as string)
.utc()
.format('YYYYMMDDTHHmmss')}Z`
);
}
if (body.recurrence.length !== 0) {
body.recurrence = [`RRULE:${body.recurrence.join('')}`];
} else {
delete body.recurrence;
}
responseData = await googleApiRequest.call(
this,
'PATCH',
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
body,
qs
);
}
}
}
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)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -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<any> { // 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<any> { // 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<IDataObject> {
//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);
}

View file

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

View file

@ -0,0 +1,293 @@
// import { google } from 'googleapis';
// import {
// IHookFunctions,
// IWebhookFunctions,
// } from 'n8n-core';
// import {
// IDataObject,
// INodeTypeDescription,
// INodeType,
// IWebhookResponseData,
// } from 'n8n-workflow';
// import { getAuthenticationClient } from './GoogleApi';
// export class GoogleDriveTrigger implements INodeType {
// description: INodeTypeDescription = {
// displayName: 'Google Drive Trigger',
// name: 'googleDriveTrigger',
// icon: 'file:googleDrive.png',
// group: ['trigger'],
// version: 1,
// subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}',
// description: 'Starts the workflow when a file on Google Drive got changed.',
// defaults: {
// name: 'Google Drive Trigger',
// color: '#3f87f2',
// },
// inputs: [],
// outputs: ['main'],
// credentials: [
// {
// name: 'googleApi',
// required: true,
// }
// ],
// webhooks: [
// {
// name: 'default',
// httpMethod: 'POST',
// responseMode: 'onReceived',
// path: 'webhook',
// },
// ],
// properties: [
// {
// displayName: 'Resource Id',
// name: 'resourceId',
// type: 'string',
// default: '',
// required: true,
// placeholder: '',
// description: 'ID of the resource to watch, for example a file ID.',
// },
// ],
// };
// // @ts-ignore (because of request)
// webhookMethods = {
// default: {
// async checkExists(this: IHookFunctions): Promise<boolean> {
// // const webhookData = this.getWorkflowStaticData('node');
// // if (webhookData.webhookId === undefined) {
// // // No webhook id is set so no webhook can exist
// // return false;
// // }
// // // 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 = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`;
// // try {
// // await githubApiRequest.call(this, 'GET', endpoint, {});
// // } catch (e) {
// // if (e.message.includes('[404]:')) {
// // // Webhook does not exist
// // delete webhookData.webhookId;
// // delete webhookData.webhookEvents;
// // return false;
// // }
// // // Some error occured
// // throw e;
// // }
// // If it did not error then the webhook exists
// // return true;
// return false;
// },
// async create(this: IHookFunctions): Promise<boolean> {
// const webhookUrl = this.getNodeWebhookUrl('default');
// const resourceId = this.getNodeParameter('resourceId') as string;
// 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',
// auth: client,
// });
// const accessToken = await client.getAccessToken();
// console.log('accessToken: ');
// console.log(accessToken);
// const asdf = await drive.changes.getStartPageToken();
// // console.log('asdf: ');
// // console.log(asdf);
// const response = await drive.changes.watch({
// //
// pageToken: asdf.data.startPageToken,
// requestBody: {
// id: 'asdf-test-2',
// address: webhookUrl,
// resourceId,
// type: 'web_hook',
// // page_token: '',
// }
// });
// console.log('...response...CREATE');
// console.log(JSON.stringify(response, null, 2));
// // const endpoint = `/repos/${owner}/${repository}/hooks`;
// // const body = {
// // name: 'web',
// // config: {
// // url: webhookUrl,
// // content_type: 'json',
// // // secret: '...later...',
// // insecure_ssl: '1', // '0' -> not allow inscure ssl | '1' -> allow insercure SSL
// // },
// // events,
// // active: true,
// // };
// // let responseData;
// // try {
// // responseData = await githubApiRequest.call(this, 'POST', endpoint, body);
// // } catch (e) {
// // if (e.message.includes('[422]:')) {
// // throw new Error('A webhook with the identical URL exists already. Please delete it manually on Github!');
// // }
// // throw e;
// // }
// // if (responseData.id === undefined || responseData.active !== true) {
// // // Required data is missing so was not successful
// // throw new Error('Github webhook creation response did not contain the expected data.');
// // }
// // const webhookData = this.getWorkflowStaticData('node');
// // webhookData.webhookId = responseData.id as string;
// // webhookData.webhookEvents = responseData.events as string[];
// return true;
// },
// async delete(this: IHookFunctions): Promise<boolean> {
// const webhookUrl = this.getNodeWebhookUrl('default');
// const resourceId = this.getNodeParameter('resourceId') as string;
// 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',
// auth: client,
// });
// // Remove channel
// const response = await drive.channels.stop({
// requestBody: {
// id: 'asdf-test-2',
// address: webhookUrl,
// resourceId,
// type: 'web_hook',
// }
// });
// console.log('...response...DELETE');
// console.log(JSON.stringify(response, null, 2));
// // const webhookData = this.getWorkflowStaticData('node');
// // if (webhookData.webhookId !== undefined) {
// // const owner = this.getNodeParameter('owner') as string;
// // const repository = this.getNodeParameter('repository') as string;
// // const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`;
// // const body = {};
// // try {
// // await githubApiRequest.call(this, 'DELETE', endpoint, body);
// // } catch (e) {
// // return false;
// // }
// // // Remove from the static workflow data so that it is clear
// // // that no webhooks are registred anymore
// // delete webhookData.webhookId;
// // delete webhookData.webhookEvents;
// // }
// return true;
// },
// },
// };
// async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
// const bodyData = this.getBodyData();
// console.log('');
// console.log('');
// console.log('GOT WEBHOOK CALL');
// console.log(JSON.stringify(bodyData, null, 2));
// // Check if the webhook is only the ping from Github to confirm if it workshook_id
// if (bodyData.hook_id !== undefined && bodyData.action === undefined) {
// // Is only the ping and not an actual webhook call. So return 'OK'
// // but do not start the workflow.
// return {
// webhookResponse: 'OK'
// };
// }
// // Is a regular webhoook call
// // TODO: Add headers & requestPath
// const returnData: IDataObject[] = [];
// returnData.push(
// {
// body: bodyData,
// headers: this.getHeaderData(),
// query: this.getQueryData(),
// }
// );
// return {
// workflowData: [
// this.helpers.returnJsonArray(returnData)
// ],
// };
// }
// }

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -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 <JWT> {
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;
}

View file

@ -0,0 +1,129 @@
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, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string;
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
uri: uri || `https://sheets.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;
}
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, 'googleSheetsOAuth2Api', options);
}
} catch (error) {
if (error.response && error.response.body && error.response.body.message) {
// Try to return the error prettier
throw new Error(`Google Sheet error response [${error.statusCode}]: ${error.response.body.message}`);
}
throw error;
}
}
export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // 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<IDataObject> {
//https://developers.google.com/identity/protocols/oauth2/service-account#httprest
const scopes = [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/spreadsheets',
];
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);
}

View file

@ -1,14 +1,20 @@
import { IDataObject } from 'n8n-workflow';
import { google, sheets_v4 } from 'googleapis';
import { JWT } from 'google-auth-library';
import { getAuthenticationClient } from './GoogleApi';
import {
IDataObject,
} from 'n8n-workflow';
import {
IExecuteFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
googleApiRequest,
} from './GenericFunctions';
import {
utils as xlsxUtils,
} from 'xlsx';
const Sheets = google.sheets('v4'); // tslint:disable-line:variable-name
export interface ISheetOptions {
scope: string[];
}
@ -46,18 +52,16 @@ export type ValueRenderOption = 'FORMATTED_VALUE' | 'FORMULA' | 'UNFORMATTED_VAL
export class GoogleSheet {
id: string;
credentials: IGoogleAuthCredentials;
scopes: string[];
executeFunctions: IExecuteFunctions | ILoadOptionsFunctions;
constructor(spreadsheetId: string, credentials: IGoogleAuthCredentials, options?: ISheetOptions | undefined) {
constructor(spreadsheetId: string, executeFunctions: IExecuteFunctions | ILoadOptionsFunctions, options?: ISheetOptions | undefined) {
// options = <SheetOptions>options || {};
if (!options) {
options = {} as ISheetOptions;
}
this.executeFunctions = executeFunctions;
this.id = spreadsheetId;
this.credentials = credentials;
this.scopes = options.scope || ['https://www.googleapis.com/auth/spreadsheets'];
}
@ -69,37 +73,29 @@ export class GoogleSheet {
* @memberof GoogleSheet
*/
async clearData(range: string): Promise<object> {
const client = await this.getAuthenticationClient();
// @ts-ignore
const response = await Sheets.spreadsheets.values.clear(
{
auth: client,
spreadsheetId: this.id,
range,
}
);
const body = {
spreadsheetId: this.id,
range,
};
return response.data;
const response = await googleApiRequest.call(this.executeFunctions, 'POST', `/v4/spreadsheets/${this.id}/values/${range}:clear`, body);
return response;
}
/**
* Returns the cell values
*/
async getData(range: string, valueRenderMode: ValueRenderOption): Promise<string[][] | undefined> {
const client = await this.getAuthenticationClient();
// @ts-ignore
const response = await Sheets.spreadsheets.values.get(
{
auth: client,
spreadsheetId: this.id,
range,
valueRenderOption: valueRenderMode,
}
);
const query = {
valueRenderOption: valueRenderMode,
};
return response.data.values as string[][] | undefined;
const response = await googleApiRequest.call(this.executeFunctions, 'GET', `/v4/spreadsheets/${this.id}/values/${range}`, {}, query);
return response.values as string[][] | undefined;
}
@ -107,39 +103,29 @@ export class GoogleSheet {
* Returns the sheets in a Spreadsheet
*/
async spreadsheetGetSheets() {
const client = await this.getAuthenticationClient();
// @ts-ignore
const response = await Sheets.spreadsheets.get(
{
auth: client,
spreadsheetId: this.id,
fields: 'sheets.properties'
}
);
const query = {
fields: 'sheets.properties',
};
return response.data;
const response = await googleApiRequest.call(this.executeFunctions, 'GET', `/v4/spreadsheets/${this.id}`, {}, query);
return response;
}
/**
* Sets values in one or more ranges of a spreadsheet.
*/
async spreadsheetBatchUpdate(requests: sheets_v4.Schema$Request[]) { // tslint:disable-line:no-any
const client = await this.getAuthenticationClient();
async spreadsheetBatchUpdate(requests: IDataObject[]) { // tslint:disable-line:no-any
// @ts-ignore
const response = await Sheets.spreadsheets.batchUpdate(
{
auth: client,
spreadsheetId: this.id,
requestBody: {
requests,
},
}
);
const body = {
requests
};
return response.data;
const response = await googleApiRequest.call(this.executeFunctions, 'POST', `/v4/spreadsheets/${this.id}:batchUpdate`, body);
return response;
}
@ -147,21 +133,15 @@ export class GoogleSheet {
* Sets the cell values
*/
async batchUpdate(updateData: ISheetUpdateData[], valueInputMode: ValueInputOption) {
const client = await this.getAuthenticationClient();
// @ts-ignore
const response = await Sheets.spreadsheets.values.batchUpdate(
{
auth: client,
spreadsheetId: this.id,
valueInputOption: valueInputMode,
resource: {
data: updateData,
},
}
);
const body = {
data: updateData,
valueInputOption: valueInputMode,
};
return response.data;
const response = await googleApiRequest.call(this.executeFunctions, 'POST', `/v4/spreadsheets/${this.id}/values:batchUpdate`, body);
return response;
}
@ -169,23 +149,15 @@ export class GoogleSheet {
* Sets the cell values
*/
async setData(range: string, data: string[][], valueInputMode: ValueInputOption) {
const client = await this.getAuthenticationClient();
// @ts-ignore
const response = await Sheets.spreadsheets.values.update(
{
// @ts-ignore
auth: client,
spreadsheetId: this.id,
range,
valueInputOption: valueInputMode,
resource: {
values: data
}
}
);
const body = {
valueInputOption: valueInputMode,
values: data,
};
return response.data;
const response = await googleApiRequest.call(this.executeFunctions, 'POST', `/v4/spreadsheets/${this.id}/values/${range}`, body);
return response;
}
@ -193,33 +165,21 @@ export class GoogleSheet {
* Appends the cell values
*/
async appendData(range: string, data: string[][], valueInputMode: ValueInputOption) {
const client = await this.getAuthenticationClient();
// @ts-ignore
const response = await Sheets.spreadsheets.values.append(
{
auth: client,
spreadsheetId: this.id,
range,
valueInputOption: valueInputMode,
resource: {
values: data
}
}
);
const body = {
range,
values: data,
};
return response.data;
const query = {
valueInputOption: valueInputMode,
};
const response = await googleApiRequest.call(this.executeFunctions, 'POST', `/v4/spreadsheets/${this.id}/values/${range}:append`, body, query);
return response;
}
/**
* Returns the authentication client needed to access spreadsheet
*/
async getAuthenticationClient(): Promise<JWT> {
return getAuthenticationClient(this.credentials.email, this.credentials.privateKey, this.scopes);
}
/**
* Returns the given sheet data in a strucutred way
*/
@ -505,5 +465,4 @@ export class GoogleSheet {
return setData;
}
}

View file

@ -1,6 +1,8 @@
import { sheets_v4 } from 'googleapis';
import { IExecuteFunctions } from 'n8n-core';
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
@ -12,7 +14,6 @@ import {
import {
GoogleSheet,
IGoogleAuthCredentials,
ILookupValues,
ISheetUpdateData,
IToDelete,
@ -30,7 +31,7 @@ export class GoogleSheets implements INodeType {
description: 'Read, update and write data to Google Sheets',
defaults: {
name: 'Google Sheets',
color: '#995533',
color: '#0aa55c',
},
inputs: ['main'],
outputs: ['main'],
@ -38,9 +39,43 @@ export class GoogleSheets implements INodeType {
{
name: 'googleApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'serviceAccount',
],
},
},
},
{
name: 'googleSheetsOAuth2Api',
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: 'Operation',
name: 'operation',
@ -541,18 +576,7 @@ export class GoogleSheets implements INodeType {
async getSheets(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const spreadsheetId = this.getCurrentNodeParameter('sheetId') as string;
const credentials = this.getCredentials('googleApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const googleCredentials = {
email: credentials.email,
privateKey: credentials.privateKey,
} as IGoogleAuthCredentials;
const sheet = new GoogleSheet(spreadsheetId, googleCredentials);
const sheet = new GoogleSheet(spreadsheetId, this);
const responseData = await sheet.spreadsheetGetSheets();
if (responseData === undefined) {
@ -579,18 +603,8 @@ export class GoogleSheets implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const spreadsheetId = this.getNodeParameter('sheetId', 0) as string;
const credentials = this.getCredentials('googleApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const googleCredentials = {
email: credentials.email,
privateKey: credentials.privateKey,
} as IGoogleAuthCredentials;
const sheet = new GoogleSheet(spreadsheetId, googleCredentials);
const sheet = new GoogleSheet(spreadsheetId, this);
const operation = this.getNodeParameter('operation', 0) as string;
@ -608,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();
@ -638,7 +652,7 @@ export class GoogleSheets implements INodeType {
// delete
// ----------------------------------
const requests: sheets_v4.Schema$Request[] = [];
const requests: IDataObject[] = [];
const toDelete = this.getNodeParameter('toDelete', 0) as IToDelete;
@ -656,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),
}
}
});
@ -679,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();
@ -721,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);
}
@ -755,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) => {

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -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<any> {
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<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;
}

View file

@ -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<INodePropertyOptions[]> {
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<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = (items.length as unknown) as number;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
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)];
}
}

View file

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

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