import express from 'express'; import { v4 as uuid } from 'uuid'; import { LoggerProxy } from 'n8n-workflow'; import axios from 'axios'; import * as Db from '@/Db'; import * as GenericHelpers from '@/GenericHelpers'; import * as ResponseHelper from '@/ResponseHelper'; import * as WorkflowHelpers from '@/WorkflowHelpers'; import type { IWorkflowResponse, IExecutionPushResponse } from '@/Interfaces'; import config from '@/config'; import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { validateEntity } from '@/GenericHelpers'; import { ExternalHooks } from '@/ExternalHooks'; import { getLogger } from '@/Logger'; import type { ListQuery, WorkflowRequest } from '@/requests'; import { isBelowOnboardingThreshold } from '@/WorkflowHelpers'; import { EEWorkflowController } from './workflows.controller.ee'; import { WorkflowsService } from './workflows.services'; import { whereClause } from '@/UserManagement/UserManagementHelper'; import { In } from 'typeorm'; import { Container } from 'typedi'; import { InternalHooks } from '@/InternalHooks'; import { RoleService } from '@/services/role.service'; import * as utils from '@/utils'; import { listQueryMiddleware } from '@/middlewares'; import { TagService } from '@/services/tag.service'; export const workflowsController = express.Router(); /** * Initialize Logger if needed */ workflowsController.use((req, res, next) => { try { LoggerProxy.getInstance(); } catch (error) { LoggerProxy.init(getLogger()); } next(); }); workflowsController.use('/', EEWorkflowController); /** * POST /workflows */ workflowsController.post( '/', ResponseHelper.send(async (req: WorkflowRequest.Create) => { delete req.body.id; // delete if sent const newWorkflow = new WorkflowEntity(); Object.assign(newWorkflow, req.body); newWorkflow.versionId = uuid(); await validateEntity(newWorkflow); await Container.get(ExternalHooks).run('workflow.create', [newWorkflow]); const { tags: tagIds } = req.body; if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) { newWorkflow.tags = await Container.get(TagService).findMany({ select: ['id', 'name'], where: { id: In(tagIds), }, }); } await WorkflowHelpers.replaceInvalidCredentials(newWorkflow); WorkflowHelpers.addNodeIds(newWorkflow); let savedWorkflow: undefined | WorkflowEntity; await Db.transaction(async (transactionManager) => { savedWorkflow = await transactionManager.save(newWorkflow); const role = await Container.get(RoleService).findWorkflowOwnerRole(); const newSharedWorkflow = new SharedWorkflow(); Object.assign(newSharedWorkflow, { role, user: req.user, workflow: savedWorkflow, }); await transactionManager.save(newSharedWorkflow); }); if (!savedWorkflow) { LoggerProxy.error('Failed to create workflow', { userId: req.user.id }); throw new ResponseHelper.InternalServerError('Failed to save workflow'); } if (tagIds && !config.getEnv('workflowTagsDisabled') && savedWorkflow.tags) { savedWorkflow.tags = Container.get(TagService).sortByRequestOrder(savedWorkflow.tags, { requestOrder: tagIds, }); } await Container.get(ExternalHooks).run('workflow.afterCreate', [savedWorkflow]); void Container.get(InternalHooks).onWorkflowCreated(req.user, newWorkflow, false); return savedWorkflow; }), ); /** * GET /workflows */ workflowsController.get( '/', listQueryMiddleware, async (req: ListQuery.Request, res: express.Response) => { try { const sharedWorkflowIds = await WorkflowHelpers.getSharedWorkflowIds(req.user, ['owner']); const { workflows: data, count } = await WorkflowsService.getMany( sharedWorkflowIds, req.listQueryOptions, ); res.json({ count, data }); } catch (maybeError) { const error = utils.toError(maybeError); ResponseHelper.reportError(error); ResponseHelper.sendErrorResponse(res, error); } }, ); /** * GET /workflows/new */ workflowsController.get( '/new', ResponseHelper.send(async (req: WorkflowRequest.NewName) => { const requestedName = req.query.name && req.query.name !== '' ? req.query.name : config.getEnv('workflows.defaultName'); const name = await GenericHelpers.generateUniqueName(requestedName, 'workflow'); const onboardingFlowEnabled = !config.getEnv('workflows.onboardingFlowDisabled') && !req.user.settings?.isOnboarded && (await isBelowOnboardingThreshold(req.user)); return { name, onboardingFlowEnabled }; }), ); // Reads and returns workflow data from an URL /** * GET /workflows/from-url */ workflowsController.get( '/from-url', ResponseHelper.send(async (req: express.Request): Promise => { if (req.query.url === undefined) { throw new ResponseHelper.BadRequestError('The parameter "url" is missing!'); } if (!/^http[s]?:\/\/.*\.json$/i.exec(req.query.url as string)) { throw new ResponseHelper.BadRequestError( 'The parameter "url" is not valid! It does not seem to be a URL pointing to a n8n workflow JSON file.', ); } let workflowData: IWorkflowResponse | undefined; try { const { data } = await axios.get(req.query.url as string); workflowData = data; } catch (error) { throw new ResponseHelper.BadRequestError('The URL does not point to valid JSON file!'); } // Do a very basic check if it is really a n8n-workflow-json if ( workflowData?.nodes === undefined || !Array.isArray(workflowData.nodes) || workflowData.connections === undefined || typeof workflowData.connections !== 'object' || Array.isArray(workflowData.connections) ) { throw new ResponseHelper.BadRequestError( 'The data in the file does not seem to be a n8n workflow JSON file!', ); } return workflowData; }), ); /** * GET /workflows/:id */ workflowsController.get( '/:id(\\w+)', ResponseHelper.send(async (req: WorkflowRequest.Get) => { const { id: workflowId } = req.params; let relations = ['workflow', 'workflow.tags', 'role']; if (config.getEnv('workflowTagsDisabled')) { relations = relations.filter((relation) => relation !== 'workflow.tags'); } const shared = await Db.collections.SharedWorkflow.findOne({ relations, where: whereClause({ user: req.user, entityType: 'workflow', entityId: workflowId, roles: ['owner'], }), }); if (!shared) { LoggerProxy.verbose('User attempted to access a workflow without permissions', { workflowId, userId: req.user.id, }); throw new ResponseHelper.NotFoundError( 'Could not load the workflow - you can only access workflows owned by you', ); } return shared.workflow; }), ); // Updates an existing workflow /** * PATCH /workflows/:id */ workflowsController.patch( '/:id(\\w+)', ResponseHelper.send(async (req: WorkflowRequest.Update) => { const { id: workflowId } = req.params; const updateData = new WorkflowEntity(); const { tags, ...rest } = req.body; Object.assign(updateData, rest); const updatedWorkflow = await WorkflowsService.update( req.user, updateData, workflowId, tags, true, ['owner'], ); return updatedWorkflow; }), ); // Deletes a specific workflow /** * DELETE /workflows/:id */ workflowsController.delete( '/:id(\\w+)', ResponseHelper.send(async (req: WorkflowRequest.Delete) => { const { id: workflowId } = req.params; const workflow = await WorkflowsService.delete(req.user, workflowId); if (!workflow) { LoggerProxy.verbose('User attempted to delete a workflow without permissions', { workflowId, userId: req.user.id, }); throw new ResponseHelper.BadRequestError( 'Could not delete the workflow - you can only remove workflows owned by you', ); } return true; }), ); /** * POST /workflows/run */ workflowsController.post( '/run', ResponseHelper.send(async (req: WorkflowRequest.ManualRun): Promise => { return WorkflowsService.runManually(req.body, req.user, GenericHelpers.getSessionId(req)); }), );