This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/workflow_instance_handlers.go
Jeffrey Smith 438143318f
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Has been skipped
CI/CD / test-go-pg (pull_request) Failing after 2m43s
CI/CD / test-sqlite (pull_request) Successful in 2m57s
CI/CD / build-and-deploy (pull_request) Has been skipped
Feat v0.3.2 workflow engine + handlers
Workflow execution engine with Start/Advance/Cancel lifecycle, automated
stage processor with Starlark hook firing and cycle guard (max 10),
instance and assignment HTTP handlers, store round-trip tests for all
v0.3.1 methods, and Starlark module expansion (get_instance, list_instances).

New routes:
- POST/GET /workflows/:id/instances (start, list)
- GET /workflows/:id/instances/:iid (get)
- POST /workflows/:id/instances/:iid/advance (advance)
- POST /workflows/:id/instances/:iid/cancel (cancel)
- POST /assignments/:id/claim|unclaim|complete|cancel
- GET /assignments/mine
- Team-scoped mirrors under /teams/:teamId/...

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:36:40 +00:00

135 lines
3.7 KiB
Go

package handlers
import (
"database/sql"
"encoding/json"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"switchboard-core/store"
"switchboard-core/workflow"
)
// ── Instance Handlers ───────────────────────
// WorkflowInstanceHandler manages workflow instance HTTP endpoints.
type WorkflowInstanceHandler struct {
engine *workflow.Engine
stores store.Stores
}
// NewWorkflowInstanceHandler creates an instance handler.
func NewWorkflowInstanceHandler(engine *workflow.Engine, stores store.Stores) *WorkflowInstanceHandler {
return &WorkflowInstanceHandler{engine: engine, stores: stores}
}
// Start creates a new workflow instance.
// POST /api/v1/workflows/:id/instances
func (h *WorkflowInstanceHandler) Start(c *gin.Context) {
workflowID := c.Param("id")
userID := c.GetString("user_id")
var body struct {
Data json.RawMessage `json:"data"`
Metadata json.RawMessage `json:"metadata"`
}
if err := c.ShouldBindJSON(&body); err != nil && err.Error() != "EOF" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if len(body.Data) == 0 {
body.Data = json.RawMessage(`{}`)
}
inst, err := h.engine.Start(c.Request.Context(), workflowID, body.Data, userID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, inst)
}
// ListInstances returns instances for a workflow.
// GET /api/v1/workflows/:id/instances?status=&limit=&offset=
func (h *WorkflowInstanceHandler) ListInstances(c *gin.Context) {
workflowID := c.Param("id")
status := c.Query("status")
opts := store.ListOptions{}
if l := c.Query("limit"); l != "" {
opts.Limit, _ = strconv.Atoi(l)
}
if o := c.Query("offset"); o != "" {
opts.Offset, _ = strconv.Atoi(o)
}
result, err := h.stores.Workflows.ListInstances(c.Request.Context(), workflowID, status, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list instances"})
return
}
if result == nil {
c.JSON(http.StatusOK, gin.H{"data": []struct{}{}})
return
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
// GetInstance returns a single instance.
// GET /api/v1/workflows/:id/instances/:iid
func (h *WorkflowInstanceHandler) GetInstance(c *gin.Context) {
inst, err := h.stores.Workflows.GetInstance(c.Request.Context(), c.Param("iid"))
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "instance not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Verify it belongs to the requested workflow
if inst.WorkflowID != c.Param("id") {
c.JSON(http.StatusNotFound, gin.H{"error": "instance not found"})
return
}
c.JSON(http.StatusOK, inst)
}
// Advance moves an instance to the next stage.
// POST /api/v1/workflows/:id/instances/:iid/advance
func (h *WorkflowInstanceHandler) Advance(c *gin.Context) {
userID := c.GetString("user_id")
var body struct {
Data json.RawMessage `json:"data"`
}
if err := c.ShouldBindJSON(&body); err != nil && err.Error() != "EOF" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
inst, err := h.engine.Advance(c.Request.Context(), c.Param("iid"), body.Data, userID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, inst)
}
// Cancel terminates an active instance.
// POST /api/v1/workflows/:id/instances/:iid/cancel
func (h *WorkflowInstanceHandler) Cancel(c *gin.Context) {
userID := c.GetString("user_id")
if err := h.engine.Cancel(c.Request.Context(), c.Param("iid"), userID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"cancelled": true})
}