Updated node design and node versioning (#1961)

*  introduce versioned nodes

* Export versioned nodes for separate process run

* Add bse node for versioned nodes

* fix node name for versioned nodes

* extend node from nodeVersionedType

* improve nodes base and flow to FE

* revert lib es2019 to es2017

* include version in key to prevent duplicate key

* handle type versions on FE

* clean up

* cleanup nodes base

* add type versions in getNodeParameterOptions

* cleanup

* code review

* code review + add default version to node type description

* remove node default types from store

* 💄 cleanups

* Draft for migrated Mattermost node

* First version of Mattermost node versioned according to node standards

* Correcting deactivate operations name to match currently used one

*  Create utility types

*  Simplify Mattermost types

*  Rename exports for consistency

*  Type channel properties

*  Type message properties

*  Type reaction properties

*  Type user properties

*  Add type import to router

* 🐛 Add missing key

* 🔨 Adjust typo in operation name

* 🔨 Inline exports for channel properties

* 🔨 Inline exports for message properties

* 🔨 Inline exports for reaction properties

* 🔨 Inline exports for user properties

* 🔨 Inline exports for load options

* 👕 Fix lint issue

* 🔨 Inline export for description

* 🔨 Rename descriptions for clarity

* 🔨 Refactor imports/exports for methods

* 🔨 Refactor latest version retrieval

* 🔥 Remove unneeded else clause

When the string literal union is exhausted, the resource key becomes never, so TS disallows wrong key usage.

*  Add overloads to getNodeParameter

*  Improve overload

* 🔥 Remove superfluous INodeVersions type

* 🔨 Relocate pre-existing interface

* 🔥 Remove JSDoc arg descriptions

*  Minor reformatting in transport file

*  Fix API call function type

* Created first draft for Axios requests

* Working version of mattermost node with Axios

* Work in progress for replacing request library

* Improvements to request translations

* Fixed sending files via multipart / form-data

* Fixing translation from request to axios and loading node parameter options

* Improved typing for new http helper

* Added ignore any for specific lines for linting

* Fixed follow redirects changes on http request node and manual execution of previously existing workflow with older node versions

* Adding default headers according to body on httpRequest helper

* Spec error handling and fixed workflows with older node versions

* Showcase how to export errors in a standard format

* Merging master

* Refactored mattermost node to keep files in a uniform structure. Also fix bugs with merges

* Reverting changes to http request node

* Changed nullish comparison and removed repeated code from nodes

* Renamed queryString back to qs and simplified node output

* Simplified some comparisons

* Changed header names to be uc first

* Added default user agent to requests and patch http method support

* Fixed indentation, remove unnecessary file and console log

* Fixed mattermost node name

* Fixed lint issues

* Further fix linting issues

* Further fix lint issues

* Fixed http request helper's return type

Co-authored-by: ahsan-virani <ahsan.virani@gmail.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
Omar Ajoue 2021-09-21 19:38:24 +02:00 committed by GitHub
parent 53fbf664b5
commit 443c2a4d51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
101 changed files with 4016 additions and 2643 deletions

View file

@ -170,6 +170,7 @@ export class ExecuteBatch extends Command {
'missing a required parameter',
'insufficient credit balance',
'request timed out',
'status code 401',
];
// eslint-disable-next-line no-param-reassign

View file

@ -30,6 +30,9 @@ const mockNodeTypes: INodeTypes = {
getByName: (nodeType: string): INodeType | undefined => {
return undefined;
},
getByNameAndVersion: (): INodeType | undefined => {
return undefined;
},
};
export class CredentialsHelper extends ICredentialsHelper {

View file

@ -15,6 +15,7 @@ import {
ILogger,
INodeType,
INodeTypeData,
INodeVersionedType,
LoggerProxy,
} from 'n8n-workflow';
@ -181,13 +182,14 @@ class LoadNodesAndCredentialsClass {
* @returns {Promise<void>}
*/
async loadNodeFromFile(packageName: string, nodeName: string, filePath: string): Promise<void> {
let tempNode: INodeType;
let tempNode: INodeType | INodeVersionedType;
let fullNodeName: string;
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
const tempModule = require(filePath);
try {
tempNode = new tempModule[nodeName]() as INodeType;
tempNode = new tempModule[nodeName]();
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
} catch (error) {
// eslint-disable-next-line no-console
@ -207,13 +209,36 @@ class LoadNodesAndCredentialsClass {
)}`;
}
if (tempNode.executeSingle) {
if (tempNode.hasOwnProperty('executeSingle')) {
this.logger.warn(
`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`,
{ filePath },
);
}
if (tempNode.hasOwnProperty('nodeVersions')) {
const versionedNodeType = (tempNode as INodeVersionedType).getNodeType();
this.addCodex({ node: versionedNodeType, filePath, isCustom: packageName === 'CUSTOM' });
if (
versionedNodeType.description.icon !== undefined &&
versionedNodeType.description.icon.startsWith('file:')
) {
// If a file icon gets used add the full path
versionedNodeType.description.icon = `file:${path.join(
path.dirname(filePath),
versionedNodeType.description.icon.substr(5),
)}`;
}
if (versionedNodeType.hasOwnProperty('executeSingle')) {
this.logger.warn(
`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`,
{ filePath },
);
}
}
if (this.includeNodes !== undefined && !this.includeNodes.includes(fullNodeName)) {
return;
}
@ -257,7 +282,15 @@ class LoadNodesAndCredentialsClass {
* @param obj.isCustom Whether the node is custom
* @returns {void}
*/
addCodex({ node, filePath, isCustom }: { node: INodeType; filePath: string; isCustom: boolean }) {
addCodex({
node,
filePath,
isCustom,
}: {
node: INodeType | INodeVersionedType;
filePath: string;
isCustom: boolean;
}) {
try {
const codex = this.getCodex(filePath);

View file

@ -1,4 +1,14 @@
import { INodeType, INodeTypeData, INodeTypes, NodeHelpers } from 'n8n-workflow';
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import {
INodeType,
INodeTypeData,
INodeTypes,
INodeVersionedType,
NodeHelpers,
} from 'n8n-workflow';
class NodeTypesClass implements INodeTypes {
nodeTypes: INodeTypeData = {};
@ -8,29 +18,30 @@ class NodeTypesClass implements INodeTypes {
// polling nodes the polling times
// eslint-disable-next-line no-restricted-syntax
for (const nodeTypeData of Object.values(nodeTypes)) {
const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeTypeData.type);
const nodeType = NodeHelpers.getVersionedTypeNode(nodeTypeData.type);
const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeType);
if (applyParameters.length) {
// eslint-disable-next-line prefer-spread
nodeTypeData.type.description.properties.unshift.apply(
nodeTypeData.type.description.properties,
applyParameters,
);
nodeType.description.properties.unshift(...applyParameters);
}
}
this.nodeTypes = nodeTypes;
}
getAll(): INodeType[] {
getAll(): Array<INodeType | INodeVersionedType> {
return Object.values(this.nodeTypes).map((data) => data.type);
}
getByName(nodeType: string): INodeType | undefined {
getByName(nodeType: string): INodeType | INodeVersionedType | undefined {
if (this.nodeTypes[nodeType] === undefined) {
throw new Error(`The node-type "${nodeType}" is not known!`);
}
return this.nodeTypes[nodeType].type;
}
getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedTypeNode(this.nodeTypes[nodeType].type, version);
}
}
let nodeTypesInstance: NodeTypesClass | undefined;

View file

@ -68,17 +68,23 @@ import {
INodeCredentials,
INodeParameters,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
INodeTypeNameVersion,
IRunData,
INodeVersionedType,
IWorkflowBase,
IWorkflowCredentials,
LoggerProxy,
NodeCredentialTestRequest,
NodeCredentialTestResult,
NodeHelpers,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
import { NodeVersionedType } from 'n8n-nodes-base';
import * as basicAuth from 'basic-auth';
import * as compression from 'compression';
import * as jwt from 'jsonwebtoken';
@ -882,7 +888,6 @@ class App {
await this.externalHooks.run('workflow.delete', [id]);
const isActive = await this.activeWorkflowRunner.isActive(id);
if (isActive) {
// Before deleting a workflow deactivate it
await this.activeWorkflowRunner.remove(id);
@ -1060,7 +1065,9 @@ class App {
`/${this.restEndpoint}/node-parameter-options`,
ResponseHelper.send(
async (req: express.Request, res: express.Response): Promise<INodePropertyOptions[]> => {
const nodeType = req.query.nodeType as string;
const nodeTypeAndVersion = JSON.parse(
`${req.query.nodeTypeAndVersion}`,
) as INodeTypeNameVersion;
const path = req.query.path as string;
let credentials: INodeCredentials | undefined;
const currentNodeParameters = JSON.parse(
@ -1075,10 +1082,10 @@ class App {
// @ts-ignore
const loadDataInstance = new LoadNodeParameterOptions(
nodeType,
nodeTypeAndVersion,
nodeTypes,
path,
JSON.parse(`${req.query.currentNodeParameters}`),
currentNodeParameters,
credentials,
);
@ -1095,46 +1102,58 @@ class App {
ResponseHelper.send(
async (req: express.Request, res: express.Response): Promise<INodeTypeDescription[]> => {
const returnData: INodeTypeDescription[] = [];
const onlyLatest = req.query.onlyLatest === 'true';
const nodeTypes = NodeTypes();
const allNodes = nodeTypes.getAll();
allNodes.forEach((nodeData) => {
// Make a copy of the object. If we don't do this, then when
// The method below is called the properties are removed for good
// This happens because nodes are returned as reference.
const nodeInfo: INodeTypeDescription = { ...nodeData.description };
const getNodeDescription = (nodeType: INodeType): INodeTypeDescription => {
const nodeInfo: INodeTypeDescription = { ...nodeType.description };
if (req.query.includeProperties !== 'true') {
// @ts-ignore
delete nodeInfo.properties;
}
returnData.push(nodeInfo);
});
return nodeInfo;
};
if (onlyLatest) {
allNodes.forEach((nodeData) => {
const nodeType = NodeHelpers.getVersionedTypeNode(nodeData);
const nodeInfo: INodeTypeDescription = getNodeDescription(nodeType);
returnData.push(nodeInfo);
});
} else {
allNodes.forEach((nodeData) => {
const allNodeTypes = NodeHelpers.getVersionedTypeNodeAll(nodeData);
allNodeTypes.forEach((element) => {
const nodeInfo: INodeTypeDescription = getNodeDescription(element);
returnData.push(nodeInfo);
});
});
}
return returnData;
},
),
);
// Returns node information baesd on namese
// Returns node information based on node names and versions
this.app.post(
`/${this.restEndpoint}/node-types`,
ResponseHelper.send(
async (req: express.Request, res: express.Response): Promise<INodeTypeDescription[]> => {
const nodeNames = _.get(req, 'body.nodeNames', []) as string[];
const nodeInfos = _.get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[];
const nodeTypes = NodeTypes();
return nodeNames
.map((name) => {
try {
return nodeTypes.getByName(name);
} catch (e) {
return undefined;
}
})
.filter((nodeData) => !!nodeData)
.map((nodeData) => nodeData!.description);
const returnData: INodeTypeDescription[] = [];
nodeInfos.forEach((nodeInfo) => {
const nodeType = nodeTypes.getByNameAndVersion(nodeInfo.name, nodeInfo.version);
if (nodeType?.description) {
returnData.push(nodeType.description);
}
});
return returnData;
},
),
);
@ -1156,7 +1175,7 @@ class App {
}`;
const nodeTypes = NodeTypes();
const nodeType = nodeTypes.getByName(nodeTypeName);
const nodeType = nodeTypes.getByNameAndVersion(nodeTypeName);
if (nodeType === undefined) {
res.status(404).send('The nodeType is not known.');
@ -1342,14 +1361,42 @@ class App {
) {
return false;
}
const credentialTestable = node.description.credentials?.find((credential) => {
const testFunctionSearch =
credential.name === credentialType && !!credential.testedBy;
if (testFunctionSearch) {
foundTestFunction = node.methods!.credentialTest![credential.testedBy!];
if (node instanceof NodeVersionedType) {
const versionNames = Object.keys((node as INodeVersionedType).nodeVersions);
for (const versionName of versionNames) {
const nodeType = (node as INodeVersionedType).nodeVersions[
versionName as unknown as number
];
// eslint-disable-next-line @typescript-eslint/no-loop-func
const credentialTestable = nodeType.description.credentials?.find((credential) => {
const testFunctionSearch =
credential.name === credentialType && !!credential.testedBy;
if (testFunctionSearch) {
foundTestFunction = (node as unknown as INodeType).methods!.credentialTest![
credential.testedBy!
];
}
return testFunctionSearch;
});
if (credentialTestable) {
return true;
}
}
return testFunctionSearch;
});
return false;
}
const credentialTestable = (node as INodeType).description.credentials?.find(
(credential) => {
const testFunctionSearch =
credential.name === credentialType && !!credential.testedBy;
if (testFunctionSearch) {
foundTestFunction = (node as INodeType).methods!.credentialTest![
credential.testedBy!
];
}
return testFunctionSearch;
},
);
return !!credentialTestable;
});

View file

@ -139,7 +139,10 @@ export async function executeWebhook(
responseCallback: (error: Error | null, data: IResponseCallbackData) => void,
): Promise<string | undefined> {
// Get the nodeType to know which responseMode is set
const nodeType = workflow.nodeTypes.getByName(workflowStartNode.type);
const nodeType = workflow.nodeTypes.getByNameAndVersion(
workflowStartNode.type,
workflowStartNode.typeVersion,
);
if (nodeType === undefined) {
const errorMessage = `The type of the webhook node "${workflowStartNode.name}" is not known.`;
responseCallback(new Error(errorMessage), {});

View file

@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable no-underscore-dangle */
/* eslint-disable no-continue */
@ -226,13 +229,13 @@ export function getNodeTypeData(nodes: INode[]): ITransferNodeTypes {
// can be loaded again in the process
const returnData: ITransferNodeTypes = {};
for (const nodeTypeName of neededNodeTypes) {
if (nodeTypes.nodeTypes[nodeTypeName] === undefined) {
throw new Error(`The NodeType "${nodeTypeName}" could not be found!`);
if (nodeTypes.nodeTypes[nodeTypeName.type] === undefined) {
throw new Error(`The NodeType "${nodeTypeName.type}" could not be found!`);
}
returnData[nodeTypeName] = {
className: nodeTypes.nodeTypes[nodeTypeName].type.constructor.name,
sourcePath: nodeTypes.nodeTypes[nodeTypeName].sourcePath,
returnData[nodeTypeName.type] = {
className: nodeTypes.nodeTypes[nodeTypeName.type].type.constructor.name,
sourcePath: nodeTypes.nodeTypes[nodeTypeName.type].sourcePath,
};
}
@ -306,12 +309,12 @@ export function getCredentialsDataByNodes(nodes: INode[]): ICredentialsTypeData
* @param {INode[]} nodes
* @returns {string[]}
*/
export function getNeededNodeTypes(nodes: INode[]): string[] {
export function getNeededNodeTypes(nodes: INode[]): Array<{ type: string; version: number }> {
// Check which node-types have to be loaded
const neededNodeTypes: string[] = [];
const neededNodeTypes: Array<{ type: string; version: number }> = [];
for (const node of nodes) {
if (!neededNodeTypes.includes(node.type)) {
neededNodeTypes.push(node.type);
if (neededNodeTypes.find((neededNodes) => node.type === neededNodes.type) === undefined) {
neededNodeTypes.push({ type: node.type, version: node.typeVersion });
}
}

View file

@ -98,7 +98,15 @@ export class WorkflowRunnerProcess {
const tempModule = require(filePath);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const nodeObject = new tempModule[className]();
if (nodeObject.getNodeType !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
tempNode = nodeObject.getNodeType();
} else {
tempNode = nodeObject;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
tempNode = new tempModule[className]() as INodeType;
} catch (error) {
throw new Error(`Error loading node "${nodeTypeName}" from: "${filePath}"`);

View file

@ -42,15 +42,18 @@
"typescript": "~4.3.5"
},
"dependencies": {
"axios": "^0.21.1",
"client-oauth2": "^4.2.5",
"cron": "^1.7.2",
"crypto-js": "~4.1.1",
"file-type": "^14.6.2",
"form-data": "^4.0.0",
"lodash.get": "^4.4.2",
"mime-types": "^2.1.27",
"n8n-workflow": "~0.69.0",
"oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0",
"qs": "^6.10.1",
"request": "^2.88.2",
"request-promise-native": "^1.0.7"
},

View file

@ -8,6 +8,7 @@ import {
IExecuteFunctions as IExecuteFunctionsBase,
IExecuteSingleFunctions as IExecuteSingleFunctionsBase,
IHookFunctions as IHookFunctionsBase,
IHttpRequestOptions,
ILoadOptionsFunctions as ILoadOptionsFunctionsBase,
INodeExecutionData,
INodeType,
@ -34,13 +35,14 @@ export interface IProcessMessage {
export interface IExecuteFunctions extends IExecuteFunctionsBase {
helpers: {
httpRequest(requestOptions: IHttpRequestOptions): Promise<any>; // tslint:disable-line:no-any
prepareBinaryData(
binaryData: Buffer,
filePath?: string,
mimeType?: string,
): Promise<IBinaryData>;
getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise<Buffer>;
request: requestPromise.RequestPromiseAPI;
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -58,12 +60,13 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase {
export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
helpers: {
httpRequest(requestOptions: IHttpRequestOptions): Promise<any>; // tslint:disable-line:no-any
prepareBinaryData(
binaryData: Buffer,
filePath?: string,
mimeType?: string,
): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI;
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -80,12 +83,13 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
export interface IPollFunctions extends IPollFunctionsBase {
helpers: {
httpRequest(requestOptions: IHttpRequestOptions): Promise<any>; // tslint:disable-line:no-any
prepareBinaryData(
binaryData: Buffer,
filePath?: string,
mimeType?: string,
): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI;
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -107,12 +111,13 @@ export interface IResponseError extends Error {
export interface ITriggerFunctions extends ITriggerFunctionsBase {
helpers: {
httpRequest(requestOptions: IHttpRequestOptions): Promise<any>; // tslint:disable-line:no-any
prepareBinaryData(
binaryData: Buffer,
filePath?: string,
mimeType?: string,
): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI;
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -144,7 +149,8 @@ export interface IUserSettings {
export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
helpers: {
request?: requestPromise.RequestPromiseAPI;
httpRequest(requestOptions: IHttpRequestOptions): Promise<any>; // tslint:disable-line:no-any
request?: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestOAuth2?: (
this: IAllExecuteFunctions,
credentialsType: string,
@ -167,7 +173,8 @@ export interface ICredentialTestFunctions extends ICredentialTestFunctionsBase {
export interface IHookFunctions extends IHookFunctionsBase {
helpers: {
request: requestPromise.RequestPromiseAPI;
httpRequest(requestOptions: IHttpRequestOptions): Promise<any>; // tslint:disable-line:no-any
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -184,12 +191,13 @@ export interface IHookFunctions extends IHookFunctionsBase {
export interface IWebhookFunctions extends IWebhookFunctionsBase {
helpers: {
httpRequest(requestOptions: IHttpRequestOptions): Promise<any>; // tslint:disable-line:no-any
prepareBinaryData(
binaryData: Buffer,
filePath?: string,
mimeType?: string,
): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI;
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,

View file

@ -1,9 +1,15 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {
INode,
INodeCredentials,
INodeParameters,
INodePropertyOptions,
INodeTypeNameVersion,
INodeTypes,
IWorkflowExecuteAdditionalData,
Workflow,
@ -21,27 +27,30 @@ export class LoadNodeParameterOptions {
workflow: Workflow;
constructor(
nodeTypeName: string,
nodeTypeNameAndVersion: INodeTypeNameVersion,
nodeTypes: INodeTypes,
path: string,
currentNodeParameters: INodeParameters,
credentials?: INodeCredentials,
) {
const nodeType = nodeTypes.getByNameAndVersion(
nodeTypeNameAndVersion.name,
nodeTypeNameAndVersion.version,
);
this.path = path;
const nodeType = nodeTypes.getByName(nodeTypeName);
if (nodeType === undefined) {
throw new Error(`The node-type "${nodeTypeName}" is not known!`);
throw new Error(
`The node-type "${nodeTypeNameAndVersion.name} v${nodeTypeNameAndVersion.version}" is not known!`,
);
}
const nodeData: INode = {
parameters: currentNodeParameters,
name: TEMP_NODE_NAME,
type: nodeTypeName,
typeVersion: 1,
type: nodeTypeNameAndVersion.name,
typeVersion: nodeTypeNameAndVersion.version,
position: [0, 0],
};
if (credentials) {
nodeData.credentials = credentials;
}
@ -91,12 +100,13 @@ export class LoadNodeParameterOptions {
): Promise<INodePropertyOptions[]> {
const node = this.workflow.getNode(TEMP_NODE_NAME);
const nodeType = this.workflow.nodeTypes.getByName(node!.type);
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(node!.type, node?.typeVersion);
if (
nodeType!.methods === undefined ||
nodeType!.methods.loadOptions === undefined ||
nodeType!.methods.loadOptions[methodName] === undefined
!nodeType ||
nodeType.methods === undefined ||
nodeType.methods.loadOptions === undefined ||
nodeType.methods.loadOptions[methodName] === undefined
) {
throw new Error(
`The node-type "${node!.type}" does not have the method "${methodName}" defined!`,
@ -110,6 +120,6 @@ export class LoadNodeParameterOptions {
additionalData,
);
return nodeType!.methods.loadOptions[methodName].call(thisArgs);
return nodeType.methods.loadOptions[methodName].call(thisArgs);
}
}

View file

@ -1,3 +1,4 @@
/* eslint-disable no-lonely-if */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-prototype-builtins */
/* eslint-disable @typescript-eslint/no-unused-vars */
@ -13,6 +14,7 @@
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable no-param-reassign */
import {
GenericValue,
IAllExecuteFunctions,
IBinaryData,
IContextObject,
@ -22,6 +24,9 @@ import {
IExecuteFunctions,
IExecuteSingleFunctions,
IExecuteWorkflowInfo,
IHttpRequestOptions,
IN8nHttpFullResponse,
IN8nHttpResponse,
INode,
INodeExecutionData,
INodeParameters,
@ -48,6 +53,8 @@ import {
LoggerProxy as Logger,
} from 'n8n-workflow';
import { Agent } from 'https';
import { stringify } from 'qs';
import * as clientOAuth1 from 'oauth-1.0a';
import { Token } from 'oauth-1.0a';
import * as clientOAuth2 from 'client-oauth2';
@ -55,6 +62,7 @@ import * as clientOAuth2 from 'client-oauth2';
import { get } from 'lodash';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as express from 'express';
import * as FormData from 'form-data';
import * as path from 'path';
import { OptionsWithUri, OptionsWithUrl } from 'request';
import * as requestPromise from 'request-promise-native';
@ -62,6 +70,8 @@ import { createHmac } from 'crypto';
import { fromBuffer } from 'file-type';
import { lookup } from 'mime-types';
import axios, { AxiosProxyConfig, AxiosRequestConfig, Method } from 'axios';
import { URLSearchParams } from 'url';
// eslint-disable-next-line import/no-cycle
import {
BINARY_ENCODING,
@ -73,10 +83,425 @@ import {
PLACEHOLDER_EMPTY_EXECUTION_ID,
} from '.';
axios.defaults.timeout = 300000;
// Prevent axios from adding x-form-www-urlencoded headers by default
axios.defaults.headers.post = {};
const requestPromiseWithDefaults = requestPromise.defaults({
timeout: 300000, // 5 minutes
});
async function parseRequestObject(requestObject: IDataObject) {
// This function is a temporary implementation
// That translates all http requests done via
// the request library to axios directly
// We are not using n8n's interface as it would
// an unnecessary step, considering the `request`
// helper can be deprecated and removed.
const axiosConfig: AxiosRequestConfig = {};
if (requestObject.headers !== undefined) {
axiosConfig.headers = requestObject.headers as string;
}
// Let's start parsing the hardest part, which is the request body.
// The process here is as following?
// - Check if we have a `content-type` header. If this was set,
// we will follow
// - Check if the `form` property was set. If yes, then it's x-www-form-urlencoded
// - Check if the `formData` property exists. If yes, then it's multipart/form-data
// - Lastly, we should have a regular `body` that is probably a JSON.
const contentTypeHeaderKeyName =
axiosConfig.headers &&
Object.keys(axiosConfig.headers).find(
(headerName) => headerName.toLowerCase() === 'content-type',
);
const contentType =
contentTypeHeaderKeyName &&
(axiosConfig.headers[contentTypeHeaderKeyName] as string | undefined);
if (contentType === 'application/x-www-form-urlencoded' && requestObject.formData === undefined) {
// there are nodes incorrectly created, informing the content type header
// and also using formData. Request lib takes precedence for the formData.
// We will do the same.
// Merge body and form properties.
// @ts-ignore
axiosConfig.data =
typeof requestObject.body === 'string'
? requestObject.body
: new URLSearchParams(
Object.assign(requestObject.body || {}, requestObject.form || {}) as Record<
string,
string
>,
);
} else if (contentType && contentType.includes('multipart/form-data') !== false) {
if (requestObject.formData !== undefined && requestObject.formData instanceof FormData) {
axiosConfig.data = requestObject.formData;
} else {
const allData = Object.assign(requestObject.body || {}, requestObject.formData || {});
const objectKeys = Object.keys(allData);
if (objectKeys.length > 0) {
// Should be a standard object. We must convert to formdata
const form = new FormData();
objectKeys.forEach((key) => {
const formField = (allData as IDataObject)[key] as IDataObject;
if (formField.hasOwnProperty('value') && formField.value instanceof Buffer) {
let filename;
// @ts-ignore
if (!!formField.options && formField.options.filename !== undefined) {
filename = (formField.options as IDataObject).filename as string;
}
form.append(key, formField.value, filename);
} else {
form.append(key, formField);
}
});
axiosConfig.data = form;
}
}
// replace the existing header with a new one that
// contains the boundary property.
// @ts-ignore
delete axiosConfig.headers[contentTypeHeaderKeyName];
const headers = axiosConfig.data.getHeaders();
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, headers);
} else {
// When using the `form` property it means the content should be x-www-form-urlencoded.
if (requestObject.form !== undefined && requestObject.body === undefined) {
// If we have only form
axiosConfig.data = new URLSearchParams(requestObject.form as Record<string, string>);
if (axiosConfig.headers !== undefined) {
// remove possibly existing content-type headers
const headers = Object.keys(axiosConfig.headers);
headers.forEach((header) =>
header.toLowerCase() === 'content-type' ? delete axiosConfig.headers[header] : null,
);
axiosConfig.headers['Content-Type'] = 'application/x-www-form-urlencoded';
} else {
axiosConfig.headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
}
} else if (requestObject.formData !== undefined) {
// remove any "content-type" that might exist.
if (axiosConfig.headers !== undefined) {
const headers = Object.keys(axiosConfig.headers);
headers.forEach((header) =>
header.toLowerCase() === 'content-type' ? delete axiosConfig.headers[header] : null,
);
}
if (requestObject.formData instanceof FormData) {
axiosConfig.data = requestObject.formData;
} else {
const objectKeys = Object.keys(requestObject.formData as object);
if (objectKeys.length > 0) {
// Should be a standard object. We must convert to formdata
const form = new FormData();
objectKeys.forEach((key) => {
const formField = (requestObject.formData as IDataObject)[key] as IDataObject;
if (formField.hasOwnProperty('value') && formField.value instanceof Buffer) {
let filename;
// @ts-ignore
if (!!formField.options && formField.options.filename !== undefined) {
filename = (formField.options as IDataObject).filename as string;
}
form.append(key, formField.value, filename);
} else {
form.append(key, formField);
}
});
axiosConfig.data = form;
}
}
// Mix in headers as FormData creates the boundary.
const headers = axiosConfig.data.getHeaders();
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, headers);
} else if (requestObject.body !== undefined) {
// If we have body and possibly form
if (requestObject.form !== undefined) {
// merge both objects when exist.
requestObject.body = Object.assign(requestObject.body, requestObject.form);
}
axiosConfig.data = requestObject.body as FormData | GenericValue | GenericValue[];
}
}
if (requestObject.uri !== undefined) {
axiosConfig.url = requestObject.uri as string;
}
if (requestObject.url !== undefined) {
axiosConfig.url = requestObject.url as string;
}
if (requestObject.method !== undefined) {
axiosConfig.method = requestObject.method as Method;
}
if (requestObject.qs !== undefined && Object.keys(requestObject.qs as object).length > 0) {
axiosConfig.params = requestObject.qs as IDataObject;
}
if (requestObject.useQuerystring === true) {
axiosConfig.paramsSerializer = (params) => {
return stringify(params, { arrayFormat: 'repeat' });
};
}
if (requestObject.auth !== undefined) {
// Check support for sendImmediately
if ((requestObject.auth as IDataObject).bearer !== undefined) {
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
Authorization: `Bearer ${(requestObject.auth as IDataObject).bearer}`,
});
} else {
const authObj = requestObject.auth as IDataObject;
// Request accepts both user/username and pass/password
axiosConfig.auth = {
username: (authObj.user || authObj.username) as string,
password: (authObj.password || authObj.pass) as string,
};
}
}
// Only set header if we have a body, otherwise it may fail
if (requestObject.json === true) {
// Add application/json headers - do not set charset as it breaks a lot of stuff
// only add if no other accept headers was sent.
const acceptHeaderExists =
axiosConfig.headers === undefined
? false
: Object.keys(axiosConfig.headers)
.map((headerKey) => headerKey.toLowerCase())
.includes('accept');
if (!acceptHeaderExists) {
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, {
Accept: 'application/json',
});
}
}
if (requestObject.json === false) {
// Prevent json parsing
axiosConfig.transformResponse = (res) => res;
}
// Axios will follow redirects by default, so we simply tell it otherwise if needed.
if (
requestObject.followRedirect === false &&
((requestObject.method as string | undefined) || 'get').toLowerCase() === 'get'
) {
axiosConfig.maxRedirects = 0;
}
if (
requestObject.followAllRedirect === false &&
((requestObject.method as string | undefined) || 'get').toLowerCase() !== 'get'
) {
axiosConfig.maxRedirects = 0;
}
if (requestObject.rejectUnauthorized === false) {
axiosConfig.httpsAgent = new Agent({
rejectUnauthorized: false,
});
}
if (requestObject.timeout !== undefined) {
axiosConfig.timeout = requestObject.timeout as number;
}
if (requestObject.proxy !== undefined) {
axiosConfig.proxy = requestObject.proxy as AxiosProxyConfig;
}
if (requestObject.encoding === null) {
// When downloading files, return an arrayBuffer.
axiosConfig.responseType = 'arraybuffer';
}
// If we don't set an accept header
// Axios forces "application/json, text/plan, */*"
// Which causes some nodes like NextCloud to break
// as the service returns XML unless requested otherwise.
const allHeaders = axiosConfig.headers ? Object.keys(axiosConfig.headers) : [];
if (!allHeaders.some((headerKey) => headerKey.toLowerCase() === 'accept')) {
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, { accept: '*/*' });
}
if (
axiosConfig.data !== undefined &&
!(axiosConfig.data instanceof Buffer) &&
!allHeaders.some((headerKey) => headerKey.toLowerCase() === 'content-type')
) {
// Use default header for application/json
// If we don't specify this here, axios will add
// application/json; charset=utf-8
// and this breaks a lot of stuff
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, {
'content-type': 'application/json',
});
}
/**
* Missing properties:
* encoding (need testing)
* gzip (ignored - default already works)
* resolveWithFullResponse (implemented elsewhere)
* simple (???)
*/
return axiosConfig;
}
async function proxyRequestToAxios(
uriOrObject: string | IDataObject,
options?: IDataObject,
): Promise<any> {
// tslint:disable-line:no-any
// Check if there's a better way of getting this config here
if (process.env.N8N_USE_DEPRECATED_REQUEST_LIB) {
// @ts-ignore
return requestPromiseWithDefaults.call(null, uriOrObject, options);
}
let axiosConfig: AxiosRequestConfig = {};
let configObject: IDataObject;
if (uriOrObject !== undefined && typeof uriOrObject === 'string') {
axiosConfig.url = uriOrObject;
}
if (uriOrObject !== undefined && typeof uriOrObject === 'object') {
configObject = uriOrObject;
} else {
configObject = options || {};
}
axiosConfig = Object.assign(axiosConfig, await parseRequestObject(configObject));
return new Promise((resolve, reject) => {
axios(axiosConfig)
.then((response) => {
if (configObject.resolveWithFullResponse === true) {
resolve({
body: response.data,
headers: response.headers,
statusCode: response.status,
statusMessage: response.statusText,
request: response.request,
});
} else {
resolve(response.data);
}
})
.catch((error) => {
reject(error);
});
});
}
function searchForHeader(headers: IDataObject, headerName: string) {
if (headers === undefined) {
return undefined;
}
const headerNames = Object.keys(headers);
headerName = headerName.toLowerCase();
return headerNames.find((thisHeader) => thisHeader.toLowerCase() === headerName);
}
function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequestConfig {
// Destructure properties with the same name first.
const { headers, method, timeout, auth, proxy, url } = n8nRequest;
const axiosRequest = {
headers: headers ?? {},
method,
timeout,
auth,
proxy,
url,
} as AxiosRequestConfig;
axiosRequest.params = n8nRequest.qs;
if (n8nRequest.disableFollowRedirect === true) {
axiosRequest.maxRedirects = 0;
}
if (n8nRequest.encoding !== undefined) {
axiosRequest.responseType = n8nRequest.encoding;
}
if (n8nRequest.skipSslCertificateValidation === true) {
axiosRequest.httpsAgent = new Agent({
rejectUnauthorized: false,
});
}
if (n8nRequest.arrayFormat !== undefined) {
axiosRequest.paramsSerializer = (params) => {
return stringify(params, { arrayFormat: n8nRequest.arrayFormat });
};
}
if (n8nRequest.body) {
axiosRequest.data = n8nRequest.body;
// Let's add some useful header standards here.
const existingContentTypeHeaderKey = searchForHeader(axiosRequest.headers, 'content-type');
if (existingContentTypeHeaderKey === undefined) {
// We are only setting content type headers if the user did
// not set it already manually. We're not overriding, even if it's wrong.
if (axiosRequest.data instanceof FormData) {
axiosRequest.headers = axiosRequest.headers || {};
axiosRequest.headers['Content-Type'] = 'multipart/form-data';
} else if (axiosRequest.data instanceof URLSearchParams) {
axiosRequest.headers = axiosRequest.headers || {};
axiosRequest.headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
}
}
if (n8nRequest.json) {
const key = searchForHeader(axiosRequest.headers, 'accept');
// If key exists, then the user has set both accept
// header and the json flag. Header should take precedence.
if (!key) {
axiosRequest.headers.Accept = 'application/json';
}
}
const userAgentHeader = searchForHeader(axiosRequest.headers, 'user-agent');
// If key exists, then the user has set both accept
// header and the json flag. Header should take precedence.
if (!userAgentHeader) {
axiosRequest.headers['User-Agent'] = 'n8n';
}
return axiosRequest;
}
async function httpRequest(
requestParams: IHttpRequestOptions,
): Promise<IN8nHttpFullResponse | IN8nHttpResponse> {
// tslint:disable-line:no-any
const axiosRequest = convertN8nRequestToAxios(requestParams);
const result = await axios(axiosRequest);
if (requestParams.returnFullResponse) {
return {
body: result.data,
headers: result.headers,
statusCode: result.status,
statusMessage: result.statusText,
};
}
return result.data;
}
/**
* Returns binary data buffer for given item index and property name.
*
@ -412,7 +837,7 @@ export async function getCredentials(
itemIndex?: number,
): Promise<ICredentialDataDecryptedObject | undefined> {
// Get the NodeType as it has the information if the credentials are required
const nodeType = workflow.nodeTypes.getByName(node.type);
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
throw new NodeOperationError(
node,
@ -543,7 +968,7 @@ export function getNodeParameter(
additionalKeys: IWorkflowDataProxyAdditionalKeys,
fallbackValue?: any,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object {
const nodeType = workflow.nodeTypes.getByName(node.type);
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
throw new Error(`Node type "${node.type}" is not known so can not return paramter value!`);
}
@ -669,7 +1094,7 @@ export function getWebhookDescription(
workflow: Workflow,
node: INode,
): IWebhookDescription | undefined {
const nodeType = workflow.nodeTypes.getByName(node.type) as INodeType;
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion) as INodeType;
if (nodeType.description.webhooks === undefined) {
// Node does not have any webhooks so return
@ -776,8 +1201,9 @@ export function getExecutePollFunctions(
return workflow.getStaticData(type, node);
},
helpers: {
httpRequest,
prepareBinaryData,
request: requestPromiseWithDefaults,
request: proxyRequestToAxios,
async requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -881,8 +1307,10 @@ export function getExecuteTriggerFunctions(
return workflow.getStaticData(type, node);
},
helpers: {
httpRequest,
prepareBinaryData,
request: requestPromiseWithDefaults,
request: proxyRequestToAxios,
async requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -1072,6 +1500,7 @@ export function getExecuteFunctions(
}
},
helpers: {
httpRequest,
prepareBinaryData,
async getBinaryDataBuffer(
itemIndex: number,
@ -1080,7 +1509,7 @@ export function getExecuteFunctions(
): Promise<Buffer> {
return getBinaryDataBuffer.call(this, inputData, itemIndex, propertyName, inputIndex);
},
request: requestPromiseWithDefaults,
request: proxyRequestToAxios,
async requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -1252,8 +1681,9 @@ export function getExecuteSingleFunctions(
return workflow.getStaticData(type, node);
},
helpers: {
httpRequest,
prepareBinaryData,
request: requestPromiseWithDefaults,
request: proxyRequestToAxios,
async requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -1366,7 +1796,8 @@ export function getLoadOptionsFunctions(
return additionalData.restApiUrl;
},
helpers: {
request: requestPromiseWithDefaults,
httpRequest,
request: proxyRequestToAxios,
async requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -1485,7 +1916,8 @@ export function getExecuteHookFunctions(
return workflow.getStaticData(type, node);
},
helpers: {
request: requestPromiseWithDefaults,
httpRequest,
request: proxyRequestToAxios,
async requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -1630,8 +2062,9 @@ export function getExecuteWebhookFunctions(
},
prepareOutputData: NodeHelpers.prepareOutputData,
helpers: {
httpRequest,
prepareBinaryData,
request: requestPromiseWithDefaults,
request: proxyRequestToAxios,
async requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,

View file

@ -27,6 +27,8 @@ import {
IWaitingForExecution,
IWorkflowExecuteAdditionalData,
LoggerProxy as Logger,
NodeApiError,
NodeOperationError,
Workflow,
WorkflowExecuteMode,
WorkflowOperationError,
@ -624,9 +626,9 @@ export class WorkflowExecute {
} catch (error) {
// Set the error that it can be saved correctly
executionError = {
...error,
message: error.message,
stack: error.stack,
...(error as NodeOperationError | NodeApiError),
message: (error as NodeOperationError | NodeApiError).message,
stack: (error as NodeOperationError | NodeApiError).stack,
};
// Set the incoming data of the node that it can be saved correctly
@ -837,9 +839,9 @@ export class WorkflowExecute {
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
executionError = {
...error,
message: error.message,
stack: error.stack,
...(error as NodeOperationError | NodeApiError),
message: (error as NodeOperationError | NodeApiError).message,
stack: (error as NodeOperationError | NodeApiError).stack,
};
Logger.debug(`Running node "${executionNode.name}" finished with error`, {
@ -889,6 +891,22 @@ export class WorkflowExecute {
}
}
// Merge error information to default output for now
// As the new nodes can report the errors in
// the `error` property.
for (const execution of nodeSuccessData!) {
for (const lineResult of execution) {
if (lineResult.json.$error !== undefined && lineResult.json.$json !== undefined) {
lineResult.error = lineResult.json.$error as NodeApiError | NodeOperationError;
lineResult.json = {
error: (lineResult.json.$error as NodeApiError | NodeOperationError).message,
};
} else if (lineResult.error !== undefined) {
lineResult.json = { error: lineResult.error.message };
}
}
}
// Node executed successfully. So add data and go on.
taskData.data = {
main: nodeSuccessData,

View file

@ -14,6 +14,7 @@ import {
ITaskData,
IWorkflowBase,
IWorkflowExecuteAdditionalData,
NodeHelpers,
NodeParameterValue,
WorkflowHooks,
} from 'n8n-workflow';
@ -720,11 +721,15 @@ class NodeTypesClass implements INodeTypes {
async init(nodeTypes: INodeTypeData): Promise<void> {}
getAll(): INodeType[] {
return Object.values(this.nodeTypes).map((data) => data.type);
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedTypeNode(data.type));
}
getByName(nodeType: string): INodeType {
return this.nodeTypes[nodeType].type;
return this.getByNameAndVersion(nodeType);
}
getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedTypeNode(this.nodeTypes[nodeType].type, version);
}
}

View file

@ -13,6 +13,7 @@ import {
INodeParameters,
INodePropertyOptions,
INodeTypeDescription,
INodeTypeNameVersion,
IRunExecutionData,
IRun,
IRunData,
@ -129,9 +130,9 @@ export interface IRestApi {
stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>;
makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any
getSettings(): Promise<IN8nUISettings>;
getNodeTypes(): Promise<INodeTypeDescription[]>;
getNodesInformation(nodeList: string[]): Promise<INodeTypeDescription[]>;
getNodeParameterOptions(nodeType: string, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
getNodeTypes(onlyLatest?: boolean): Promise<INodeTypeDescription[]>;
getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise<INodeTypeDescription[]>;
getNodeParameterOptions(nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
removeTestWebhook(workflowId: string): Promise<boolean>;
runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>;
createNewWorkflow(sendData: IWorkflowDataUpdate): Promise<IWorkflowDb>;

View file

@ -88,7 +88,6 @@ export default mixins(externalHooks).extend({
filteredNodeTypes(): INodeCreateElement[] {
const nodeTypes: INodeCreateElement[] = this.searchItems;
const filter = this.searchFilter;
const returnData = nodeTypes.filter((el: INodeCreateElement) => {
const nodeType = (el.properties as INodeItemProps).nodeType;
return filter && matchesSelectType(el, this.selectedType) && matchesNodeType(el, filter);

View file

@ -42,7 +42,19 @@ export default Vue.extend({
return this.allNodeTypes
.filter((nodeType: INodeTypeDescription) => {
return !HIDDEN_NODES.includes(nodeType.name);
});
}).reduce((accumulator: INodeTypeDescription[], currentValue: INodeTypeDescription) => {
// keep only latest version of the nodes
// accumulator starts as an empty array.
const exists = accumulator.findIndex(nodes => nodes.name === currentValue.name);
if (exists >= 0 && accumulator[exists].version < currentValue.version) {
// This must be a versioned node and we've found a newer version.
// Replace the previous one with this one.
accumulator[exists] = currentValue;
} else {
accumulator.push(currentValue);
}
return accumulator;
}, []);
},
categoriesWithNodes(): ICategoriesWithNodes {
return getCategoriesWithNodes(this.visibleNodeTypes);

View file

@ -80,7 +80,7 @@ export default mixins(
computed: {
nodeType (): INodeTypeDescription | null {
if (this.node) {
return this.$store.getters.nodeType(this.node.type);
return this.$store.getters.nodeType(this.node.type, this.node.typeVersion);
}
return null;

View file

@ -571,7 +571,7 @@ export default mixins(
const resolvedNodeParameters = this.resolveParameter(currentNodeParameters) as INodeParameters;
try {
const options = await this.restApi().getNodeParameterOptions(this.node.type, this.path, this.remoteMethod, resolvedNodeParameters, this.node.credentials);
const options = await this.restApi().getNodeParameterOptions({name: this.node.type, version: this.node.typeVersion}, this.path, this.remoteMethod, resolvedNodeParameters, this.node.credentials);
this.remoteParameterOptions.push.apply(this.remoteParameterOptions, options);
} catch (error) {
this.remoteParameterOptionsLoadingIssues = error.message;

View file

@ -24,6 +24,7 @@ import {
INodeParameters,
INodePropertyOptions,
INodeTypeDescription,
INodeTypeNameVersion,
} from 'n8n-workflow';
import { makeRestApiRequest } from '@/api/helpers';
@ -82,18 +83,18 @@ export const restApi = Vue.extend({
},
// Returns all node-types
getNodeTypes: (): Promise<INodeTypeDescription[]> => {
return self.restApi().makeRestApiRequest('GET', `/node-types`);
getNodeTypes: (onlyLatest = false): Promise<INodeTypeDescription[]> => {
return self.restApi().makeRestApiRequest('GET', `/node-types`, {onlyLatest});
},
getNodesInformation: (nodeList: string[]): Promise<INodeTypeDescription[]> => {
return self.restApi().makeRestApiRequest('POST', `/node-types`, {nodeNames: nodeList});
getNodesInformation: (nodeInfos: INodeTypeNameVersion[]): Promise<INodeTypeDescription[]> => {
return self.restApi().makeRestApiRequest('POST', `/node-types`, {nodeInfos});
},
// Returns all the parameter options from the server
getNodeParameterOptions: (nodeType: string, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => {
getNodeParameterOptions: (nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => {
const sendData = {
nodeType,
nodeTypeAndVersion,
path,
methodName,
credentials,

View file

@ -16,6 +16,7 @@ import {
INodeTypes,
INodeTypeData,
INodeTypeDescription,
INodeVersionedType,
IRunData,
IRunExecutionData,
IWorfklowIssues,
@ -158,7 +159,7 @@ export const workflowHelpers = mixins(
continue;
}
nodeType = workflow.nodeTypes.getByName(node.type);
nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
// Node type is not known
@ -189,17 +190,28 @@ export const workflowHelpers = mixins(
const nodeTypes: INodeTypes = {
nodeTypes: {},
init: async (nodeTypes?: INodeTypeData): Promise<void> => { },
getAll: (): INodeType[] => {
getAll: (): Array<INodeType | INodeVersionedType> => {
// Does not get used in Workflow so no need to return it
return [];
},
getByName: (nodeType: string): INodeType | undefined => {
getByName: (nodeType: string): INodeType | INodeVersionedType | undefined => {
const nodeTypeDescription = this.$store.getters.nodeType(nodeType);
if (nodeTypeDescription === null) {
return undefined;
}
return {
description: nodeTypeDescription,
};
},
getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => {
const nodeTypeDescription = this.$store.getters.nodeType(nodeType, version);
if (nodeTypeDescription === null) {
return undefined;
}
return {
description: nodeTypeDescription,
};
@ -283,7 +295,7 @@ export const workflowHelpers = mixins(
// Get the data of the node type that we can get the default values
// TODO: Later also has to care about the node-type-version as defaults could be different
const nodeType = this.$store.getters.nodeType(node.type) as INodeTypeDescription;
const nodeType = this.$store.getters.nodeType(node.type, node.typeVersion) as INodeTypeDescription;
if (nodeType !== null) {
// Node-Type is known so we can save the parameters correctly

View file

@ -6,6 +6,7 @@ export const PLACEHOLDER_FILLED_AT_EXECUTION_TIME = '[filled at execution time]'
// workflows
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
export const DEFAULT_NODETYPE_VERSION = 1;
export const DEFAULT_NEW_WORKFLOW_NAME = 'My workflow';
export const MIN_WORKFLOW_NAME_LENGTH = 1;
export const MAX_WORKFLOW_NAME_LENGTH = 128;

View file

@ -2,7 +2,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { PLACEHOLDER_EMPTY_WORKFLOW_ID, DEFAULT_NODETYPE_VERSION } from '@/constants';
import {
IConnection,
@ -589,11 +589,10 @@ export const store = new Vuex.Store({
},
updateNodeTypes (state, nodeTypes: INodeTypeDescription[]) {
const updatedNodeNames = nodeTypes.map(node => node.name) as string[];
const oldNodesNotChanged = state.nodeTypes.filter(node => !updatedNodeNames.includes(node.name));
const updatedNodes = [...oldNodesNotChanged, ...nodeTypes];
Vue.set(state, 'nodeTypes', updatedNodes);
state.nodeTypes = updatedNodes;
const oldNodesToKeep = state.nodeTypes.filter(node => !nodeTypes.find(n => n.name === node.name && n.version === node.version));
const newNodesState = [...oldNodesToKeep, ...nodeTypes];
Vue.set(state, 'nodeTypes', newNodesState);
state.nodeTypes = newNodesState;
},
addSidebarMenuItems (state, menuItems: IMenuItem[]) {
@ -754,9 +753,9 @@ export const store = new Vuex.Store({
allNodeTypes: (state): INodeTypeDescription[] => {
return state.nodeTypes;
},
nodeType: (state) => (nodeType: string): INodeTypeDescription | null => {
nodeType: (state, getters) => (nodeType: string, typeVersion?: number): INodeTypeDescription | null => {
const foundType = state.nodeTypes.find(typeData => {
return typeData.name === nodeType;
return typeData.name === nodeType && typeData.version === (typeVersion || typeData.defaultVersion || DEFAULT_NODETYPE_VERSION);
});
if (foundType === undefined) {

View file

@ -142,6 +142,8 @@ import {
INodeConnections,
INodeIssues,
INodeTypeDescription,
INodeTypeNameVersion,
NodeInputConnections,
NodeHelpers,
Workflow,
IRun,
@ -1867,13 +1869,13 @@ export default mixins(
// Before proceeding we must check if all nodes contain the `properties` attribute.
// Nodes are loaded without this information so we must make sure that all nodes
// being added have this information.
await this.loadNodesProperties(nodes.map(node => node.type));
await this.loadNodesProperties(nodes.map(node => ({name: node.type, version: node.typeVersion})));
// Add the node to the node-list
let nodeType: INodeTypeDescription | null;
let foundNodeIssues: INodeIssues | null;
nodes.forEach((node) => {
nodeType = this.$store.getters.nodeType(node.type);
nodeType = this.$store.getters.nodeType(node.type, node.typeVersion);
// Make sure that some properties always exist
if (!node.hasOwnProperty('disabled')) {
@ -1980,7 +1982,7 @@ export default mixins(
let newName: string;
const createNodes: INode[] = [];
await this.loadNodesProperties(data.nodes.map(node => node.type));
await this.loadNodesProperties(data.nodes.map(node => ({name: node.type, version: node.typeVersion})));
data.nodes.forEach(node => {
if (nodeTypesCount[node.type] !== undefined) {
@ -2206,9 +2208,19 @@ export default mixins(
async loadCredentials (): Promise<void> {
await this.$store.dispatch('credentials/fetchAllCredentials');
},
async loadNodesProperties(nodeNames: string[]): Promise<void> {
const allNodes = this.$store.getters.allNodeTypes;
const nodesToBeFetched = allNodes.filter((node: INodeTypeDescription) => nodeNames.includes(node.name) && !node.hasOwnProperty('properties')).map((node: INodeTypeDescription) => node.name) as string[];
async loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise<void> {
const allNodes:INodeTypeDescription[] = this.$store.getters.allNodeTypes;
const nodesToBeFetched:INodeTypeNameVersion[] = [];
allNodes.forEach(node => {
if(!!nodeInfos.find(n => n.name === node.name && n.version === node.version) && !node.hasOwnProperty('properties')) {
nodesToBeFetched.push({
name: node.name,
version: node.version,
});
}
});
if (nodesToBeFetched.length > 0) {
// Only call API if node information is actually missing
this.startLoading();

View file

@ -133,7 +133,7 @@ export class DeepL implements INodeType {
}
} catch (error) {
if (this.continueOnFail()) {
responseData.push({ error: error.message });
responseData.push({ $error: error, $json: this.getInputData(i)});
continue;
}
throw error;

View file

@ -1,3 +1,4 @@
{
"node": "n8n-nodes-base.httpRequest",
"nodeVersion": "1.0",

View file

@ -1057,4 +1057,4 @@ export class HttpRequest implements INodeType {
return this.prepareOutputData(returnItems);
}
}
}

View file

@ -1,78 +0,0 @@
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
OptionsWithUri,
} from 'request';
import {
IDataObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow';
export interface IAttachment {
fields: {
item?: object[];
};
actions: {
item?: object[];
};
}
/**
* Make an API request to Telegram
*
* @param {IHookFunctions} this
* @param {string} method
* @param {string} url
* @param {object} body
* @returns {Promise<any>}
*/
export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise<any> { // tslint:disable-line:no-any
const credentials = await this.getCredentials('mattermostApi');
if (credentials === undefined) {
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
query = query || {};
const options: OptionsWithUri = {
method,
body,
qs: query,
uri: `${credentials.baseUrl}/api/v4/${endpoint}`,
headers: {
Authorization: `Bearer ${credentials.accessToken}`,
'content-type': 'application/json; charset=utf-8',
},
json: true,
};
try {
return await this.helpers.request!(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
export async function apiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.page = 0;
query.per_page = 100;
do {
responseData = await apiRequest.call(this, method, endpoint, body, query);
query.page++;
returnData.push.apply(returnData, responseData);
} while (
responseData.length !== 0
);
return returnData;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,34 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
} from 'n8n-workflow';
import { versionDescription } from './actions/versionDescription';
import { loadOptions } from './methods';
import { router } from './actions/router';
export class MattermostV1 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...versionDescription,
};
}
methods = { loadOptions };
async execute(this: IExecuteFunctions) {
// Router returns INodeExecutionData[]
// We need to output INodeExecutionData[][]
// So we wrap in []
return [await router.call(this)];
}
}

View file

@ -0,0 +1,33 @@
import {
AllEntities,
Entity,
PropertiesOf,
} from 'n8n-workflow';
type MattermostMap = {
channel: 'addUser' | 'create' | 'delete' | 'members' | 'restore' | 'statistics';
message: 'delete' | 'post' | 'postEphemeral';
reaction: 'create' | 'delete' | 'getAll';
user: 'create' | 'deactive' | 'getAll' | 'getByEmail' | 'getById' | 'invite';
};
export type Mattermost = AllEntities<MattermostMap>;
export type MattermostChannel = Entity<MattermostMap, 'channel'>;
export type MattermostMessage = Entity<MattermostMap, 'message'>;
export type MattermostReaction = Entity<MattermostMap, 'reaction'>;
export type MattermostUser = Entity<MattermostMap, 'user'>;
export type ChannelProperties = PropertiesOf<MattermostChannel>;
export type MessageProperties = PropertiesOf<MattermostMessage>;
export type ReactionProperties = PropertiesOf<MattermostReaction>;
export type UserProperties = PropertiesOf<MattermostUser>;
export interface IAttachment {
fields: {
item?: object[];
};
actions: {
item?: object[];
};
}

View file

@ -0,0 +1,50 @@
import {
ChannelProperties,
} from '../../Interfaces';
export const channelAddUserDescription: ChannelProperties = [
{
displayName: 'Channel ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
operation: [
'addUser',
],
resource: [
'channel',
],
},
},
description: 'The ID of the channel to invite user to.',
},
{
displayName: 'User ID',
name: 'userId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
operation: [
'addUser',
],
resource: [
'channel',
],
},
},
description: 'The ID of the user to invite into channel.',
},
];

View file

@ -0,0 +1,27 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function addUser(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const channelId = this.getNodeParameter('channelId', index) as string;
const body = {} as IDataObject;
const qs = {} as IDataObject;
const requestMethod = 'POST';
const endpoint = `channels/${channelId}/members`;
body.user_id = this.getNodeParameter('userId', index) as string;
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { addUser as execute } from './execute';
import { channelAddUserDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,93 @@
import {
ChannelProperties,
} from '../../Interfaces';
export const channelCreateDescription: ChannelProperties = [
{
displayName: 'Team ID',
name: 'teamId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeams',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'channel',
],
},
},
description: 'The Mattermost Team.',
},
{
displayName: 'Display Name',
name: 'displayName',
type: 'string',
default: '',
placeholder: 'Announcements',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'channel',
],
},
},
required: true,
description: 'The non-unique UI name for the channel',
},
{
displayName: 'Name',
name: 'channel',
type: 'string',
default: '',
placeholder: 'announcements',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'channel',
],
},
},
required: true,
description: 'The unique handle for the channel, will be present in the channel URL',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'channel',
],
},
},
options: [
{
name: 'Private',
value: 'private',
},
{
name: 'Public',
value: 'public',
},
],
default: 'public',
description: 'The type of channel to create.',
},
];

View file

@ -0,0 +1,30 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function create(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const body = {} as IDataObject;
const qs = {} as IDataObject;
const requestMethod = 'POST';
const endpoint = 'channels';
const type = this.getNodeParameter('type', index) as string;
body.team_id = this.getNodeParameter('teamId', index) as string;
body.display_name = this.getNodeParameter('displayName', index) as string;
body.name = this.getNodeParameter('channel', index) as string;
body.type = type === 'public' ? 'O' : 'P';
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { create as execute } from './execute';
import { channelCreateDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,28 @@
import {
ChannelProperties,
} from '../../Interfaces';
export const channelDeleteDescription: ChannelProperties = [
{
displayName: 'Channel ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'channel',
],
},
},
description: 'The ID of the channel to soft delete',
},
];

View file

@ -0,0 +1,25 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function del(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const channelId = this.getNodeParameter('channelId', index) as string;
const body = {} as IDataObject;
const qs = {} as IDataObject;
const requestMethod = 'DELETE';
const endpoint = `channels/${channelId}`;
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { del as execute } from './execute';
import { channelDeleteDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,72 @@
import * as create from './create';
import * as del from './del';
import * as members from './members';
import * as restore from './restore';
import * as addUser from './addUser';
import * as statistics from './statistics';
import { INodeProperties } from 'n8n-workflow';
export {
create,
del as delete,
members,
restore,
addUser,
statistics,
};
export const descriptions = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'channel',
],
},
},
options: [
{
name: 'Add User',
value: 'addUser',
description: 'Add a user to a channel',
},
{
name: 'Create',
value: 'create',
description: 'Create a new channel',
},
{
name: 'Delete',
value: 'delete',
description: 'Soft delete a channel',
},
{
name: 'Member',
value: 'members',
description: 'Get a page of members for a channel',
},
{
name: 'Restore',
value: 'restore',
description: 'Restores a soft deleted channel',
},
{
name: 'Statistics',
value: 'statistics',
description: 'Get statistics for a channel',
},
],
default: 'create',
description: 'The operation to perform.',
},
...create.description,
...del.description,
...members.description,
...restore.description,
...addUser.description,
...statistics.description,
] as INodeProperties[];

View file

@ -0,0 +1,112 @@
import {
ChannelProperties,
} from '../../Interfaces';
export const channelMembersDescription: ChannelProperties = [
{
displayName: 'Team ID',
name: 'teamId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeams',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
operation: [
'members',
],
resource: [
'channel',
],
},
},
description: 'The Mattermost Team.',
},
{
displayName: 'Channel ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannelsInTeam',
loadOptionsDependsOn: [
'teamId',
],
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
operation: [
'members',
],
resource: [
'channel',
],
},
},
description: 'The Mattermost Team.',
},
{
displayName: 'Resolve Data',
name: 'resolveData',
type: 'boolean',
displayOptions: {
show: {
resource: [
'channel',
],
operation: [
'members',
],
},
},
default: true,
description: 'By default the response only contain the ID of the user.<br />If this option gets activated it will resolve the user automatically.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'members',
],
resource: [
'channel',
],
},
},
default: true,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'members',
],
resource: [
'channel',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'How many results to return.',
},
];

View file

@ -0,0 +1,51 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
apiRequestAllItems,
} from '../../../transport';
export async function members(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const channelId = this.getNodeParameter('channelId', index) as string;
const returnAll = this.getNodeParameter('returnAll', index) as boolean;
const resolveData = this.getNodeParameter('resolveData', index) as boolean;
const limit = this.getNodeParameter('limit', index, 0) as number;
const body = {} as IDataObject;
const qs = {} as IDataObject;
const requestMethod = 'GET';
const endpoint = `channels/${channelId}/members`;
if (returnAll === false) {
qs.per_page = this.getNodeParameter('limit', index) as number;
}
let responseData;
if (returnAll) {
responseData = await apiRequestAllItems.call(this, requestMethod, endpoint, body, qs);
} else {
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
if (limit) {
responseData = responseData.slice(0, limit);
}
if (resolveData) {
const userIds: string[] = [];
for (const data of responseData) {
userIds.push(data.user_id);
}
if (userIds.length > 0) {
responseData = await apiRequest.call(this, 'POST', 'users/ids', userIds, qs);
}
}
}
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { members as execute } from './execute';
import { channelMembersDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,24 @@
import {
ChannelProperties,
} from '../../Interfaces';
export const channelRestoreDescription: ChannelProperties = [
{
displayName: 'Channel ID',
name: 'channelId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'restore',
],
resource: [
'channel',
],
},
},
description: 'The ID of the channel to restore.',
},
];

View file

@ -0,0 +1,27 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
apiRequestAllItems,
} from '../../../transport';
export async function restore(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const channelId = this.getNodeParameter('channelId', index) as string;
const body = {} as IDataObject;
const qs = {} as IDataObject;
const requestMethod = 'POST';
const endpoint = `channels/${channelId}/restore`;
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { restore as execute } from './execute';
import { channelRestoreDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,29 @@
import {
ChannelProperties,
} from '../../Interfaces';
export const channelStatisticsDescription: ChannelProperties = [
{
displayName: 'Channel ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
operation: [
'statistics',
],
resource: [
'channel',
],
},
},
description: 'The ID of the channel to get the statistics from.',
},
];

View file

@ -0,0 +1,25 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function statistics(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const channelId = this.getNodeParameter('channelId', index) as string;
const body = {} as IDataObject;
const qs = {} as IDataObject;
const requestMethod = 'GET';
const endpoint = `channels/${channelId}/stats`;
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { statistics as execute } from './execute';
import { channelStatisticsDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,24 @@
import {
MessageProperties,
} from '../../Interfaces';
export const messageDeleteDescription: MessageProperties = [
{
displayName: 'Post ID',
name: 'postId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'delete',
],
},
},
default: '',
description: 'ID of the post to delete',
},
];

View file

@ -0,0 +1,25 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function del(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const postId = this.getNodeParameter('postId', index) as string;
const body = {} as IDataObject;
const qs = {} as IDataObject;
const requestMethod = 'DELETE';
const endpoint = `posts/${postId}`;
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { del as execute } from './execute';
import { messageDeleteDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,48 @@
import * as del from './del';
import * as post from './post';
import * as postEphemeral from './postEphemeral';
import { INodeProperties } from 'n8n-workflow';
export {
del as delete,
post,
postEphemeral,
};
export const descriptions = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'message',
],
},
},
options: [
{
name: 'Delete',
value: 'delete',
description: 'Soft delete a post, by marking the post as deleted in the database',
},
{
name: 'Post',
value: 'post',
description: 'Post a message into a channel',
},
{
name: 'Post Ephemeral',
value: 'postEphemeral',
description: 'Post an ephemeral message into a channel',
},
],
default: 'post',
description: 'The operation to perform',
},
...del.description,
...post.description,
...postEphemeral.description,
] as INodeProperties[];

View file

@ -0,0 +1,440 @@
import {
MessageProperties,
} from '../../Interfaces';
export const messagePostDescription: MessageProperties = [
{
displayName: 'Channel ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
operation: [
'post',
],
resource: [
'message',
],
},
},
description: 'The ID of the channel to post to.',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
operation: [
'post',
],
resource: [
'message',
],
},
},
description: 'The text to send.',
},
{
displayName: 'Attachments',
name: 'attachments',
type: 'collection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add attachment',
},
displayOptions: {
show: {
operation: [
'post',
],
resource: [
'message',
],
},
},
default: {},
description: 'The attachment to add',
placeholder: 'Add attachment item',
options: [
{
displayName: 'Actions',
name: 'actions',
placeholder: 'Add Actions',
description: 'Actions to add to message. More information can be found <a href="https://docs.mattermost.com/developer/interactive-messages.html" target="_blank">here</a>',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Item',
name: 'item',
values: [
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Button',
value: 'button',
},
{
name: 'Select',
value: 'select',
},
],
default: 'button',
description: 'The type of the action.',
},
{
displayName: 'Data Source',
name: 'data_source',
type: 'options',
displayOptions: {
show: {
type: [
'select',
],
},
},
options: [
{
name: 'Channels',
value: 'channels',
},
{
name: 'Custom',
value: 'custom',
},
{
name: 'Users',
value: 'users',
},
],
default: 'custom',
description: 'The type of the action.',
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
description: 'Adds a new option to select field.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
data_source: [
'custom',
],
type: [
'select',
],
},
},
default: {},
options: [
{
name: 'option',
displayName: 'Option',
default: {},
values: [
{
displayName: 'Option Text',
name: 'text',
type: 'string',
default: '',
description: 'Text of the option.',
},
{
displayName: 'Option Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the option.',
},
],
},
],
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the Action.',
},
{
displayName: 'Integration',
name: 'integration',
placeholder: 'Add Integration',
description: 'Integration to add to message.',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
displayName: 'Item',
name: 'item',
default: {},
values: [
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
description: 'URL of the Integration.',
},
{
displayName: 'Context',
name: 'context',
placeholder: 'Add Context to Integration',
description: 'Adds a Context values set.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'property',
displayName: 'Property',
default: {},
values: [
{
displayName: 'Property Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the property to set.',
},
{
displayName: 'Property Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the property to set.',
},
],
},
],
},
],
},
],
},
],
},
],
},
{
displayName: 'Author Icon',
name: 'author_icon',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Icon which should appear for the user.',
},
{
displayName: 'Author Link',
name: 'author_link',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Link for the author.',
},
{
displayName: 'Author Name',
name: 'author_name',
type: 'string',
default: '',
description: 'Name that should appear.',
},
{
displayName: 'Color',
name: 'color',
type: 'color',
default: '#ff0000',
description: 'Color of the line left of text.',
},
{
displayName: 'Fallback Text',
name: 'fallback',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Required plain-text summary of the attachment.',
},
{
displayName: 'Fields',
name: 'fields',
placeholder: 'Add Fields',
description: 'Fields to add to message.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'item',
displayName: 'Item',
values: [
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'Title of the item.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the item.',
},
{
displayName: 'Short',
name: 'short',
type: 'boolean',
default: true,
description: 'If items can be displayed next to each other.',
},
],
},
],
},
{
displayName: 'Footer',
name: 'footer',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Text of footer to add.',
},
{
displayName: 'Footer Icon',
name: 'footer_icon',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Icon which should appear next to footer.',
},
{
displayName: 'Image URL',
name: 'image_url',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'URL of image.',
},
{
displayName: 'Pretext',
name: 'pretext',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Text which appears before the message block.',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Text to send.',
},
{
displayName: 'Thumbnail URL',
name: 'thumb_url',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'URL of thumbnail.',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Title of the message.',
},
{
displayName: 'Title Link',
name: 'title_link',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Link of the title.',
},
],
},
{
displayName: 'Other Options',
name: 'otherOptions',
type: 'collection',
displayOptions: {
show: {
operation: [
'post',
],
resource: [
'message',
],
},
},
default: {},
description: 'Other options to set',
placeholder: 'Add options',
options: [
{
displayName: 'Make Comment',
name: 'root_id',
type: 'string',
default: '',
description: 'The post ID to comment on',
},
],
},
];

