mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-23 10:32:17 -08:00
Merge branch 'master' into static-stateless-webhooks
This commit is contained in:
commit
494b1de93f
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}/
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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[] {
|
||||
|
|
159
packages/cli/src/CredentialsHelper.ts
Normal file
159
packages/cli/src/CredentialsHelper.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
63
packages/cli/src/CredentialsOverwrites.ts
Normal file
63
packages/cli/src/CredentialsOverwrites.ts
Normal 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;
|
||||
}
|
79
packages/cli/src/ExternalHooks.ts
Normal file
79
packages/cli/src/ExternalHooks.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
9
packages/cli/templates/oauth-callback.html
Normal file
9
packages/cli/templates/oauth-callback.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<html>
|
||||
<script>
|
||||
(function messageParent() {
|
||||
window.opener.postMessage('success', '*');
|
||||
}());
|
||||
</script>
|
||||
|
||||
Got connected. The window can be closed now.
|
||||
</html>
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -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[];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -19,6 +19,12 @@ export default new Router({
|
|||
sidebar: MainSidebar,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/oauth2/callback',
|
||||
name: 'oAuth2Callback',
|
||||
components: {
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/workflow',
|
||||
name: 'NodeViewNew',
|
||||
|
|
|
@ -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}`;
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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(' '),
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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(' '),
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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(' '),
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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(' ')
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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.',
|
||||
},
|
||||
];
|
||||
}
|
48
packages/nodes-base/credentials/KeapOAuth2Api.credentials.ts
Normal file
48
packages/nodes-base/credentials/KeapOAuth2Api.credentials.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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: ''
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
63
packages/nodes-base/credentials/OAuth1Api.credentials.ts
Normal file
63
packages/nodes-base/credentials/OAuth1Api.credentials.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
}
|
76
packages/nodes-base/credentials/OAuth2Api.credentials.ts
Normal file
76
packages/nodes-base/credentials/OAuth2Api.credentials.ts
Normal 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.',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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.',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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'
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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.',
|
||||
},
|
||||
];
|
||||
}
|
80
packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts
Normal file
80
packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
1125
packages/nodes-base/nodes/Google/Calendar/EventDescription.ts
Normal file
1125
packages/nodes-base/nodes/Google/Calendar/EventDescription.ts
Normal file
File diff suppressed because it is too large
Load diff
28
packages/nodes-base/nodes/Google/Calendar/EventInterface.ts
Normal file
28
packages/nodes-base/nodes/Google/Calendar/EventInterface.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
510
packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts
Normal file
510
packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts
Normal 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)];
|
||||
}
|
||||
}
|
BIN
packages/nodes-base/nodes/Google/Calendar/googleCalendar.png
Normal file
BIN
packages/nodes-base/nodes/Google/Calendar/googleCalendar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
142
packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts
Normal file
142
packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts
Normal 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);
|
||||
}
|
|
@ -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({
|
|
@ -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)
|
||||
// ],
|
||||
// };
|
||||
// }
|
||||
// }
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
@ -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;
|
||||
}
|
129
packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts
Normal file
129
packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts
Normal 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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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) => {
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
92
packages/nodes-base/nodes/Google/Task/GenericFunctions.ts
Normal file
92
packages/nodes-base/nodes/Google/Task/GenericFunctions.ts
Normal 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;
|
||||
}
|
283
packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts
Normal file
283
packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts
Normal 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)];
|
||||
}
|
||||
}
|
492
packages/nodes-base/nodes/Google/Task/TaskDescription.ts
Normal file
492
packages/nodes-base/nodes/Google/Task/TaskDescription.ts
Normal 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
Loading…
Reference in a new issue