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,
|
2019-08-08 11:38:25 -07:00
|
|
|
WorkflowCredentials,
|
|
|
|
WorkflowRunner,
|
2019-06-23 03:35:23 -07:00
|
|
|
} from './';
|
|
|
|
|
|
|
|
import {
|
2019-08-08 11:38:25 -07:00
|
|
|
IDataObject,
|
2019-06-23 03:35:23 -07:00
|
|
|
IExecuteData,
|
|
|
|
INode,
|
2019-12-19 14:07:55 -08:00
|
|
|
IRun,
|
2019-06-23 03:35:23 -07:00
|
|
|
IRunExecutionData,
|
2019-12-19 14:07:55 -08:00
|
|
|
ITaskData,
|
2020-05-16 10:05:40 -07:00
|
|
|
IWorkflowCredentials,
|
2021-05-01 20:43:01 -07:00
|
|
|
LoggerProxy as Logger,
|
|
|
|
Workflow,} from 'n8n-workflow';
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2021-06-22 14:18:52 -07:00
|
|
|
import { config } from '@config';
|
2021-05-29 11:31:21 -07:00
|
|
|
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
|
|
|
import { validate } from 'class-validator';
|
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 {
|
|
|
|
const runData = inputData.data.resultData.runData;
|
|
|
|
const lastNodeExecuted = inputData.data.resultData.lastNodeExecuted;
|
|
|
|
|
|
|
|
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
|
|
|
|
*/
|
|
|
|
export function isWorkflowIdValid (id: string | null | undefined | number): boolean {
|
|
|
|
if (typeof id === 'string') {
|
|
|
|
id = parseInt(id, 10);
|
|
|
|
}
|
|
|
|
|
|
|
|
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>}
|
|
|
|
*/
|
|
|
|
export async function executeErrorWorkflow(workflowId: string, workflowErrorData: IWorkflowErrorData): Promise<void> {
|
|
|
|
// 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-05-01 20:43:01 -07:00
|
|
|
Logger.error(`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();
|
|
|
|
|
2020-02-15 17:07:01 -08: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-05-01 20:43:01 -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[] = [];
|
|
|
|
nodeExecutionStack.push(
|
|
|
|
{
|
|
|
|
node: workflowStartNode,
|
|
|
|
data: {
|
|
|
|
main: [
|
|
|
|
[
|
|
|
|
{
|
2020-10-22 06:46:03 -07:00
|
|
|
json: workflowErrorData,
|
|
|
|
},
|
|
|
|
],
|
2019-06-23 03:35:23 -07:00
|
|
|
],
|
|
|
|
},
|
2020-10-22 06:46:03 -07:00
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
const runExecutionData: IRunExecutionData = {
|
|
|
|
startData: {
|
|
|
|
},
|
|
|
|
resultData: {
|
|
|
|
runData: {},
|
|
|
|
},
|
|
|
|
executionData: {
|
|
|
|
contextData: {},
|
|
|
|
nodeExecutionStack,
|
|
|
|
waitingExecution: {},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2019-08-08 11:38:25 -07:00
|
|
|
const credentials = await WorkflowCredentials(workflowData.nodes);
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2019-08-08 11:38:25 -07:00
|
|
|
const runData: IWorkflowExecutionDataProcess = {
|
|
|
|
credentials,
|
|
|
|
executionMode,
|
|
|
|
executionData: runExecutionData,
|
|
|
|
workflowData,
|
|
|
|
};
|
|
|
|
|
|
|
|
const workflowRunner = new WorkflowRunner();
|
|
|
|
await workflowRunner.run(runData);
|
2019-06-23 03:35:23 -07:00
|
|
|
} catch (error) {
|
2021-05-01 20:43:01 -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
|
|
|
|
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}
|
|
|
|
*/
|
|
|
|
export function getCredentialsData(credentials: IWorkflowCredentials): ICredentialsTypeData {
|
|
|
|
const credentialTypeData: ICredentialsTypeData = {};
|
|
|
|
|
|
|
|
for (const credentialType of Object.keys(credentials)) {
|
|
|
|
if (credentialTypeData[credentialType] !== undefined) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.assign(credentialTypeData, getCredentialsDataWithParents(credentialType));
|
|
|
|
}
|
|
|
|
|
|
|
|
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>}
|
|
|
|
*/
|
|
|
|
export async function saveStaticData(workflow: Workflow): Promise <void> {
|
|
|
|
if (workflow.staticData.__dataChanged === true) {
|
|
|
|
// Static data of workflow changed and so has to be saved
|
|
|
|
if (isWorkflowIdValid(workflow.id) === true) {
|
|
|
|
// Workflow is saved so update in database
|
|
|
|
try {
|
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-05-01 20:43:01 -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>}
|
|
|
|
*/
|
|
|
|
export async function saveStaticDataById(workflowId: string | number, newStaticData: IDataObject): Promise<void> {
|
|
|
|
await Db.collections.Workflow!
|
|
|
|
.update(workflowId, {
|
|
|
|
staticData: newStaticData,
|
|
|
|
});
|
|
|
|
}
|
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
|
|
|
|
*/
|
|
|
|
export async function getStaticDataById(workflowId: string | number) {
|
|
|
|
const workflowData = await Db.collections.Workflow!
|
|
|
|
.findOne(workflowId, { select: ['staticData']});
|
|
|
|
|
|
|
|
if (workflowData === undefined) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
return workflowData.staticData || {};
|
|
|
|
}
|
2021-05-29 11:31:21 -07:00
|
|
|
|
|
|
|
|
|
|
|
// TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers?
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function throwDuplicateEntryError(error: Error) {
|
|
|
|
const errorMessage = error.message.toLowerCase();
|
|
|
|
if (errorMessage.includes('unique') || errorMessage.includes('duplicate')) {
|
|
|
|
throw new ResponseHelper.ResponseError('There is already a workflow with this name', undefined, 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new ResponseHelper.ResponseError(errorMessage, undefined, 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
export type WorkflowNameRequest = Express.Request & {
|
|
|
|
query: {
|
|
|
|
name?: string;
|
|
|
|
offset?: string;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|