Updated node design and node versioning (#1961)

*  introduce versioned nodes

* Export versioned nodes for separate process run

* Add bse node for versioned nodes

* fix node name for versioned nodes

* extend node from nodeVersionedType

* improve nodes base and flow to FE

* revert lib es2019 to es2017

* include version in key to prevent duplicate key

* handle type versions on FE

* clean up

* cleanup nodes base

* add type versions in getNodeParameterOptions

* cleanup

* code review

* code review + add default version to node type description

* remove node default types from store

* 💄 cleanups

* Draft for migrated Mattermost node

* First version of Mattermost node versioned according to node standards

* Correcting deactivate operations name to match currently used one

*  Create utility types

*  Simplify Mattermost types

*  Rename exports for consistency

*  Type channel properties

*  Type message properties

*  Type reaction properties

*  Type user properties

*  Add type import to router

* 🐛 Add missing key

* 🔨 Adjust typo in operation name

* 🔨 Inline exports for channel properties

* 🔨 Inline exports for message properties

* 🔨 Inline exports for reaction properties

* 🔨 Inline exports for user properties

* 🔨 Inline exports for load options

* 👕 Fix lint issue

* 🔨 Inline export for description

* 🔨 Rename descriptions for clarity

* 🔨 Refactor imports/exports for methods

* 🔨 Refactor latest version retrieval

* 🔥 Remove unneeded else clause

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

*  Add overloads to getNodeParameter

*  Improve overload

* 🔥 Remove superfluous INodeVersions type

* 🔨 Relocate pre-existing interface

* 🔥 Remove JSDoc arg descriptions

*  Minor reformatting in transport file

*  Fix API call function type

* Created first draft for Axios requests

* Working version of mattermost node with Axios

* Work in progress for replacing request library

* Improvements to request translations

* Fixed sending files via multipart / form-data

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

* Improved typing for new http helper

* Added ignore any for specific lines for linting

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

* Adding default headers according to body on httpRequest helper

* Spec error handling and fixed workflows with older node versions

* Showcase how to export errors in a standard format

* Merging master

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

* Reverting changes to http request node

* Changed nullish comparison and removed repeated code from nodes

* Renamed queryString back to qs and simplified node output

* Simplified some comparisons

* Changed header names to be uc first

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

* Fixed indentation, remove unnecessary file and console log

* Fixed mattermost node name

* Fixed lint issues

* Further fix linting issues

* Further fix lint issues

* Fixed http request helper's return type

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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