✨ 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:
@@ -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;
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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}"`);
|
||||
|
||||
Reference in New Issue
Block a user