View file

@ -0,0 +1,98 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
import {
IAttachment,
} from '../../Interfaces';
export async function post(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const body = {} as IDataObject;
const qs = {} as IDataObject;
const requestMethod = 'POST';
const endpoint = `posts`;
body.channel_id = this.getNodeParameter('channelId', index) as string;
body.message = this.getNodeParameter('message', index) as string;
const attachments = this.getNodeParameter('attachments', index, []) as unknown as IAttachment[];
// The node does save the fields data differently than the API
// expects so fix the data befre we send the request
for (const attachment of attachments) {
if (attachment.fields !== undefined) {
if (attachment.fields.item !== undefined) {
// Move the field-content up
// @ts-ignore
attachment.fields = attachment.fields.item;
} else {
// If it does not have any items set remove it
// @ts-ignore
delete attachment.fields;
}
}
}
for (const attachment of attachments) {
if (attachment.actions !== undefined) {
if (attachment.actions.item !== undefined) {
// Move the field-content up
// @ts-ignore
attachment.actions = attachment.actions.item;
} else {
// If it does not have any items set remove it
// @ts-ignore
delete attachment.actions;
}
}
}
for (const attachment of attachments) {
if (Array.isArray(attachment.actions)) {
for (const attaction of attachment.actions) {
if (attaction.type === 'button') {
delete attaction.type;
}
if (attaction.data_source === 'custom') {
delete attaction.data_source;
}
if (attaction.options) {
attaction.options = attaction.options.option;
}
if (attaction.integration.item !== undefined) {
attaction.integration = attaction.integration.item;
if (Array.isArray(attaction.integration.context.property)) {
const tmpcontex = {};
for (const attactionintegprop of attaction.integration.context.property) {
Object.assign(tmpcontex, { [attactionintegprop.name]: attactionintegprop.value });
}
delete attaction.integration.context;
attaction.integration.context = tmpcontex;
}
}
}
}
}
body.props = {
attachments,
};
// Add all the other options to the request
const otherOptions = this.getNodeParameter('otherOptions', index) as IDataObject;
Object.assign(body, otherOptions);
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { post as execute } from './execute';
import { messagePostDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,69 @@
import {
MessageProperties,
} from '../../Interfaces';
export const messagePostEphemeralDescription: MessageProperties = [
{
displayName: 'User ID',
name: 'userId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
operation: [
'postEphemeral',
],
resource: [
'message',
],
},
},
description: 'ID of the user to send the ephemeral message to.',
},
{
displayName: 'Channel ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
default: '',
required: true,
displayOptions: {
show: {
operation: [
'postEphemeral',
],
resource: [
'message',
],
},
},
description: 'ID of the channel to send the ephemeral message in.',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
operation: [
'postEphemeral',
],
resource: [
'message',
],
},
},
description: 'Text to send in the ephemeral message.',
},
];

