Files
plunk/apps/api/src/controllers/Workflows.ts
T

374 lines
11 KiB
TypeScript

import {Controller, Delete, Get, Middleware, Patch, Post} from '@overnightjs/core';
import {WorkflowExecutionStatus} from '@plunk/db';
import type {NextFunction, Request, Response} from 'express';
import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth} from '../middleware/auth.js';
import {WorkflowService} from '../services/WorkflowService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('workflows')
export class Workflows {
/**
* GET /workflows
* List all workflows for the authenticated project
*/
@Get('')
@Middleware([requireAuth])
@CatchAsync
public async list(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const page = parseInt(req.query.page as string) || 1;
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100);
const search = req.query.search as string | undefined;
const result = await WorkflowService.list(auth.projectId!, page, pageSize, search);
return res.status(200).json(result);
}
/**
* GET /workflows/fields
* Get all available fields for workflow conditions (contact fields + event fields)
* Query param: eventName - Optional event name to filter event fields
* NOTE: This must be defined BEFORE the :id route to avoid conflicts
*/
@Get('fields')
@Middleware([requireAuth])
@CatchAsync
public async getAvailableFields(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const eventName = req.query.eventName as string | undefined;
try {
const result = await WorkflowService.getAvailableFields(auth.projectId!, eventName);
return res.status(200).json(result);
} catch (error) {
console.error('[WORKFLOWS] Failed to get available fields:', error);
return res.status(500).json({
error: error instanceof Error ? error.message : 'Failed to get available fields',
});
}
}
/**
* GET /workflows/:id
* Get a specific workflow with all steps and transitions
*/
@Get(':id')
@Middleware([requireAuth])
@CatchAsync
public async get(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
if (!workflowId) {
return res.status(400).json({error: 'Workflow ID is required'});
}
const workflow = await WorkflowService.get(auth.projectId!, workflowId);
return res.status(200).json(workflow);
}
/**
* POST /workflows
* Create a new workflow
*/
@Post('')
@Middleware([requireAuth])
@CatchAsync
public async create(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {name, description, eventName, enabled, allowReentry} = req.body;
if (!name) {
return res.status(400).json({error: 'Name is required'});
}
if (!eventName) {
return res.status(400).json({error: 'Event name is required'});
}
const workflow = await WorkflowService.create(auth.projectId!, {
name,
description,
eventName,
enabled,
allowReentry,
});
return res.status(201).json(workflow);
}
/**
* PATCH /workflows/:id
* Update a workflow
*/
@Patch(':id')
@Middleware([requireAuth])
@CatchAsync
public async update(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const {name, description, triggerType, triggerConfig, enabled, allowReentry} = req.body;
if (!workflowId) {
return res.status(400).json({error: 'Workflow ID is required'});
}
const workflow = await WorkflowService.update(auth.projectId!, workflowId, {
name,
description,
triggerType,
triggerConfig,
enabled,
allowReentry,
});
return res.status(200).json(workflow);
}
/**
* DELETE /workflows/:id
* Delete a workflow
*/
@Delete(':id')
@Middleware([requireAuth])
@CatchAsync
public async delete(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
if (!workflowId) {
return res.status(400).json({error: 'Workflow ID is required'});
}
await WorkflowService.delete(auth.projectId!, workflowId);
return res.status(204).send();
}
/**
* POST /workflows/:id/steps
* Add a step to a workflow
*/
@Post(':id/steps')
@Middleware([requireAuth])
@CatchAsync
public async addStep(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const {type, name, position, config, templateId, autoConnect} = req.body;
if (!workflowId) {
return res.status(400).json({error: 'Workflow ID is required'});
}
if (!type || !name || !position || !config) {
return res.status(400).json({error: 'Type, name, position, and config are required'});
}
const step = await WorkflowService.addStep(auth.projectId!, workflowId, {
type,
name,
position,
config,
templateId,
autoConnect,
});
return res.status(201).json(step);
}
/**
* PATCH /workflows/:id/steps/:stepId
* Update a workflow step
*/
@Patch(':id/steps/:stepId')
@Middleware([requireAuth])
@CatchAsync
public async updateStep(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const stepId = req.params.stepId;
const {name, position, config, templateId} = req.body;
if (!workflowId || !stepId) {
return res.status(400).json({error: 'Workflow ID and Step ID are required'});
}
const step = await WorkflowService.updateStep(auth.projectId!, workflowId, stepId, {
name,
position,
config,
templateId,
});
return res.status(200).json(step);
}
/**
* DELETE /workflows/:id/steps/:stepId
* Delete a workflow step
*/
@Delete(':id/steps/:stepId')
@Middleware([requireAuth])
@CatchAsync
public async deleteStep(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const stepId = req.params.stepId;
if (!workflowId || !stepId) {
return res.status(400).json({error: 'Workflow ID and Step ID are required'});
}
await WorkflowService.deleteStep(auth.projectId!, workflowId, stepId);
return res.status(204).send();
}
/**
* POST /workflows/:id/transitions
* Create a transition between steps
*/
@Post(':id/transitions')
@Middleware([requireAuth])
@CatchAsync
public async createTransition(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const {fromStepId, toStepId, condition, priority} = req.body;
if (!workflowId) {
return res.status(400).json({error: 'Workflow ID is required'});
}
if (!fromStepId || !toStepId) {
return res.status(400).json({error: 'From step ID and to step ID are required'});
}
const transition = await WorkflowService.createTransition(auth.projectId!, workflowId, {
fromStepId,
toStepId,
condition,
priority,
});
return res.status(201).json(transition);
}
/**
* DELETE /workflows/:id/transitions/:transitionId
* Delete a transition
*/
@Delete(':id/transitions/:transitionId')
@Middleware([requireAuth])
@CatchAsync
public async deleteTransition(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const transitionId = req.params.transitionId;
if (!workflowId || !transitionId) {
return res.status(400).json({error: 'Workflow ID and Transition ID are required'});
}
await WorkflowService.deleteTransition(auth.projectId!, workflowId, transitionId);
return res.status(204).send();
}
/**
* POST /workflows/:id/executions
* Start a workflow execution for a contact
*/
@Post(':id/executions')
@Middleware([requireAuth])
@CatchAsync
public async startExecution(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const {contactId, context} = req.body;
if (!workflowId) {
return res.status(400).json({error: 'Workflow ID is required'});
}
if (!contactId) {
return res.status(400).json({error: 'Contact ID is required'});
}
const execution = await WorkflowService.startExecution(auth.projectId!, workflowId, contactId, context);
return res.status(201).json(execution);
}
/**
* GET /workflows/:id/executions
* List executions for a workflow
*/
@Get(':id/executions')
@Middleware([requireAuth])
@CatchAsync
public async listExecutions(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const page = parseInt(req.query.page as string) || 1;
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100);
const status = req.query.status as WorkflowExecutionStatus | undefined;
if (!workflowId) {
return res.status(400).json({error: 'Workflow ID is required'});
}
const result = await WorkflowService.listExecutions(auth.projectId!, workflowId, page, pageSize, status);
return res.status(200).json(result);
}
/**
* GET /workflows/:id/executions/:executionId
* Get a specific execution with details
*/
@Get(':id/executions/:executionId')
@Middleware([requireAuth])
@CatchAsync
public async getExecution(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const executionId = req.params.executionId;
if (!workflowId || !executionId) {
return res.status(400).json({error: 'Workflow ID and Execution ID are required'});
}
const execution = await WorkflowService.getExecution(auth.projectId!, workflowId, executionId);
return res.status(200).json(execution);
}
/**
* DELETE /workflows/:id/executions/:executionId
* Cancel a workflow execution
*/
@Delete(':id/executions/:executionId')
@Middleware([requireAuth])
@CatchAsync
public async cancelExecution(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const executionId = req.params.executionId;
if (!workflowId || !executionId) {
return res.status(400).json({error: 'Workflow ID and Execution ID are required'});
}
const execution = await WorkflowService.cancelExecution(auth.projectId!, workflowId, executionId);
return res.status(200).json(execution);
}
}