Skip to content

Commit

Permalink
✨ Updated node design and node versioning (n8n-io#1961)
Browse files Browse the repository at this point in the history
* ⚡ 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 <[email protected]>
Co-authored-by: Iván Ovejero <[email protected]>
  • Loading branch information
3 people authored Sep 21, 2021
1 parent 53fbf66 commit 443c2a4
Show file tree
Hide file tree
Showing 101 changed files with 4,016 additions and 2,643 deletions.
1 change: 1 addition & 0 deletions packages/cli/commands/executeBatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/CredentialsHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const mockNodeTypes: INodeTypes = {
getByName: (nodeType: string): INodeType | undefined => {
return undefined;
},
getByNameAndVersion: (): INodeType | undefined => {
return undefined;
},
};

export class CredentialsHelper extends ICredentialsHelper {
Expand Down
41 changes: 37 additions & 4 deletions packages/cli/src/LoadNodesAndCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ILogger,
INodeType,
INodeTypeData,
INodeVersionedType,
LoggerProxy,
} from 'n8n-workflow';

Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);

Expand Down
29 changes: 20 additions & 9 deletions packages/cli/src/NodeTypes.ts
Original file line number Diff line number Diff line change
@@ -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 = {};
Expand All @@ -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;
Expand Down
111 changes: 79 additions & 32 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand All @@ -1075,10 +1082,10 @@ class App {

// @ts-ignore
const loadDataInstance = new LoadNodeParameterOptions(
nodeType,
nodeTypeAndVersion,
nodeTypes,
path,
JSON.parse(`${req.query.currentNodeParameters}`),
currentNodeParameters,
credentials,
);

Expand All @@ -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;
},
),
);
Expand All @@ -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.');
Expand Down Expand Up @@ -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;
});

Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/WebhookHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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), {});
Expand Down
Loading

0 comments on commit 443c2a4

Please sign in to comment.