View file

@ -0,0 +1,30 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function postEphemeral(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const qs = {} as IDataObject;
const requestMethod = 'POST';
const endpoint = `posts/ephemeral`;
const body = {
user_id: this.getNodeParameter('userId', index),
post: {
channel_id: this.getNodeParameter('channelId', index),
message: this.getNodeParameter('message', index),
},
} as IDataObject;
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { postEphemeral as execute } from './execute';
import { messagePostEphemeralDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,65 @@
import {
ReactionProperties,
} from '../../Interfaces';
export const reactionCreateDescription: ReactionProperties = [
{
displayName: 'User ID',
name: 'userId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
resource: [
'reaction',
],
operation: [
'create',
],
},
},
description: 'ID of the user sending the reaction.',
},
{
displayName: 'Post ID',
name: 'postId',
type: 'string',
default: '',
placeholder: '3moacfqxmbdw38r38fjprh6zsr',
required: true,
displayOptions: {
show: {
resource: [
'reaction',
],
operation: [
'create',
],
},
},
description: 'ID of the post to react to.<br>Obtainable from the post link:<br><code>https://mattermost.internal.n8n.io/[server]/pl/[postId]</code>',
},
{
displayName: 'Emoji Name',
name: 'emojiName',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'reaction',
],
operation: [
'create',
],
},
},
description: 'Emoji to use for this reaction.',
},
];

