mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
✨ 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:
parent
53fbf664b5
commit
443c2a4d51
|
@ -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
|
||||
|
|
|
@ -30,6 +30,9 @@ const mockNodeTypes: INodeTypes = {
|
|||
getByName: (nodeType: string): INodeType | undefined => {
|
||||
return undefined;
|
||||
},
|
||||
getByNameAndVersion: (): INodeType | undefined => {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
|
||||
export class CredentialsHelper extends ICredentialsHelper {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
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;
|
||||
const returnData: INodeTypeDescription[] = [];
|
||||
nodeInfos.forEach((nodeInfo) => {
|
||||
const nodeType = nodeTypes.getByNameAndVersion(nodeInfo.name, nodeInfo.version);
|
||||
if (nodeType?.description) {
|
||||
returnData.push(nodeType.description);
|
||||
}
|
||||
})
|
||||
.filter((nodeData) => !!nodeData)
|
||||
.map((nodeData) => nodeData!.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) => {
|
||||
|
||||
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.methods!.credentialTest![credential.testedBy!];
|
||||
foundTestFunction = (node as unknown as INodeType).methods!.credentialTest![
|
||||
credential.testedBy!
|
||||
];
|
||||
}
|
||||
return testFunctionSearch;
|
||||
});
|
||||
if (credentialTestable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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), {});
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}"`);
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
{
|
||||
"node": "n8n-nodes-base.httpRequest",
|
||||
"nodeVersion": "1.0",
|
||||
|
|
|
@ -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
34
packages/nodes-base/nodes/Mattermost/v1/MattermostV1.node.ts
Normal file
34
packages/nodes-base/nodes/Mattermost/v1/MattermostV1.node.ts
Normal 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)];
|
||||
}
|
||||
}
|
|
@ -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[];
|
||||
};
|
||||
}
|
|
@ -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.',
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { addUser as execute } from './execute';
|
||||
import { channelAddUserDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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.',
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { create as execute } from './execute';
|
||||
import { channelCreateDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { del as execute } from './execute';
|
||||
import { channelDeleteDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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[];
|
|
@ -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.',
|
||||
},
|
||||
];
|
||||
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { members as execute } from './execute';
|
||||
import { channelMembersDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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.',
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { restore as execute } from './execute';
|
||||
import { channelRestoreDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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.',
|
||||
},
|
||||
];
|
||||
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { statistics as execute } from './execute';
|
||||
import { channelStatisticsDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { del as execute } from './execute';
|
||||
import { messageDeleteDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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[];
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { post as execute } from './execute';
|
||||
import { messagePostDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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.',
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { postEphemeral as execute } from './execute';
|
||||
import { messagePostEphemeralDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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.',
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { create as execute } from './execute';
|
||||
import { reactionCreateDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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.',
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { del as execute } from './execute';
|
||||
import { reactionDeleteDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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.',
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { getAll as execute } from './execute';
|
||||
import { reactionGetAllDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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[];
|
53
packages/nodes-base/nodes/Mattermost/v1/actions/router.ts
Normal file
53
packages/nodes-base/nodes/Mattermost/v1/actions/router.ts
Normal 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;
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { create as execute } from './execute';
|
||||
import { userCreateDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { deactive as execute } from './execute';
|
||||
import { userDeactiveDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { getAll as execute } from './execute';
|
||||
import { userGetAllDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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`,
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { getByEmail as execute } from './execute';
|
||||
import { userGetByEmailDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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).',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { getById as execute } from './execute';
|
||||
import { userGetByIdDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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[];
|
||||
|
|
@ -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.`,
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { invite as execute } from './execute';
|
||||
import { userInviteDescription as description } from './description';
|
||||
|
||||
export {
|
||||
description,
|
||||
execute,
|
||||
};
|
|
@ -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,
|
||||
],
|
||||
};
|
1
packages/nodes-base/nodes/Mattermost/v1/mattermost.svg
Normal file
1
packages/nodes-base/nodes/Mattermost/v1/mattermost.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.6 KiB |
1
packages/nodes-base/nodes/Mattermost/v1/methods/index.ts
Normal file
1
packages/nodes-base/nodes/Mattermost/v1/methods/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * as loadOptions from './loadOptions';
|
148
packages/nodes-base/nodes/Mattermost/v1/methods/loadOptions.ts
Normal file
148
packages/nodes-base/nodes/Mattermost/v1/methods/loadOptions.ts
Normal 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;
|
||||
}
|
||||
|
72
packages/nodes-base/nodes/Mattermost/v1/transport/index.ts
Normal file
72
packages/nodes-base/nodes/Mattermost/v1/transport/index.ts
Normal 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;
|
||||
}
|
25
packages/nodes-base/src/NodeVersionedType.ts
Normal file
25
packages/nodes-base/src/NodeVersionedType.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import { NodeVersionedType } from './NodeVersionedType';
|
||||
|
||||
export { NodeVersionedType };
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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!`);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { INodeType, INodeTypeData, INodeTypes } from '../src';
|
||||
import {
|
||||
INodeType,
|
||||
INodeTypeData,
|
||||
INodeTypes,
|
||||
NodeHelpers,
|
||||
} from '../src';
|
||||
|
||||
export interface INodeTypesObject {
|
||||
[key: string]: INodeType;
|
||||
|
@ -94,11 +99,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue