From 93423e32e60b354ff8281d1ca15ab5782a89b014 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Tue, 21 Jul 2026 11:31:05 +0100 Subject: [PATCH] feat(server): step usage counts endpoint --- server/internal/api/workflows.go | 10 ++++ server/internal/services/workflows.go | 28 +++++++++ .../internal/services/workflows_usage_test.go | 59 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 server/internal/services/workflows_usage_test.go diff --git a/server/internal/api/workflows.go b/server/internal/api/workflows.go index a9e77e8..8ca8248 100644 --- a/server/internal/api/workflows.go +++ b/server/internal/api/workflows.go @@ -23,6 +23,7 @@ func registerWorkflowRoutes(g *gin.RouterGroup) { g.GET("/steps/:id/export", exportStep) g.POST("/steps/import", importStep) g.POST("/steps/seed-defaults", seedDefaults) + g.GET("/steps/usage", stepUsage) g.POST("/steps/parse", parseStep) g.GET("/workflows", listWorkflows) @@ -154,6 +155,15 @@ func listSteps(c *gin.Context) { c.JSON(http.StatusOK, steps) } +func stepUsage(c *gin.Context) { + counts, err := services.StepUsageCounts() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, counts) +} + func createStep(c *gin.Context) { var s models.WorkflowStep if err := c.ShouldBindJSON(&s); err != nil { diff --git a/server/internal/services/workflows.go b/server/internal/services/workflows.go index 124d1e1..1a55c1d 100644 --- a/server/internal/services/workflows.go +++ b/server/internal/services/workflows.go @@ -61,6 +61,34 @@ func ListSteps() ([]models.WorkflowStep, error) { return steps, nil } +// StepUsageCounts returns, per library step_id, the number of distinct +// workflows that reference it. Inline steps have no step_id and are ignored. +func StepUsageCounts() (map[string]int, error) { + ctx, cancel := wfCtx() + defer cancel() + cur, err := db.Col("workflows").Find(ctx, bson.M{}) + if err != nil { + return nil, err + } + defer cur.Close(ctx) + var wfs []models.Workflow + if err := cur.All(ctx, &wfs); err != nil { + return nil, err + } + counts := map[string]int{} + for _, w := range wfs { + seen := map[string]bool{} + for _, ref := range w.Steps { + if ref.StepID == "" || seen[ref.StepID] { + continue + } + seen[ref.StepID] = true + counts[ref.StepID]++ + } + } + return counts, nil +} + func CreateStep(s models.WorkflowStep) (*models.WorkflowStep, error) { ctx, cancel := wfCtx() defer cancel() diff --git a/server/internal/services/workflows_usage_test.go b/server/internal/services/workflows_usage_test.go new file mode 100644 index 0000000..3e54cb9 --- /dev/null +++ b/server/internal/services/workflows_usage_test.go @@ -0,0 +1,59 @@ +package services + +import ( + "os" + "testing" + + "github.com/mrhid6/vantage/server/internal/db" + "github.com/mrhid6/vantage/server/internal/models" +) + +func TestMain(m *testing.M) { + uri := os.Getenv("VANTAGE_TEST_MONGO_URI") + if uri == "" { + uri = "mongodb://localhost:27117" + } + if err := db.Connect(uri, "vantage_test"); err != nil { + // No MongoDB available in this environment; skip DB-backed tests. + os.Exit(0) + } + code := m.Run() + os.Exit(code) +} + +func mkUsageStep(name string) models.WorkflowStep { + return models.WorkflowStep{Name: name, Interpreter: "bash", Script: "echo hi"} +} + +func mkWorkflowWithStep(name, stepID string) models.Workflow { + return models.Workflow{Name: name, Steps: []models.WorkflowStepRef{{StepID: stepID, Order: 0, OnFailure: "stop"}}} +} + +func TestStepUsageCounts(t *testing.T) { + // A step used by two workflows, a step used by none. + used, err := CreateStep(mkUsageStep("used-step")) + if err != nil { + t.Fatal(err) + } + unused, err := CreateStep(mkUsageStep("unused-step")) + if err != nil { + t.Fatal(err) + } + if _, err := CreateWorkflow(mkWorkflowWithStep("wf-a", used.StepID)); err != nil { + t.Fatal(err) + } + if _, err := CreateWorkflow(mkWorkflowWithStep("wf-b", used.StepID)); err != nil { + t.Fatal(err) + } + + counts, err := StepUsageCounts() + if err != nil { + t.Fatal(err) + } + if counts[used.StepID] != 2 { + t.Fatalf("used step: want 2, got %d", counts[used.StepID]) + } + if counts[unused.StepID] != 0 { + t.Fatalf("unused step: want 0, got %d", counts[unused.StepID]) + } +}