View file

@ -0,0 +1,29 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function create(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const qs = {} as IDataObject;
const requestMethod = 'POST';
const endpoint = 'reactions';
const body = {
user_id: this.getNodeParameter('userId', index),
post_id: this.getNodeParameter('postId', index),
emoji_name: (this.getNodeParameter('emojiName', index) as string).replace(/:/g, ''),
create_at: Date.now(),
} as { user_id: string; post_id: string; emoji_name: string; create_at: number };
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { create as execute } from './execute';
import { reactionCreateDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,65 @@
import {
ReactionProperties,
} from '../../Interfaces';
export const reactionDeleteDescription: ReactionProperties = [
{
displayName: 'User ID',
name: 'userId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
resource: [
'reaction',
],
operation: [
'delete',
],
},
},
description: 'ID of the user whose reaction to delete.',
},
{
displayName: 'Post ID',
name: 'postId',
type: 'string',
default: '',
placeholder: '3moacfqxmbdw38r38fjprh6zsr',
required: true,
displayOptions: {
show: {
resource: [
'reaction',
],
operation: [
'delete',
],
},
},
description: 'ID of the post whose reaction to delete.<br>Obtainable from the post link:<br><code>https://mattermost.internal.n8n.io/[server]/pl/[postId]</code>',
},
{
displayName: 'Emoji Name',
name: 'emojiName',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'reaction',
],
operation: [
'delete',
],
},
},
description: 'Name of the emoji to delete.',
},
];

