2021-08-29 11:58:11 -07:00
|
|
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
|
|
/* eslint-disable no-underscore-dangle */
|
|
|
|
/* eslint-disable no-continue */
|
|
|
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
|
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
|
|
/* eslint-disable no-restricted-syntax */
|
|
|
|
/* eslint-disable no-param-reassign */
|
|
|
|
import {
|
|
|
|
IDataObject,
|
|
|
|
IExecuteData,
|
|
|
|
INode,
|
|
|
|
IRun,
|
|
|
|
IRunExecutionData,
|
|
|
|
ITaskData,
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
IWorkflowCredentials,
|
|
|
|
LoggerProxy as Logger,
|
|
|
|
Workflow,
|
|
|
|
} from 'n8n-workflow';
|
|
|
|
import { validate } from 'class-validator';
|
|
|
|
// eslint-disable-next-line import/no-cycle
|
2019-06-23 03:35:23 -07:00
|
|
|
import {
|
2020-05-16 10:05:40 -07:00
|
|
|
CredentialTypes,
|
2019-06-23 03:35:23 -07:00
|
|
|
Db,
|
2020-05-16 10:05:40 -07:00
|
|
|
ICredentialsTypeData,
|
2019-12-19 14:07:55 -08:00
|
|
|
ITransferNodeTypes,
|
2019-06-23 03:35:23 -07:00
|
|
|
IWorkflowErrorData,
|
2020-10-22 06:46:03 -07:00
|
|
|
IWorkflowExecutionDataProcess,
|
2019-06-23 03:35:23 -07:00
|
|
|
NodeTypes,
|
2021-05-29 11:31:21 -07:00
|
|
|
ResponseHelper,
|
2021-08-29 11:58:11 -07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
2019-08-08 11:38:25 -07:00
|
|
|
WorkflowCredentials,
|
|
|
|
WorkflowRunner,
|
2021-08-29 11:58:11 -07:00
|
|
|
} from '.';
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2021-06-23 02:20:07 -07:00
|
|
|
import * as config from '../config';
|
2021-08-29 11:58:11 -07:00
|
|
|
// eslint-disable-next-line import/no-cycle
|
2021-05-29 11:31:21 -07:00
|
|
|
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
2019-06-23 03:35:23 -07:00
|
|
|
|
|
|
|
const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
|
|
|
|
|
2019-12-19 14:07:55 -08:00
|
|
|
/**
|
|
|
|
* Returns the data of the last executed node
|
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @param {IRun} inputData
|
|
|
|
* @returns {(ITaskData | undefined)}
|
|
|
|
*/
|
|
|
|
export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefined {
|
2021-08-29 11:58:11 -07:00
|
|
|
const { runData } = inputData.data.resultData;
|
|
|
|
const { lastNodeExecuted } = inputData.data.resultData;
|
2019-12-19 14:07:55 -08:00
|
|
|
|
|
|
|
if (lastNodeExecuted === undefined) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (runData[lastNodeExecuted] === undefined) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return runData[lastNodeExecuted][runData[lastNodeExecuted].length - 1];
|
|
|
|
}
|
|
|
|
|
2019-06-23 03:35:23 -07:00
|
|
|
/**
|
|
|
|
* Returns if the given id is a valid workflow id
|
|
|
|
*
|
|
|
|
* @param {(string | null | undefined)} id The id to check
|
|
|
|
* @returns {boolean}
|
|
|
|
* @memberof App
|
|
|
|
*/
|
2021-08-29 11:58:11 -07:00
|
|
|
export function isWorkflowIdValid(id: string | null | undefined | number): boolean {
|
2019-06-23 03:35:23 -07:00
|
|
|
if (typeof id === 'string') {
|
|
|
|
id = parseInt(id, 10);
|
|
|
|
}
|
|
|
|
|
2021-08-29 11:58:11 -07:00
|
|
|
// eslint-disable-next-line no-restricted-globals
|
2019-06-23 03:35:23 -07:00
|
|
|
if (isNaN(id as number)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Executes the error workflow
|
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @param {string} workflowId The id of the error workflow
|
|
|
|
* @param {IWorkflowErrorData} workflowErrorData The error data
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
*/
|
2021-08-29 11:58:11 -07:00
|
|
|
export async function executeErrorWorkflow(
|
|
|
|
workflowId: string,
|
|
|
|
workflowErrorData: IWorkflowErrorData,
|
|
|
|
): Promise<void> {
|
2019-06-23 03:35:23 -07:00
|
|
|
// Wrap everything in try/catch to make sure that no errors bubble up and all get caught here
|
|
|
|
try {
|
2021-05-29 11:31:21 -07:00
|
|
|
const workflowData = await Db.collections.Workflow!.findOne({ id: Number(workflowId) });
|
2019-06-23 03:35:23 -07:00
|
|
|
|
|
|
|
if (workflowData === undefined) {
|
|
|
|
// The error workflow could not be found
|
2021-08-29 11:58:11 -07:00
|
|
|
Logger.error(
|
|
|
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
|
|
`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find error workflow "${workflowId}"`,
|
|
|
|
{ workflowId },
|
|
|
|
);
|
2019-06-23 03:35:23 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const executionMode = 'error';
|
|
|
|
const nodeTypes = NodeTypes();
|
|
|
|
|
2021-08-29 11:58:11 -07:00
|
|
|
const workflowInstance = new Workflow({
|
|
|
|
id: workflowId,
|
|
|
|
name: workflowData.name,
|
|
|
|
nodeTypes,
|
|
|
|
nodes: workflowData.nodes,
|
|
|
|
connections: workflowData.connections,
|
|
|
|
active: workflowData.active,
|
|
|
|
staticData: workflowData.staticData,
|
|
|
|
settings: workflowData.settings,
|
|
|
|
});
|
2019-06-23 03:35:23 -07:00
|
|
|
|
|
|
|
let node: INode;
|
|
|
|
let workflowStartNode: INode | undefined;
|
|
|
|
for (const nodeName of Object.keys(workflowInstance.nodes)) {
|
|
|
|
node = workflowInstance.nodes[nodeName];
|
|
|
|
if (node.type === ERROR_TRIGGER_TYPE) {
|
|
|
|
workflowStartNode = node;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (workflowStartNode === undefined) {
|
2021-08-29 11:58:11 -07:00
|
|
|
Logger.error(
|
|
|
|
`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`,
|
|
|
|
);
|
2019-06-23 03:35:23 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Can execute without webhook so go on
|
|
|
|
|
|
|
|
// Initialize the data of the webhook node
|
|
|
|
const nodeExecutionStack: IExecuteData[] = [];
|
2021-08-29 11:58:11 -07:00
|
|
|
nodeExecutionStack.push({
|
|
|
|
node: workflowStartNode,
|
|
|
|
data: {
|
|
|
|
main: [
|
|
|
|
[
|
|
|
|
{
|
|
|
|
json: workflowErrorData,
|
|
|
|
},
|
2019-06-23 03:35:23 -07:00
|
|
|
],
|
2021-08-29 11:58:11 -07:00
|
|
|
],
|
|
|
|
},
|
|
|
|
});
|
2019-06-23 03:35:23 -07:00
|
|
|
|
|
|
|
const runExecutionData: IRunExecutionData = {
|
2021-08-29 11:58:11 -07:00
|
|
|
startData: {},
|
2019-06-23 03:35:23 -07:00
|
|
|
resultData: {
|
|
|
|
runData: {},
|
|
|
|
},
|
|
|
|
executionData: {
|
|
|
|
contextData: {},
|
|
|
|
nodeExecutionStack,
|
|
|
|
waitingExecution: {},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2019-08-08 11:38:25 -07:00
|
|
|
const runData: IWorkflowExecutionDataProcess = {
|
|
|
|
executionMode,
|
|
|
|
executionData: runExecutionData,
|
|
|
|
workflowData,
|
|
|
|
};
|
|
|
|
|
|
|
|
const workflowRunner = new WorkflowRunner();
|
|
|
|
await workflowRunner.run(runData);
|
2019-06-23 03:35:23 -07:00
|
|
|
} catch (error) {
|
2021-08-29 11:58:11 -07:00
|
|
|
Logger.error(
|
|
|
|
`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`,
|
|
|
|
{ workflowId: workflowErrorData.workflow.id },
|
|
|
|
);
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-19 14:07:55 -08:00
|
|
|
/**
|
|
|
|
* Returns all the defined NodeTypes
|
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @returns {ITransferNodeTypes}
|
|
|
|
*/
|
|
|
|
export function getAllNodeTypeData(): ITransferNodeTypes {
|
|
|
|
const nodeTypes = NodeTypes();
|
|
|
|
|
|
|
|
// Get the data of all thenode types that they
|
|
|
|
// can be loaded again in the process
|
|
|
|
const returnData: ITransferNodeTypes = {};
|
|
|
|
for (const nodeTypeName of Object.keys(nodeTypes.nodeTypes)) {
|
|
|
|
if (nodeTypes.nodeTypes[nodeTypeName] === undefined) {
|
|
|
|
throw new Error(`The NodeType "${nodeTypeName}" could not be found!`);
|
|
|
|
}
|
|
|
|
|
|
|
|
returnData[nodeTypeName] = {
|
|
|
|
className: nodeTypes.nodeTypes[nodeTypeName].type.constructor.name,
|
|
|
|
sourcePath: nodeTypes.nodeTypes[nodeTypeName].sourcePath,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return returnData;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the data of the node types that are needed
|
|
|
|
* to execute the given nodes
|
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @param {INode[]} nodes
|
|
|
|
* @returns {ITransferNodeTypes}
|
|
|
|
*/
|
|
|
|
export function getNodeTypeData(nodes: INode[]): ITransferNodeTypes {
|
|
|
|
const nodeTypes = NodeTypes();
|
|
|
|
|
|
|
|
// Check which node-types have to be loaded
|
2021-08-29 11:58:11 -07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
2019-12-19 14:07:55 -08:00
|
|
|
const neededNodeTypes = getNeededNodeTypes(nodes);
|
|
|
|
|
|
|
|
// Get all the data of the needed node types that they
|
|
|
|
// 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!`);
|
|
|
|
}
|
|
|
|
|
|
|
|
returnData[nodeTypeName] = {
|
|
|
|
className: nodeTypes.nodeTypes[nodeTypeName].type.constructor.name,
|
|
|
|
sourcePath: nodeTypes.nodeTypes[nodeTypeName].sourcePath,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return returnData;
|
|
|
|
}
|
|
|
|
|
2020-05-16 10:05:40 -07:00
|
|
|
/**
|
|
|
|
* Returns the credentials data of the given type and its parent types
|
|
|
|
* it extends
|
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @param {string} type The credential type to return data off
|
|
|
|
* @returns {ICredentialsTypeData}
|
|
|
|
*/
|
|
|
|
export function getCredentialsDataWithParents(type: string): ICredentialsTypeData {
|
|
|
|
const credentialTypes = CredentialTypes();
|
|
|
|
const credentialType = credentialTypes.getByName(type);
|
|
|
|
|
|
|
|
const credentialTypeData: ICredentialsTypeData = {};
|
|
|
|
credentialTypeData[type] = credentialType;
|
|
|
|
|
|
|
|
if (credentialType === undefined || credentialType.extends === undefined) {
|
|
|
|
return credentialTypeData;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const typeName of credentialType.extends) {
|
|
|
|
if (credentialTypeData[typeName] !== undefined) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
credentialTypeData[typeName] = credentialTypes.getByName(typeName);
|
|
|
|
Object.assign(credentialTypeData, getCredentialsDataWithParents(typeName));
|
|
|
|
}
|
|
|
|
|
|
|
|
return credentialTypeData;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns all the credentialTypes which are needed to resolve
|
|
|
|
* the given workflow credentials
|
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @param {IWorkflowCredentials} credentials The credentials which have to be able to be resolved
|
|
|
|
* @returns {ICredentialsTypeData}
|
|
|
|
*/
|
2021-08-26 04:45:15 -07:00
|
|
|
export function getCredentialsDataByNodes(nodes: INode[]): ICredentialsTypeData {
|
2020-05-16 10:05:40 -07:00
|
|
|
const credentialTypeData: ICredentialsTypeData = {};
|
|
|
|
|
2021-08-26 04:45:15 -07:00
|
|
|
for (const node of nodes) {
|
|
|
|
const credentialsUsedByThisNode = node.credentials;
|
|
|
|
if (credentialsUsedByThisNode) {
|
|
|
|
// const credentialTypesUsedByThisNode = Object.keys(credentialsUsedByThisNode!);
|
2021-08-29 11:58:11 -07:00
|
|
|
for (const credentialType of Object.keys(credentialsUsedByThisNode)) {
|
2021-08-26 04:45:15 -07:00
|
|
|
if (credentialTypeData[credentialType] !== undefined) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.assign(credentialTypeData, getCredentialsDataWithParents(credentialType));
|
|
|
|
}
|
2020-05-16 10:05:40 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return credentialTypeData;
|
|
|
|
}
|
|
|
|
|
2019-12-19 14:07:55 -08:00
|
|
|
/**
|
|
|
|
* Returns the names of the NodeTypes which are are needed
|
|
|
|
* to execute the gives nodes
|
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @param {INode[]} nodes
|
|
|
|
* @returns {string[]}
|
|
|
|
*/
|
|
|
|
export function getNeededNodeTypes(nodes: INode[]): string[] {
|
|
|
|
// Check which node-types have to be loaded
|
|
|
|
const neededNodeTypes: string[] = [];
|
|
|
|
for (const node of nodes) {
|
|
|
|
if (!neededNodeTypes.includes(node.type)) {
|
|
|
|
neededNodeTypes.push(node.type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return neededNodeTypes;
|
|
|
|
}
|
|
|
|
|
2019-06-23 03:35:23 -07:00
|
|
|
/**
|
|
|
|
* Saves the static data if it changed
|
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @param {Workflow} workflow
|
|
|
|
* @returns {Promise <void>}
|
|
|
|
*/
|
2021-08-29 11:58:11 -07:00
|
|
|
export async function saveStaticData(workflow: Workflow): Promise<void> {
|
2019-06-23 03:35:23 -07:00
|
|
|
if (workflow.staticData.__dataChanged === true) {
|
|
|
|
// Static data of workflow changed and so has to be saved
|
2021-08-29 11:58:11 -07:00
|
|
|
if (isWorkflowIdValid(workflow.id)) {
|
2019-06-23 03:35:23 -07:00
|
|
|
// Workflow is saved so update in database
|
|
|
|
try {
|
2021-08-29 11:58:11 -07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
2019-08-08 11:38:25 -07:00
|
|
|
await saveStaticDataById(workflow.id!, workflow.staticData);
|
2019-06-23 03:35:23 -07:00
|
|
|
workflow.staticData.__dataChanged = false;
|
|
|
|
} catch (e) {
|
2021-08-29 11:58:11 -07:00
|
|
|
Logger.error(
|
|
|
|
`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${e.message}"`,
|
|
|
|
{ workflowId: workflow.id },
|
|
|
|
);
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-08-08 11:38:25 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Saves the given static data on workflow
|
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @param {(string | number)} workflowId The id of the workflow to save data on
|
|
|
|
* @param {IDataObject} newStaticData The static data to save
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
*/
|
2021-08-29 11:58:11 -07:00
|
|
|
export async function saveStaticDataById(
|
|
|
|
workflowId: string | number,
|
|
|
|
newStaticData: IDataObject,
|
|
|
|
): Promise<void> {
|
|
|
|
await Db.collections.Workflow!.update(workflowId, {
|
|
|
|
staticData: newStaticData,
|
|
|
|
});
|
2019-08-08 11:38:25 -07:00
|
|
|
}
|
2019-10-14 22:36:53 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the static data of workflow
|
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @param {(string | number)} workflowId The id of the workflow to get static data of
|
|
|
|
* @returns
|
|
|
|
*/
|
2021-08-29 11:58:11 -07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
2019-10-14 22:36:53 -07:00
|
|
|
export async function getStaticDataById(workflowId: string | number) {
|
2021-08-29 11:58:11 -07:00
|
|
|
const workflowData = await Db.collections.Workflow!.findOne(workflowId, {
|
|
|
|
select: ['staticData'],
|
|
|
|
});
|
2019-10-14 22:36:53 -07:00
|
|
|
|
|
|
|
if (workflowData === undefined) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2021-08-29 11:58:11 -07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
2019-10-14 22:36:53 -07:00
|
|
|
return workflowData.staticData || {};
|
|
|
|
}
|
2021-05-29 11:31:21 -07:00
|
|
|
|
|
|
|
// TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers?
|
|
|
|
|
2021-08-29 11:58:11 -07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
2021-05-29 11:31:21 -07:00
|
|
|
export async function validateWorkflow(newWorkflow: WorkflowEntity) {
|
|
|
|
const errors = await validate(newWorkflow);
|
|
|
|
|
|
|
|
if (errors.length) {
|
|
|
|
const validationErrorMessage = Object.values(errors[0].constraints!)[0];
|
|
|
|
throw new ResponseHelper.ResponseError(validationErrorMessage, undefined, 400);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-29 11:58:11 -07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
2021-05-29 11:31:21 -07:00
|
|
|
export function throwDuplicateEntryError(error: Error) {
|
|
|
|
const errorMessage = error.message.toLowerCase();
|
|
|
|
if (errorMessage.includes('unique') || errorMessage.includes('duplicate')) {
|
2021-08-29 11:58:11 -07:00
|
|
|
throw new ResponseHelper.ResponseError(
|
|
|
|
'There is already a workflow with this name',
|
|
|
|
undefined,
|
|
|
|
400,
|
|
|
|
);
|
2021-05-29 11:31:21 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
throw new ResponseHelper.ResponseError(errorMessage, undefined, 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
export type WorkflowNameRequest = Express.Request & {
|
|
|
|
query: {
|
|
|
|
name?: string;
|
|
|
|
offset?: string;
|
2021-08-29 11:58:11 -07:00
|
|
|
};
|
2021-05-29 11:31:21 -07:00
|
|
|
};
|