Files
Primora/apps/backend/internal/handlers/functions.go
Tomas DvorakandDevin 32587c8063 feat: Functions — JS/TS execution with run log
core.functions + core.function_runs: project-scoped code executed by a
runtime binary on the backend host (bun or deno per function). Payload
arrives on stdin + PRIMORA_PAYLOAD env; stdout/stderr captured per run
(capped), with status/exit code/duration. Invoke returns 502 + the
recorded run on failure so errors stay observable.

Dashboard gains a Functions page: code editor, JSON payload invoke,
live result, run history. Runner config via FUNCTIONS_RUNTIME /
FUNCTIONS_TIMEOUT_SECONDS / FUNCTIONS_MAX_OUTPUT_BYTES.

Verified live: bun ran code, stdin payload delivered, error path returns
502 with captured stderr; timeout kills at deadline.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-24 19:46:32 +02:00

147 lines
3.4 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
apperrors "github.com/tdvorak/primora/apps/backend/internal/response"
"github.com/tdvorak/primora/apps/backend/internal/middleware"
"github.com/tdvorak/primora/apps/backend/internal/services"
)
func (h *HTTPHandler) listFunctions(c *gin.Context) {
actor, projectID, ok := h.actorAndProject(c)
if !ok {
return
}
items, err := h.Platform.ListFunctions(c.Request.Context(), actor, projectID)
if err != nil {
h.handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (h *HTTPHandler) createFunction(c *gin.Context) {
actor, projectID, ok := h.actorAndProject(c)
if !ok {
return
}
var body services.CreateFunctionInput
if !h.bindAndValidate(c, &body) {
return
}
fn, err := h.Platform.CreateFunction(c.Request.Context(), actor, projectID, body, middleware.RequestIDFromContext(c))
if err != nil {
h.handleError(c, err)
return
}
c.JSON(http.StatusCreated, fn)
}
func (h *HTTPHandler) getFunction(c *gin.Context) {
actor, _, ok := h.actorAndProject(c)
if !ok {
return
}
functionID, ok := parseUUIDParam(c, "functionID")
if !ok {
return
}
fn, code, err := h.Platform.GetFunctionCode(c.Request.Context(), actor, functionID)
if err != nil {
h.handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"function": fn, "code": code})
}
func (h *HTTPHandler) updateFunction(c *gin.Context) {
actor, _, ok := h.actorAndProject(c)
if !ok {
return
}
functionID, ok := parseUUIDParam(c, "functionID")
if !ok {
return
}
var body services.UpdateFunctionInput
if !h.bindAndValidate(c, &body) {
return
}
fn, err := h.Platform.UpdateFunction(c.Request.Context(), actor, functionID, body, middleware.RequestIDFromContext(c))
if err != nil {
h.handleError(c, err)
return
}
c.JSON(http.StatusOK, fn)
}
func (h *HTTPHandler) deleteFunction(c *gin.Context) {
actor, _, ok := h.actorAndProject(c)
if !ok {
return
}
functionID, ok := parseUUIDParam(c, "functionID")
if !ok {
return
}
if err := h.Platform.DeleteFunction(c.Request.Context(), actor, functionID, middleware.RequestIDFromContext(c)); err != nil {
h.handleError(c, err)
return
}
c.Status(http.StatusNoContent)
}
func (h *HTTPHandler) invokeFunction(c *gin.Context) {
actor, _, ok := h.actorAndProject(c)
if !ok {
return
}
functionID, ok := parseUUIDParam(c, "functionID")
if !ok {
return
}
var body struct {
Payload json.RawMessage `json:"payload"`
}
// An empty body is a valid invoke — defaults to {}.
if c.Request.Body != nil && c.Request.ContentLength > 0 {
if err := c.ShouldBindJSON(&body); err != nil {
apperrors.Abort(c, http.StatusBadRequest, "invalid_json", err.Error())
return
}
}
run, err := h.Platform.InvokeFunction(c.Request.Context(), actor, functionID, body.Payload, middleware.RequestIDFromContext(c))
if err != nil {
h.handleError(c, err)
return
}
status := http.StatusOK
if run.Status != "success" {
status = http.StatusBadGateway
}
c.JSON(status, run)
}
func (h *HTTPHandler) listFunctionRuns(c *gin.Context) {
actor, _, ok := h.actorAndProject(c)
if !ok {
return
}
functionID, ok := parseUUIDParam(c, "functionID")
if !ok {
return
}
limit, _ := strconv.ParseInt(c.DefaultQuery("limit", "50"), 10, 64)
runs, err := h.Platform.ListFunctionRuns(c.Request.Context(), actor, functionID, limit)
if err != nil {
h.handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"items": runs})
}