View file

@ -0,0 +1,27 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function del(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const userId = this.getNodeParameter('userId', index) as string;
const postId = this.getNodeParameter('postId', index) as string;
const emojiName = (this.getNodeParameter('emojiName', index) as string).replace(/:/g, '');
const qs = {} as IDataObject;
const requestMethod = 'DELETE';
const endpoint = `users/${userId}/posts/${postId}/reactions/${emojiName}`;
const body = {} as IDataObject;
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { del as execute } from './execute';
import { reactionDeleteDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,65 @@
import {
ReactionProperties,
} from '../../Interfaces';
export const reactionGetAllDescription: ReactionProperties = [
{
displayName: 'Post ID',
name: 'postId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'reaction',
],
operation: [
'getAll',
],
},
},
description: 'One or more (comma-separated) posts to retrieve reactions from.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'reaction',
],
},
},
default: true,
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: [
'reaction',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'How many results to return.',
},
];

View file

@ -0,0 +1,28 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function getAll(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const postId = this.getNodeParameter('postId', index) as string;
const limit = this.getNodeParameter('limit', 0, 0) as number;
const qs = {} as IDataObject;
const requestMethod = 'GET';
const endpoint = `posts/${postId}/reactions`;
const body = {} as IDataObject;
let responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
if (limit > 0) {
responseData = responseData.slice(0, limit);
}
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { getAll as execute } from './execute';
import { reactionGetAllDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,49 @@
import * as create from './create';
import * as del from './del';
import * as getAll from './getAll';
import { INodeProperties } from 'n8n-workflow';
export {
create,
del as delete,
getAll,
};
export const descriptions = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'reaction',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Add a reaction to a post.',
},
{
name: 'Delete',
value: 'delete',
description: 'Remove a reaction from a post',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all the reactions to one or more posts',
},
],
default: 'create',
description: 'The operation to perform',
},
...create.description,
...del.description,
...getAll.description,
] as INodeProperties[];

