/* eslint-disable import/no-cycle */ /* 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 */ /* 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, INodeCredentialsDetails, IRun, IRunExecutionData, ITaskData, LoggerProxy as Logger, Workflow, } from 'n8n-workflow'; // eslint-disable-next-line import/no-cycle import { CredentialTypes, Db, ICredentialsTypeData, ITransferNodeTypes, IWorkflowErrorData, IWorkflowExecutionDataProcess, NodeTypes, WhereClause, WorkflowRunner, } from '.'; import * as config from '../config'; // eslint-disable-next-line import/no-cycle import { WorkflowEntity } from './databases/entities/WorkflowEntity'; import { User } from './databases/entities/User'; import { getWorkflowOwner } from './UserManagement/UserManagementHelper'; const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string; /** * 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; const { lastNodeExecuted } = inputData.data.resultData; if (lastNodeExecuted === undefined) { return undefined; } if (runData[lastNodeExecuted] === undefined) { return undefined; } return runData[lastNodeExecuted][runData[lastNodeExecuted].length - 1]; } /** * 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); } // eslint-disable-next-line no-restricted-globals 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} */ export async function executeErrorWorkflow( workflowId: string, workflowErrorData: IWorkflowErrorData, runningUser: User, ): Promise { // Wrap everything in try/catch to make sure that no errors bubble up and all get caught here try { let workflowData; if (workflowId.toString() !== workflowErrorData.workflow.id?.toString()) { // To make this code easier to understand, we split it in 2 parts: // 1) Fetch the owner of the errored workflows and then // 2) if now instance owner, then check if the user has access to the // triggered workflow. const user = await getWorkflowOwner(workflowErrorData.workflow.id!); if (user.globalRole.name === 'owner') { workflowData = await Db.collections.Workflow!.findOne({ id: Number(workflowId) }); } else { const sharedWorkflowData = await Db.collections.SharedWorkflow!.findOne({ where: { workflow: { id: workflowId }, user, }, relations: ['workflow'], }); if (sharedWorkflowData) { workflowData = sharedWorkflowData.workflow; } } } else { workflowData = await Db.collections.Workflow!.findOne({ id: Number(workflowId) }); } if (workflowData === undefined) { // The error workflow could not be found 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 }, ); return; } const user = await getWorkflowOwner(workflowId); if (user.id !== runningUser.id) { // The error workflow could not be found Logger.warn( `An attempt to execute workflow ID ${workflowId} as error workflow was blocked due to wrong permission`, ); return; } const executionMode = 'error'; const nodeTypes = NodeTypes(); 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, }); 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) { Logger.error( `Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`, ); return; } // Can execute without webhook so go on // Initialize the data of the webhook node const nodeExecutionStack: IExecuteData[] = []; nodeExecutionStack.push({ node: workflowStartNode, data: { main: [ [ { json: workflowErrorData, }, ], ], }, }); const runExecutionData: IRunExecutionData = { startData: {}, resultData: { runData: {}, }, executionData: { contextData: {}, nodeExecutionStack, waitingExecution: {}, }, }; const runData: IWorkflowExecutionDataProcess = { executionMode, executionData: runExecutionData, workflowData, userId: user.id, }; const workflowRunner = new WorkflowRunner(); await workflowRunner.run(runData); } catch (error) { Logger.error( `Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`, { workflowId: workflowErrorData.workflow.id }, ); } } /** * 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 all the defined CredentialTypes * * @export * @returns {ICredentialsTypeData} */ export function getAllCredentalsTypeData(): ICredentialsTypeData { const credentialTypes = CredentialTypes(); // Get the data of all the credential types that they // can be loaded again in the subprocess const returnData: ICredentialsTypeData = {}; for (const credentialTypeName of Object.keys(credentialTypes.credentialTypes)) { if (credentialTypes.credentialTypes[credentialTypeName] === undefined) { throw new Error(`The CredentialType "${credentialTypeName}" could not be found!`); } returnData[credentialTypeName] = { className: credentialTypes.credentialTypes[credentialTypeName].type.constructor.name, sourcePath: credentialTypes.credentialTypes[credentialTypeName].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 // eslint-disable-next-line @typescript-eslint/no-use-before-define 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.type] === undefined) { throw new Error(`The NodeType "${nodeTypeName.type}" could not be found!`); } returnData[nodeTypeName.type] = { className: nodeTypes.nodeTypes[nodeTypeName.type].type.constructor.name, sourcePath: nodeTypes.nodeTypes[nodeTypeName.type].sourcePath, }; } return returnData; } /** * 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] = { className: credentialTypes.credentialTypes[type].type.constructor.name, sourcePath: credentialTypes.credentialTypes[type].sourcePath, }; if (credentialType === undefined || credentialType.extends === undefined) { return credentialTypeData; } for (const typeName of credentialType.extends) { if (credentialTypeData[typeName] !== undefined) { continue; } credentialTypeData[typeName] = { className: credentialTypes.credentialTypes[typeName].type.constructor.name, sourcePath: credentialTypes.credentialTypes[typeName].sourcePath, }; 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 getCredentialsDataByNodes(nodes: INode[]): ICredentialsTypeData { const credentialTypeData: ICredentialsTypeData = {}; for (const node of nodes) { const credentialsUsedByThisNode = node.credentials; if (credentialsUsedByThisNode) { // const credentialTypesUsedByThisNode = Object.keys(credentialsUsedByThisNode!); for (const credentialType of Object.keys(credentialsUsedByThisNode)) { if (credentialTypeData[credentialType] !== undefined) { continue; } Object.assign(credentialTypeData, getCredentialsDataWithParents(credentialType)); } } } return credentialTypeData; } /** * 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[]): Array<{ type: string; version: number }> { // Check which node-types have to be loaded const neededNodeTypes: Array<{ type: string; version: number }> = []; for (const node of nodes) { if (neededNodeTypes.find((neededNodes) => node.type === neededNodes.type) === undefined) { neededNodeTypes.push({ type: node.type, version: node.typeVersion }); } } return neededNodeTypes; } /** * Saves the static data if it changed * * @export * @param {Workflow} workflow * @returns {Promise } */ export async function saveStaticData(workflow: Workflow): Promise { if (workflow.staticData.__dataChanged === true) { // Static data of workflow changed and so has to be saved if (isWorkflowIdValid(workflow.id)) { // Workflow is saved so update in database try { // eslint-disable-next-line @typescript-eslint/no-use-before-define await saveStaticDataById(workflow.id!, workflow.staticData); workflow.staticData.__dataChanged = false; } catch (e) { Logger.error( `There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${e.message}"`, { workflowId: workflow.id }, ); } } } } /** * 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} */ export async function saveStaticDataById( workflowId: string | number, newStaticData: IDataObject, ): Promise { await Db.collections.Workflow!.update(workflowId, { staticData: newStaticData, }); } /** * Returns the static data of workflow * * @export * @param {(string | number)} workflowId The id of the workflow to get static data of * @returns */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export async function getStaticDataById(workflowId: string | number) { const workflowData = await Db.collections.Workflow!.findOne(workflowId, { select: ['staticData'], }); if (workflowData === undefined) { return {}; } // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return workflowData.staticData || {}; } // Checking if credentials of old format are in use and run a DB check if they might exist uniquely export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promise { const { nodes } = workflow; if (!nodes) return workflow; // caching const credentialsByName: Record> = {}; const credentialsById: Record> = {}; // for loop to run DB fetches sequential and use cache to keep pressure off DB // trade-off: longer response time for less DB queries /* eslint-disable no-await-in-loop */ for (const node of nodes) { if (!node.credentials || node.disabled) { continue; } // extract credentials types const allNodeCredentials = Object.entries(node.credentials); for (const [nodeCredentialType, nodeCredentials] of allNodeCredentials) { // Check if Node applies old credentials style if (typeof nodeCredentials === 'string' || nodeCredentials.id === null) { const name = typeof nodeCredentials === 'string' ? nodeCredentials : nodeCredentials.name; // init cache for type if (!credentialsByName[nodeCredentialType]) { credentialsByName[nodeCredentialType] = {}; } if (credentialsByName[nodeCredentialType][name] === undefined) { const credentials = await Db.collections.Credentials?.find({ name, type: nodeCredentialType, }); // if credential name-type combination is unique, use it if (credentials?.length === 1) { credentialsByName[nodeCredentialType][name] = { id: credentials[0].id.toString(), name: credentials[0].name, }; node.credentials[nodeCredentialType] = credentialsByName[nodeCredentialType][name]; continue; } // nothing found - add invalid credentials to cache to prevent further DB checks credentialsByName[nodeCredentialType][name] = { id: null, name, }; } else { // get credentials from cache node.credentials[nodeCredentialType] = credentialsByName[nodeCredentialType][name]; } continue; } // Node has credentials with an ID // init cache for type if (!credentialsById[nodeCredentialType]) { credentialsById[nodeCredentialType] = {}; } // check if credentials for ID-type are not yet cached if (credentialsById[nodeCredentialType][nodeCredentials.id] === undefined) { // check first if ID-type combination exists const credentials = await Db.collections.Credentials?.findOne({ id: nodeCredentials.id, type: nodeCredentialType, }); if (credentials) { credentialsById[nodeCredentialType][nodeCredentials.id] = { id: credentials.id.toString(), name: credentials.name, }; node.credentials[nodeCredentialType] = credentialsById[nodeCredentialType][nodeCredentials.id]; continue; } // no credentials found for ID, check if some exist for name const credsByName = await Db.collections.Credentials?.find({ name: nodeCredentials.name, type: nodeCredentialType, }); // if credential name-type combination is unique, take it if (credsByName?.length === 1) { // add found credential to cache credentialsById[nodeCredentialType][credsByName[0].id] = { id: credsByName[0].id.toString(), name: credsByName[0].name, }; node.credentials[nodeCredentialType] = credentialsById[nodeCredentialType][credsByName[0].id]; continue; } // nothing found - add invalid credentials to cache to prevent further DB checks credentialsById[nodeCredentialType][nodeCredentials.id] = nodeCredentials; continue; } // get credentials from cache node.credentials[nodeCredentialType] = credentialsById[nodeCredentialType][nodeCredentials.id]; } } /* eslint-enable no-await-in-loop */ return workflow; } /** * Build a `where` clause for a TypeORM entity search, * checking for member access if the user is not an owner. */ export function whereClause({ user, entityType, entityId = '', }: { user: User; entityType: 'workflow' | 'credentials'; entityId?: string; }): WhereClause { const where: WhereClause = entityId ? { [entityType]: { id: entityId } } : {}; // TODO: Decide if owner access should be restricted if (user.globalRole.name !== 'owner') { where.user = { id: user.id }; } return where; } /** * Get the IDs of the workflows that have been shared with the user. */ export async function getSharedWorkflowIds(user: User): Promise { const sharedWorkflows = await Db.collections.SharedWorkflow!.find({ relations: ['workflow'], where: whereClause({ user, entityType: 'workflow', }), }); return sharedWorkflows.map(({ workflow }) => workflow.id); }