View file

@ -0,0 +1,53 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
INodeExecutionData,
} from 'n8n-workflow';
import * as channel from './channel';
import * as message from './message';
import * as reaction from './reaction';
import * as user from './user';
import { Mattermost } from './Interfaces';
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[]> {
const items = this.getInputData();
const operationResult: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
const resource = this.getNodeParameter<Mattermost>('resource', i);
let operation = this.getNodeParameter('operation', i);
if (operation === 'del') {
operation = 'delete';
} else if (operation === 'desactive') {
operation = 'deactive';
}
const mattermost = {
resource,
operation,
} as Mattermost;
try {
if (mattermost.resource === 'channel') {
operationResult.push(...await channel[mattermost.operation].execute.call(this, i));
} else if (mattermost.resource === 'message') {
operationResult.push(...await message[mattermost.operation].execute.call(this, i));
} else if (mattermost.resource === 'reaction') {
operationResult.push(...await reaction[mattermost.operation].execute.call(this, i));
} else if (mattermost.resource === 'user') {
operationResult.push(...await user[mattermost.operation].execute.call(this, i));
}
} catch (err) {
if (this.continueOnFail()) {
operationResult.push({json: this.getInputData(i)[0].json, error: err});
} else {
throw err;
}
}
}
return operationResult;
}

View file

@ -0,0 +1,270 @@
import {
UserProperties,
} from '../../Interfaces';
export const userCreateDescription: UserProperties = [
{
displayName: 'Username',
name: 'username',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
},
},
default: '',
},
{
displayName: 'Auth Service',
name: 'authService',
type: 'options',
options: [
{
name: 'Email',
value: 'email',
},
{
name: 'Gitlab',
value: 'gitlab',
},
{
name: 'Google',
value: 'google',
},
{
name: 'LDAP',
value: 'ldap',
},
{
name: 'Office365',
value: 'office365',
},
{
name: 'SAML',
value: 'saml',
},
],
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
},
},
default: '',
},
{
displayName: 'Auth Data',
name: 'authData',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
},
hide: {
authService: [
'email',
],
},
},
type: 'string',
default: '',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
authService: [
'email',
],
},
},
},
{
displayName: 'Password',
name: 'password',
type: 'string',
typeOptions: {
password: true,
},
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
authService: [
'email',
],
},
},
default: '',
description: 'The password used for email authentication.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'user',
],
},
},
default: {},
options: [
{
displayName: 'First Name',
name: 'first_name',
type: 'string',
default: '',
},
{
displayName: 'Last Name',
name: 'last_name',
type: 'string',
default: '',
},
{
displayName: 'Locale',
name: 'locale',
type: 'string',
default: '',
},
{
displayName: 'Nickname',
name: 'nickname',
type: 'string',
default: '',
},
{
displayName: 'Notification Settings',
name: 'notificationUi',
type: 'fixedCollection',
placeholder: 'Add Notification Setting',
default: {},
typeOptions: {
multipleValues: false,
},
options: [
{
displayName: 'Notify',
name: 'notificationValues',
values: [
{
displayName: 'Channel',
name: 'channel',
type: 'boolean',
default: true,
description: `Set to "true" to enable channel-wide notifications (@channel, @all, etc.), "false" to disable. Defaults to "true".`,
},
{
displayName: 'Desktop',
name: 'desktop',
type: 'options',
options: [
{
name: 'All',
value: 'all',
description: 'Notifications for all activity',
},
{
name: 'Mention',
value: 'mention',
description: 'Mentions and direct messages only',
},
{
name: 'None',
value: 'none',
description: 'Mentions and direct messages only',
},
],
default: 'all',
},
{
displayName: 'Desktop Sound',
name: 'desktop_sound',
type: 'boolean',
default: true,
description: `Set to "true" to enable sound on desktop notifications, "false" to disable. Defaults to "true".`,
},
{
displayName: 'Email',
name: 'email',
type: 'boolean',
default: false,
description: `Set to "true" to enable email notifications, "false" to disable. Defaults to "true".`,
},
{
displayName: 'First Name',
name: 'first_name',
type: 'boolean',
default: false,
description: `Set to "true" to enable mentions for first name. Defaults to "true" if a first name is set, "false" otherwise.`,
},
{
displayName: 'Mention Keys',
name: 'mention_keys',
type: 'string',
default: '',
description: `A comma-separated list of words to count as mentions. Defaults to username and @username.`,
},
{
displayName: 'Push',
name: 'push',
type: 'options',
options: [
{
name: 'All',
value: 'all',
description: 'Notifications for all activity',
},
{
name: 'Mention',
value: 'mention',
description: 'Mentions and direct messages only',
},
{
name: 'None',
value: 'none',
description: 'Mentions and direct messages only',
},
],
default: 'mention',
},
],
},
],
},
],
},
];

View file

@ -0,0 +1,43 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function create(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const username = this.getNodeParameter('username', index) as string;
const authService = this.getNodeParameter('authService', index) as string;
const additionalFields = this.getNodeParameter('additionalFields', index) as IDataObject;
const qs = {} as IDataObject;
const requestMethod = 'POST';
const endpoint = 'users';
const body = {} as IDataObject;
body.auth_service = authService;
body.username = username;
Object.assign(body, additionalFields);
if (body.notificationUi) {
body.notify_props = (body.notificationUi as IDataObject).notificationValues;
}
if (authService === 'email') {
body.email = this.getNodeParameter('email', index) as string;
body.password = this.getNodeParameter('password', index) as string;
} else {
body.auth_data = this.getNodeParameter('authData', index) as string;
}
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { create as execute } from './execute';
import { userCreateDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,24 @@
import {
UserProperties,
} from '../../Interfaces';
export const userDeactiveDescription: UserProperties = [
{
displayName: 'User ID',
name: 'userId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'deactive',
],
},
},
default: '',
description: 'User GUID',
},
];

View file

@ -0,0 +1,24 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function deactive(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const userId = this.getNodeParameter('userId', index) as string;
const qs = {} as IDataObject;
const requestMethod = 'DELETE';
const endpoint = `users/${userId}`;
const body = {} as IDataObject;
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { deactive as execute } from './execute';
import { userDeactiveDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,119 @@
import {
UserProperties,
} from '../../Interfaces';
export const userGetAllDescription: UserProperties = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
},
},
default: true,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
},
},
default: {},
options: [
{
displayName: 'In Channel',
name: 'inChannel',
type: 'string',
default: '',
description: 'The ID of the channel to get users for.',
},
{
displayName: 'In Team',
name: 'inTeam',
type: 'string',
default: '',
description: 'The ID of the team to get users for.',
},
{
displayName: 'Not In Team',
name: 'notInTeam',
type: 'string',
default: '',
description: 'The ID of the team to exclude users for.',
},
{
displayName: 'Not In Channel',
name: 'notInChannel',
type: 'string',
default: '',
description: 'The ID of the channel to exclude users for.',
},
{
displayName: 'Sort',
name: 'sort',
type: 'options',
options: [
{
name: 'Created At',
value: 'createdAt',
},
{
name: 'Last Activity At',
value: 'lastActivityAt',
},
{
name: 'Status',
value: 'status',
},
{
name: 'username',
value: 'username',
},
],
default: 'username',
description: 'The ID of the channel to exclude users for.',
},
],
},
];

View file

@ -0,0 +1,98 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
NodeOperationError,
} from 'n8n-workflow';
import {
apiRequest,
apiRequestAllItems,
} from '../../../transport';
import {
snakeCase,
} from 'change-case';
export async function getAll(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const returnAll = this.getNodeParameter('returnAll', index) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', index) as IDataObject;
const qs = {} as IDataObject;
const requestMethod = 'GET';
const endpoint = '/users';
const body = {} as IDataObject;
if (additionalFields.inTeam) {
qs.in_team = additionalFields.inTeam;
}
if (additionalFields.notInTeam) {
qs.not_in_team = additionalFields.notInTeam;
}
if (additionalFields.inChannel) {
qs.in_channel = additionalFields.inChannel;
}
if (additionalFields.notInChannel) {
qs.not_in_channel = additionalFields.notInChannel;
}
if (additionalFields.sort) {
qs.sort = snakeCase(additionalFields.sort as string);
}
const validRules = {
inTeam: ['last_activity_at', 'created_at', 'username'],
inChannel: ['status', 'username'],
};
if (additionalFields.sort) {
if (additionalFields.inTeam !== undefined || additionalFields.inChannel !== undefined) {
if (additionalFields.inTeam !== undefined
&& !validRules.inTeam.includes(snakeCase(additionalFields.sort as string))) {
throw new NodeOperationError(this.getNode(), `When In Team is set the only valid values for sorting are ${validRules.inTeam.join(',')}`);
}
if (additionalFields.inChannel !== undefined
&& !validRules.inChannel.includes(snakeCase(additionalFields.sort as string))) {
throw new NodeOperationError(this.getNode(), `When In Channel is set the only valid values for sorting are ${validRules.inChannel.join(',')}`);
}
if (additionalFields.inChannel === ''
&& additionalFields.sort !== 'username') {
throw new NodeOperationError(this.getNode(), 'When sort is different than username In Channel must be set');
}
if (additionalFields.inTeam === ''
&& additionalFields.sort !== 'username') {
throw new NodeOperationError(this.getNode(), 'When sort is different than username In Team must be set');
}
} else {
throw new NodeOperationError(this.getNode(), `When sort is defined either 'in team' or 'in channel' must be defined`);
}
}
if (additionalFields.sort === 'username') {
qs.sort = '';
}
if (returnAll === false) {
qs.per_page = this.getNodeParameter('limit', index) as number;
}
let responseData;
if (returnAll) {
responseData = await apiRequestAllItems.call(this, requestMethod, endpoint, body, qs);
} else {
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
}
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { getAll as execute } from './execute';
import { userGetAllDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,24 @@
import {
UserProperties,
} from '../../Interfaces';
export const userGetByEmailDescription: UserProperties = [
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getByEmail',
],
},
},
default: '',
description: `User's email`,
},
];

View file

@ -0,0 +1,25 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function getByEmail(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const email = this.getNodeParameter('email', index) as string;
const qs = {} as IDataObject;
const requestMethod = 'GET';
const endpoint = `users/email/${email}`;
const body = {} as IDataObject;
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { getByEmail as execute } from './execute';
import { userGetByEmailDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,50 @@
import {
UserProperties,
} from '../../Interfaces';
export const userGetByIdDescription: UserProperties = [
{
displayName: 'User IDs',
name: 'userIds',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getById',
],
},
},
default: '',
description: `User's ID`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getById',
],
},
},
default: {},
options: [
{
displayName: 'Since',
name: 'since',
type: 'dateTime',
default: '',
description: 'Only return users that have been modified since the given Unix timestamp (in milliseconds).',
},
],
},
];

View file

@ -0,0 +1,29 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function getById(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const qs = {} as IDataObject;
const requestMethod = 'POST';
const endpoint = 'users/ids';
const userIds = (this.getNodeParameter('userIds', index) as string).split(',') as string[];
const additionalFields = this.getNodeParameter('additionalFields', index) as IDataObject;
const body = userIds;
if (additionalFields.since) {
qs.since = new Date(additionalFields.since as string).getTime();
}
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { getById as execute } from './execute';
import { userGetByIdDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,74 @@
import * as create from './create';
import * as deactive from './deactive';
import * as getAll from './getAll';
import * as getByEmail from './getByEmail';
import * as getById from './getById';
import * as invite from './invite';
import { INodeProperties } from 'n8n-workflow';
export {
create,
deactive,
getAll,
getByEmail,
getById,
invite,
};
export const descriptions = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'user',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new user',
},
{
name: 'Deactive',
value: 'deactive',
description: 'Deactivates the user and revokes all its sessions by archiving its user object.',
},
{
name: 'Get All',
value: 'getAll',
description: 'Retrieve all users',
},
{
name: 'Get By Email',
value: 'getByEmail',
description: 'Get a user by email',
},
{
name: 'Get By ID',
value: 'getById',
description: 'Get a user by id',
},
{
name: 'Invite',
value: 'invite',
description: 'Invite user to team',
},
],
default: '',
description: 'The operation to perform.',
},
...create.description,
...deactive.description,
...getAll.description,
...getByEmail.description,
...getById.description,
...invite.description,
] as INodeProperties[];

View file

@ -0,0 +1,44 @@
import {
UserProperties,
} from '../../Interfaces';
export const userInviteDescription: UserProperties = [
{
displayName: 'Team ID',
name: 'teamId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeams',
},
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'invite',
],
},
},
default: '',
},
{
displayName: 'Emails',
name: 'emails',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'invite',
],
},
},
default: '',
description: `User's email. Multiple can be set separated by comma.`,
},
];

View file

@ -0,0 +1,28 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
apiRequest,
} from '../../../transport';
export async function invite(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const teamId = this.getNodeParameter('teamId', index) as string;
const emails = (this.getNodeParameter('emails', index) as string).split(',');
const qs = {} as IDataObject;
const requestMethod = 'POST';
const endpoint = `teams/${teamId}/invite/email`;
const body = emails;
const responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
return this.helpers.returnJsonArray(responseData);
}

View file

@ -0,0 +1,7 @@
import { invite as execute } from './execute';
import { userInviteDescription as description } from './description';
export {
description,
execute,
};

View file

@ -0,0 +1,61 @@
import {
INodeProperties,
INodeTypeDescription,
} from 'n8n-workflow';
import * as channel from './channel';
import * as message from './message';
import * as reaction from './reaction';
import * as user from './user';
export const versionDescription: INodeTypeDescription = {
displayName: 'Mattermost',
name: 'mattermost',
icon: 'file:mattermost.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Sends data to Mattermost',
defaults: {
name: 'Mattermost',
color: '#000000',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'mattermostApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Channel',
value: 'channel',
},
{
name: 'Message',
value: 'message',
},
{
name: 'Reaction',
value: 'reaction',
},
{
name: 'User',
value: 'user',
},
],
default: 'message',
description: 'The resource to operate on',
},
...channel.descriptions,
...message.descriptions,
...reaction.descriptions,
...user.descriptions,
],
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -0,0 +1 @@
export * as loadOptions from './loadOptions';

View file

@ -0,0 +1,148 @@
import {
IDataObject,
ILoadOptionsFunctions,
INodePropertyOptions,
NodeOperationError,
} from 'n8n-workflow';
import {
apiRequest,
} from '../transport';
// Get all the available channels
export async function getChannels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const endpoint = 'channels';
const responseData = await apiRequest.call(this, 'GET', endpoint, {});
if (responseData === undefined) {
throw new NodeOperationError(this.getNode(), 'No data got returned');
}
const returnData: INodePropertyOptions[] = [];
let name: string;
for (const data of responseData) {
if (data.delete_at !== 0 || (!data.display_name || !data.name)) {
continue;
}
name = `${data.team_display_name} - ${data.display_name || data.name} (${data.type === 'O' ? 'public' : 'private'})`;
returnData.push({
name,
value: data.id,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
}
// Get all the channels in a team
export async function getChannelsInTeam(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const teamId = this.getCurrentNodeParameter('teamId');
const endpoint = `users/me/teams/${teamId}/channels`;
const responseData = await apiRequest.call(this, 'GET', endpoint, {});
if (responseData === undefined) {
throw new NodeOperationError(this.getNode(), 'No data got returned');
}
const returnData: INodePropertyOptions[] = [];
let name: string;
for (const data of responseData) {
if (data.delete_at !== 0 || (!data.display_name || !data.name)) {
continue;
}
const channelTypes: IDataObject = {
'D': 'direct',
'G': 'group',
'O': 'public',
'P': 'private',
};
name = `${data.display_name} (${channelTypes[data.type as string]})`;
returnData.push({
name,
value: data.id,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
}
export async function getTeams(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const endpoint = 'users/me/teams';
const responseData = await apiRequest.call(this, 'GET', endpoint, {});
if (responseData === undefined) {
throw new NodeOperationError(this.getNode(), 'No data got returned');
}
const returnData: INodePropertyOptions[] = [];
let name: string;
for (const data of responseData) {
if (data.delete_at !== 0) {
continue;
}
name = `${data.display_name} (${data.type === 'O' ? 'public' : 'private'})`;
returnData.push({
name,
value: data.id,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
}
export async function getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const endpoint = 'users';
const responseData = await apiRequest.call(this, 'GET', endpoint, {});
if (responseData === undefined) {
throw new NodeOperationError(this.getNode(), 'No data got returned');
}
const returnData: INodePropertyOptions[] = [];
for (const data of responseData) {
if (data.delete_at !== 0) {
continue;
}
returnData.push({
name: data.username,
value: data.id,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
}

View file

@ -0,0 +1,72 @@
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
GenericValue,
IDataObject,
IHttpRequestOptions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
/**
* Make an API request to Mattermost
*/
export async function apiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD',
endpoint: string,
body: IDataObject | GenericValue | GenericValue[] = {},
query: IDataObject = {},
) {
const credentials = await this.getCredentials('mattermostApi');
if (!credentials) {
throw new NodeOperationError(this.getNode(), 'No credentials returned!');
}
const options: IHttpRequestOptions = {
method,
body,
qs: query,
url: `${credentials.baseUrl}/api/v4/${endpoint}`,
headers: {
authorization: `Bearer ${credentials.accessToken}`,
'content-type': 'application/json; charset=utf-8',
},
};
try {
return await this.helpers.httpRequest(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
export async function apiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD',
endpoint: string,
body: IDataObject = {},
query: IDataObject = {},
) {
const returnData: IDataObject[] = [];
let responseData;
query.page = 0;
query.per_page = 100;
do {
responseData = await apiRequest.call(this, method, endpoint, body, query);
query.page++;
returnData.push.apply(returnData, responseData);
} while (
responseData.length !== 0
);
return returnData;
}

View file

@ -0,0 +1,25 @@
import { INodeType, INodeTypeBaseDescription, INodeVersionedType } from 'n8n-workflow';
export class NodeVersionedType implements INodeVersionedType {
currentVersion: number;
nodeVersions: INodeVersionedType['nodeVersions'];
description: INodeTypeBaseDescription;
constructor(nodeVersions: INodeVersionedType['nodeVersions'], description: INodeTypeBaseDescription) {
this.nodeVersions = nodeVersions;
this.currentVersion = description.defaultVersion ?? this.getLatestVersion();
this.description = description;
}
getLatestVersion() {
return Math.max(...Object.keys(this.nodeVersions).map(Number));
}
getNodeType(version?: number): INodeType {
if (version) {
return this.nodeVersions[version];
} else {
return this.nodeVersions[this.currentVersion];
}
}
}

View file

@ -0,0 +1,3 @@
import { NodeVersionedType } from './NodeVersionedType';
export { NodeVersionedType };

View file

@ -4,6 +4,8 @@
// eslint-disable-next-line import/no-extraneous-dependencies
// eslint-disable-next-line max-classes-per-file
import * as express from 'express';
import * as FormData from 'form-data';
import { URLSearchParams } from 'url';
import { Workflow } from './Workflow';
import { WorkflowHooks } from './WorkflowHooks';
import { WorkflowOperationError } from './WorkflowErrors';
@ -191,6 +193,11 @@ export interface IDataObject {
[key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[];
}
export interface INodeTypeNameVersion {
name: string;
version: number;
}
export interface IGetExecutePollFunctions {
(
workflow: Workflow,
@ -274,6 +281,43 @@ export interface IExecuteContextData {
[key: string]: IContextObject;
}
export interface IHttpRequestOptions {
url: string;
headers?: IDataObject;
method?: 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT';
body?: FormData | GenericValue | GenericValue[] | Buffer | URLSearchParams;
qs?: IDataObject;
arrayFormat?: 'indices' | 'brackets' | 'repeat' | 'comma';
auth?: {
username: string;
password: string;
};
disableFollowRedirect?: boolean;
encoding?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream';
skipSslCertificateValidation?: boolean;
returnFullResponse?: boolean;
proxy?: {
host: string;
port: number;
auth?: {
username: string;
password: string;
};
protocol?: string;
};
timeout?: number;
json?: boolean;
}
export type IN8nHttpResponse = IDataObject | Buffer | GenericValue | GenericValue[];
export interface IN8nHttpFullResponse {
body: IN8nHttpResponse;
headers: IDataObject;
statusCode: number;
statusMessage: string;
}
export interface IExecuteFunctions {
continueOnFail(): boolean;
evaluateExpression(
@ -292,6 +336,11 @@ export interface IExecuteFunctions {
getInputData(inputIndex?: number, inputName?: string): INodeExecutionData[];
getMode(): WorkflowExecuteMode;
getNode(): INode;
getNodeParameter<T extends { resource: string }>(
parameterName: 'resource',
itemIndex?: number,
): T['resource'];
// getNodeParameter(parameterName: 'operation', itemIndex?: number): string;
getNodeParameter(
parameterName: string,
itemIndex: number,
@ -309,7 +358,10 @@ export interface IExecuteFunctions {
putExecutionToWait(waitTill: Date): Promise<void>;
sendMessageToUI(message: any): void; // tslint:disable-line:no-any
helpers: {
[key: string]: (...args: any[]) => any;
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
@ -334,7 +386,10 @@ export interface IExecuteSingleFunctions {
getWorkflowDataProxy(): IWorkflowDataProxyData;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any;
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
@ -369,7 +424,10 @@ export interface ILoadOptionsFunctions {
getTimezone(): string;
getRestApiUrl(): string;
helpers: {
[key: string]: ((...args: any[]) => any) | undefined;
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: ((...args: any[]) => any) | undefined; // tslint:disable-line:no-any
};
}
@ -389,7 +447,10 @@ export interface IHookFunctions {
getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any;
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
@ -408,7 +469,10 @@ export interface IPollFunctions {
getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any;
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
@ -427,7 +491,10 @@ export interface ITriggerFunctions {
getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any;
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
@ -455,7 +522,10 @@ export interface IWebhookFunctions {
outputIndex?: number,
): Promise<INodeExecutionData[][]>;
helpers: {
[key: string]: (...args: any[]) => any;
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
@ -496,12 +566,10 @@ export interface IBinaryKeyData {
}
export interface INodeExecutionData {
[key: string]: IDataObject | IBinaryKeyData | undefined;
// TODO: Rename this one as json does not really fit as it is not json (which is a string) it is actually a JS object
[key: string]: IDataObject | IBinaryKeyData | NodeApiError | NodeOperationError | undefined;
json: IDataObject;
// json: object;
// json?: object;
binary?: IBinaryKeyData;
error?: NodeApiError | NodeOperationError;
}
export interface INodeExecuteFunctions {
@ -557,10 +625,10 @@ export interface INodePropertyTypeOptions {
export interface IDisplayOptions {
hide?: {
[key: string]: NodeParameterValue[];
[key: string]: NodeParameterValue[] | undefined;
};
show?: {
[key: string]: NodeParameterValue[];
[key: string]: NodeParameterValue[] | undefined;
};
}
@ -634,6 +702,14 @@ export interface INodeType {
};
}
export interface INodeVersionedType {
nodeVersions: {
[key: number]: INodeType;
};
currentVersion: number;
description: INodeTypeBaseDescription;
getNodeType: (version?: number) => INodeType;
}
export interface NodeCredentialTestResult {
status: 'OK' | 'Error';
message: string;
@ -684,15 +760,21 @@ export interface IWorfklowIssues {
[key: string]: INodeIssues;
}
export interface INodeTypeDescription {
export interface INodeTypeBaseDescription {
displayName: string;
name: string;
icon?: string;
group: string[];
version: number;
description: string;
defaults: INodeParameters;
documentationUrl?: string;
subtitle?: string;
defaultVersion?: number;
codex?: CodexData;
}
export interface INodeTypeDescription extends INodeTypeBaseDescription {
version: number;
defaults: INodeParameters;
inputs: string[];
inputNames?: string[];
outputs: string[];
@ -701,14 +783,12 @@ export interface INodeTypeDescription {
credentials?: INodeCredentialDescription[];
maxNodes?: number; // How many nodes of that type can be created in a workflow
polling?: boolean;
subtitle?: string;
hooks?: {
[key: string]: INodeHookDescription[] | undefined;
activate?: INodeHookDescription[];
deactivate?: INodeHookDescription[];
};
webhooks?: IWebhookDescription[];
codex?: CodexData;
}
export interface INodeHookDescription {
@ -777,13 +857,14 @@ export type WebhookResponseMode = 'onReceived' | 'lastNode';
export interface INodeTypes {
nodeTypes: INodeTypeData;
init(nodeTypes?: INodeTypeData): Promise<void>;
getAll(): INodeType[];
getByName(nodeType: string): INodeType | undefined;
getAll(): Array<INodeType | INodeVersionedType>;
getByName(nodeType: string): INodeType | INodeVersionedType | undefined;
getByNameAndVersion(nodeType: string, version?: number): INodeType | undefined;
}
export interface INodeTypeData {
[key: string]: {
type: INodeType;
type: INodeType | INodeVersionedType;
sourcePath: string;
};
}
@ -949,3 +1030,19 @@ export type CodexData = {
export type JsonValue = string | number | boolean | null | JsonObject | JsonValue[];
export type JsonObject = { [key: string]: JsonValue };
export type AllEntities<M> = M extends { [key: string]: string } ? Entity<M, keyof M> : never;
export type Entity<M, K> = K extends keyof M ? { resource: K; operation: M[K] } : never;
export type PropertiesOf<M extends { resource: string; operation: string }> = Array<
Omit<INodeProperties, 'displayOptions'> & {
displayOptions?: {
[key in 'show' | 'hide']?: {
resource?: Array<M['resource']>;
operation?: Array<M['operation']>;
[otherKey: string]: NodeParameterValue[] | undefined;
};
};
}
>;

View file

@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-use-before-define */
@ -23,6 +26,7 @@ import {
INodeProperties,
INodePropertyCollection,
INodeType,
INodeVersionedType,
IParameterDependencies,
IRunExecutionData,
IWebhookData,
@ -41,7 +45,7 @@ import { Workflow } from './Workflow';
* @param {INodeType} nodeType
* @returns
*/
export function getSpecialNodeParameters(nodeType: INodeType) {
export function getSpecialNodeParameters(nodeType: INodeType): INodeProperties[] {
if (nodeType.description.polling === true) {
return [
{
@ -296,7 +300,7 @@ export function displayParameter(
if (
values.length === 0 ||
!parameter.displayOptions.show[propertyName].some((v) => values.includes(v))
!parameter.displayOptions.show[propertyName]!.some((v) => values.includes(v))
) {
return false;
}
@ -323,7 +327,7 @@ export function displayParameter(
if (
values.length !== 0 &&
parameter.displayOptions.hide[propertyName].some((v) => values.includes(v))
parameter.displayOptions.hide[propertyName]!.some((v) => values.includes(v))
) {
return false;
}
@ -844,7 +848,7 @@ export function getNodeWebhooks(
return [];
}
const nodeType = workflow.nodeTypes.getByName(node.type) as INodeType;
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion) as INodeType;
if (nodeType.description.webhooks === undefined) {
// Node does not have any webhooks so return
@ -940,7 +944,7 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
return [];
}
const nodeType = workflow.nodeTypes.getByName(node.type) as INodeType;
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion) as INodeType;
if (nodeType.description.webhooks === undefined) {
// Node does not have any webhooks so return
@ -1385,3 +1389,27 @@ export function mergeNodeProperties(
}
}
}
export function getVersionedTypeNode(
object: INodeVersionedType | INodeType,
version?: number,
): INodeType {
if (isNodeTypeVersioned(object)) {
return (object as INodeVersionedType).getNodeType(version);
}
return object as INodeType;
}
export function getVersionedTypeNodeAll(object: INodeVersionedType | INodeType): INodeType[] {
if (isNodeTypeVersioned(object)) {
return Object.values((object as INodeVersionedType).nodeVersions).map((element) => {
element.description.name = object.description.name;
return element;
});
}
return [object as INodeType];
}
export function isNodeTypeVersioned(object: INodeVersionedType | INodeType): boolean {
return !!('getNodeType' in object);
}

View file

@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
@ -85,7 +88,8 @@ export class Workflow {
let nodeType: INodeType | undefined;
for (const node of parameters.nodes) {
this.nodes[node.name] = node;
nodeType = this.nodeTypes.getByName(node.type);
nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
// Go on to next node when its type is not known.
@ -197,7 +201,7 @@ export class Workflow {
continue;
}
nodeType = this.nodeTypes.getByName(node.type);
nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
// Type is not known so check is not possible
@ -241,7 +245,7 @@ export class Workflow {
continue;
}
nodeType = this.nodeTypes.getByName(node.type);
nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
// Node type is not known
@ -342,7 +346,7 @@ export class Workflow {
continue;
}
nodeType = this.nodeTypes.getByName(node.type);
nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType !== undefined && checkFunction(nodeType)) {
returnNodes.push(node);
@ -712,7 +716,7 @@ export class Workflow {
if (node === null) {
return undefined;
}
const nodeType = this.nodeTypes.getByName(node.type) as INodeType;
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion) as INodeType;
if (nodeType.description.outputs.length === 1) {
// If the parent node has only one output, it can only be connected
// to that one. So no further checking is required.
@ -787,7 +791,8 @@ export class Workflow {
let nodeType: INodeType;
for (const nodeName of nodeNames) {
node = this.nodes[nodeName];
nodeType = this.nodeTypes.getByName(node.type) as INodeType;
nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion) as INodeType;
if (nodeType.trigger !== undefined || nodeType.poll !== undefined) {
if (node.disabled === true) {
@ -860,7 +865,7 @@ export class Workflow {
isTest?: boolean,
): Promise<boolean | undefined> {
const node = this.getNode(webhookData.node) as INode;
const nodeType = this.nodeTypes.getByName(node.type) as INodeType;
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion) as INodeType;
if (nodeType.webhookMethods === undefined) {
return;
@ -907,7 +912,7 @@ export class Workflow {
): Promise<ITriggerResponse | undefined> {
const triggerFunctions = getTriggerFunctions(this, node, additionalData, mode, activation);
const nodeType = this.nodeTypes.getByName(node.type);
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
throw new Error(`The node type "${node.type}" of node "${node.name}" is not known.`);
@ -947,11 +952,12 @@ export class Workflow {
* @returns
* @memberof Workflow
*/
async runPoll(
node: INode,
pollFunctions: IPollFunctions,
): Promise<INodeExecutionData[][] | null> {
const nodeType = this.nodeTypes.getByName(node.type);
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
throw new Error(`The node type "${node.type}" of node "${node.name}" is not known.`);
@ -984,7 +990,7 @@ export class Workflow {
nodeExecuteFunctions: INodeExecuteFunctions,
mode: WorkflowExecuteMode,
): Promise<IWebhookResponseData> {
const nodeType = this.nodeTypes.getByName(node.type);
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
throw new Error(`The type of the webhook node "${node.name}" is not known.`);
} else if (nodeType.webhook === undefined) {
@ -1036,7 +1042,7 @@ export class Workflow {
return undefined;
}
const nodeType = this.nodeTypes.getByName(node.type);
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
throw new Error(`Node type "${node.type}" is not known so can not run it!`);
}

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