mirror of
https://github.com/hanzoai/dashboards.git
synced 2026-10-30 20:19:37 +00:00
545 lines
21 KiB
Go
545 lines
21 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"strconv"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/dbx"
|
|
zaplib "github.com/luxfi/zap"
|
|
zcap "github.com/zap-proto/go/cap"
|
|
|
|
gen "github.com/hanzoai/dashboards/gen"
|
|
)
|
|
|
|
// handlers.go holds the per-router method bodies. Each is a thin, total mapping
|
|
// between a generated request view and the project-scoped Base collection,
|
|
// returning a generated result view. The capability gate + project scoping have
|
|
// already run in server.handle — handlers trust `project` is the authorized
|
|
// tenant and never re-check the cap.
|
|
|
|
// ─────────────────────────────── dashboardRouter ───────────────────────────
|
|
|
|
func (s *Server) handleAllDashboards(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapListReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad ListReq: "+err.Error())
|
|
}
|
|
rows, total, err := s.listScoped(CollectionDashboard, project,
|
|
sortExpr(in.OrderByColumn(), in.OrderByOrder()), int(in.Page()), int(in.Limit()))
|
|
if err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
items := make([][]byte, len(rows))
|
|
for i, r := range rows {
|
|
items[i] = encodeDashboard(r)
|
|
}
|
|
return s.ok(req, gen.NewDashboardList(gen.DashboardListInput{Dashboards: items, TotalCount: uint32(total)}))
|
|
}
|
|
|
|
func (s *Server) handleGetDashboard(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapIdReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad IdReq: "+err.Error())
|
|
}
|
|
rec, err := s.findScoped(CollectionDashboard, project, in.Id())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "dashboard not found")
|
|
}
|
|
return s.ok(req, encodeDashboard(rec))
|
|
}
|
|
|
|
func (s *Server) handleCreateDashboard(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapCreateDashReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad CreateDashReq: "+err.Error())
|
|
}
|
|
if in.Name() == "" {
|
|
return s.fail(req, StatusBadRequest, "dashboard name is required")
|
|
}
|
|
col, err := s.app.FindCollectionByNameOrId(CollectionDashboard)
|
|
if err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
rec := core.NewRecord(col)
|
|
rec.Set(fProjectId, project)
|
|
rec.Set(fName, in.Name())
|
|
rec.Set(fDashDescription, in.Description())
|
|
rec.Set(fDashOwner, "PROJECT")
|
|
rec.Set(fDashDefinition, `{"widgets":[]}`)
|
|
rec.Set(fDashFilters, "[]")
|
|
rec.Set(fCreatedBy, holderOf(req))
|
|
if err := s.app.Save(rec); err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
return s.ok(req, encodeDashboard(rec))
|
|
}
|
|
|
|
func (s *Server) handleUpdateDashboardMeta(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapUpdateDashReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad UpdateDashReq: "+err.Error())
|
|
}
|
|
if in.Name() == "" {
|
|
return s.fail(req, StatusBadRequest, "dashboard name is required")
|
|
}
|
|
rec, err := s.findScoped(CollectionDashboard, project, in.DashboardId())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "dashboard not found")
|
|
}
|
|
rec.Set(fName, in.Name())
|
|
rec.Set(fDashDescription, in.Description())
|
|
if err := s.app.Save(rec); err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
return s.ok(req, encodeDashboard(rec))
|
|
}
|
|
|
|
func (s *Server) handleUpdateDashboardDef(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapDashDefReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad DashDefReq: "+err.Error())
|
|
}
|
|
rec, err := s.findScoped(CollectionDashboard, project, in.DashboardId())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "dashboard not found")
|
|
}
|
|
rec.Set(fDashDefinition, jsonOrEmpty(in.Definition(), `{"widgets":[]}`))
|
|
if err := s.app.Save(rec); err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
return s.ok(req, encodeDashboard(rec))
|
|
}
|
|
|
|
func (s *Server) handleUpdateDashboardFilters(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapDashFiltersReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad DashFiltersReq: "+err.Error())
|
|
}
|
|
rec, err := s.findScoped(CollectionDashboard, project, in.DashboardId())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "dashboard not found")
|
|
}
|
|
rec.Set(fDashFilters, jsonOrEmpty(in.Filters(), "[]"))
|
|
if err := s.app.Save(rec); err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
return s.ok(req, encodeDashboard(rec))
|
|
}
|
|
|
|
func (s *Server) handleCloneDashboard(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapIdReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad IdReq: "+err.Error())
|
|
}
|
|
src, err := s.findScoped(CollectionDashboard, project, in.Id())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "source dashboard not found")
|
|
}
|
|
col, err := s.app.FindCollectionByNameOrId(CollectionDashboard)
|
|
if err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
clone := core.NewRecord(col)
|
|
clone.Set(fProjectId, project)
|
|
clone.Set(fName, src.GetString(fName)+" (Clone)")
|
|
clone.Set(fDashDescription, src.GetString(fDashDescription))
|
|
clone.Set(fDashOwner, "PROJECT")
|
|
clone.Set(fDashDefinition, src.GetString(fDashDefinition))
|
|
clone.Set(fDashFilters, src.GetString(fDashFilters))
|
|
clone.Set(fCreatedBy, holderOf(req))
|
|
if err := s.app.Save(clone); err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
return s.ok(req, encodeDashboard(clone))
|
|
}
|
|
|
|
func (s *Server) handleDeleteDashboard(req Call, project string) (*zaplib.Message, error) {
|
|
return s.deleteScoped(req, CollectionDashboard, project)
|
|
}
|
|
|
|
// ─────────────────────────── dashboardRouter analytics ─────────────────────
|
|
|
|
// handleAnalytics serves chart / scoreHistogram / executeQuery. These read from
|
|
// ClickHouse (OLAP), NOT from Base (OLTP). The query implementations live in
|
|
// console's @hanzo/shared analytics layer (getScoreAggregate,
|
|
// getNumericScoreHistogram, getObservation*ByTime, and the query-builder in
|
|
// features/query). Until the ClickHouse client is wired into this binary the
|
|
// method returns 501; it never fabricates rows.
|
|
//
|
|
// TODO(clickhouse): wire a ClickHouse reader and port the analytics functions
|
|
//
|
|
// from ~/work/hanzo/console/web/../packages/shared/src/server (score-analytics
|
|
// + the features/query executeQuery builder) into a server/analytics.go shim
|
|
// that returns AnalyticsResult.Rows as the JSON array the UI already expects.
|
|
func (s *Server) handleAnalytics(req Call) (*zaplib.Message, error) {
|
|
return s.fail(req, StatusNotImpl,
|
|
"analytics queries require the ClickHouse shim (server/analytics.go) — not yet wired")
|
|
}
|
|
|
|
// ──────────────────────────── dashboardWidgetRouter ────────────────────────
|
|
|
|
func (s *Server) handleAllWidgets(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapListReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad ListReq: "+err.Error())
|
|
}
|
|
rows, total, err := s.listScoped(CollectionWidget, project,
|
|
sortExpr(in.OrderByColumn(), in.OrderByOrder()), int(in.Page()), int(in.Limit()))
|
|
if err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
items := make([][]byte, len(rows))
|
|
for i, r := range rows {
|
|
items[i] = encodeWidget(r)
|
|
}
|
|
return s.ok(req, gen.NewWidgetList(gen.WidgetListInput{Widgets: items, TotalCount: uint32(total)}))
|
|
}
|
|
|
|
func (s *Server) handleGetWidget(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapIdReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad IdReq: "+err.Error())
|
|
}
|
|
rec, err := s.findScoped(CollectionWidget, project, in.Id())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "widget not found")
|
|
}
|
|
return s.ok(req, encodeWidget(rec))
|
|
}
|
|
|
|
// handleUpsertWidget creates (update=false) or updates (update=true) a widget.
|
|
func (s *Server) handleUpsertWidget(req Call, project string, update bool) (*zaplib.Message, error) {
|
|
in, err := gen.WrapWidgetReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad WidgetReq: "+err.Error())
|
|
}
|
|
if in.Name() == "" {
|
|
return s.fail(req, StatusBadRequest, "widget name is required")
|
|
}
|
|
var rec *core.Record
|
|
if update {
|
|
rec, err = s.findScoped(CollectionWidget, project, in.WidgetId())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "widget not found")
|
|
}
|
|
} else {
|
|
col, cerr := s.app.FindCollectionByNameOrId(CollectionWidget)
|
|
if cerr != nil {
|
|
return s.fail(req, StatusInternal, cerr.Error())
|
|
}
|
|
rec = core.NewRecord(col)
|
|
rec.Set(fProjectId, project)
|
|
rec.Set(fWidgetOwner, "PROJECT")
|
|
rec.Set(fCreatedBy, holderOf(req))
|
|
}
|
|
rec.Set(fName, in.Name())
|
|
rec.Set(fWidgetDescription, in.Description())
|
|
rec.Set(fWidgetView, in.View())
|
|
rec.Set(fWidgetDimensions, jsonOrEmpty(in.Dimensions(), "[]"))
|
|
rec.Set(fWidgetMetrics, jsonOrEmpty(in.Metrics(), "[]"))
|
|
rec.Set(fWidgetFilters, jsonOrEmpty(in.Filters(), "[]"))
|
|
rec.Set(fWidgetChartType, in.ChartType())
|
|
rec.Set(fWidgetChartConfig, jsonOrEmpty(in.ChartConfig(), "{}"))
|
|
if err := s.app.Save(rec); err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
return s.ok(req, encodeWidget(rec))
|
|
}
|
|
|
|
func (s *Server) handleCopyWidget(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapCopyWidgetReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad CopyWidgetReq: "+err.Error())
|
|
}
|
|
src, err := s.findScoped(CollectionWidget, project, in.WidgetId())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "source widget not found")
|
|
}
|
|
col, err := s.app.FindCollectionByNameOrId(CollectionWidget)
|
|
if err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
copyRec := core.NewRecord(col)
|
|
copyRec.Set(fProjectId, project)
|
|
copyRec.Set(fName, src.GetString(fName))
|
|
copyRec.Set(fWidgetDescription, src.GetString(fWidgetDescription))
|
|
copyRec.Set(fWidgetView, src.GetString(fWidgetView))
|
|
copyRec.Set(fWidgetOwner, "PROJECT")
|
|
copyRec.Set(fWidgetDimensions, src.GetString(fWidgetDimensions))
|
|
copyRec.Set(fWidgetMetrics, src.GetString(fWidgetMetrics))
|
|
copyRec.Set(fWidgetFilters, src.GetString(fWidgetFilters))
|
|
copyRec.Set(fWidgetChartType, src.GetString(fWidgetChartType))
|
|
copyRec.Set(fWidgetChartConfig, src.GetString(fWidgetChartConfig))
|
|
copyRec.Set(fCreatedBy, holderOf(req))
|
|
if err := s.app.Save(copyRec); err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
return s.ok(req, gen.NewMutation(gen.MutationInput{Success: true, Id: copyRec.Id}))
|
|
}
|
|
|
|
func (s *Server) handleDeleteWidget(req Call, project string) (*zaplib.Message, error) {
|
|
return s.deleteScoped(req, CollectionWidget, project)
|
|
}
|
|
|
|
// ─────────────────────────────── tableRouter ───────────────────────────────
|
|
|
|
// handleIsBatchActionInProgress mirrors tableRouter.getIsBatchActionInProgress,
|
|
// which queries the BullMQ BatchActionQueue for the job state of
|
|
// (projectId, tableName, actionId). The queue (Valkey/BullMQ) is the source of
|
|
// truth for live job state — Base does NOT hold it. Until the queue client is
|
|
// wired into this binary the method returns 501; it never guesses a state.
|
|
//
|
|
// TODO(queue): wire a Valkey/BullMQ reader (hanzoai/kv) and port
|
|
//
|
|
// generateBatchActionId + getJobState from
|
|
// ~/work/hanzo/console/web/src/features/table/server/{helpers,tableRouter}.ts
|
|
// into a server/batchqueue.go shim returning BoolResult{Value: inProgress}.
|
|
func (s *Server) handleIsBatchActionInProgress(req Call) (*zaplib.Message, error) {
|
|
return s.fail(req, StatusNotImpl,
|
|
"batch-action progress requires the BullMQ queue shim (server/batchqueue.go) — not yet wired")
|
|
}
|
|
|
|
// ────────────────────────── TableViewPresetsRouter ─────────────────────────
|
|
|
|
func (s *Server) handleGetPresetsByTableName(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapPresetListReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad PresetListReq: "+err.Error())
|
|
}
|
|
rows, err := s.app.FindRecordsByFilter(CollectionPreset,
|
|
"projectId = {:p} && tableName = {:t}", "-created", 0, 0,
|
|
dbx.Params{"p": project, "t": in.TableName()})
|
|
if err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
items := make([][]byte, len(rows))
|
|
for i, r := range rows {
|
|
items[i] = encodePreset(r)
|
|
}
|
|
return s.ok(req, gen.NewPresetList(gen.PresetListInput{Presets: items}))
|
|
}
|
|
|
|
func (s *Server) handleGetPresetById(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapIdReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad IdReq: "+err.Error())
|
|
}
|
|
rec, err := s.findScoped(CollectionPreset, project, in.Id())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "preset not found")
|
|
}
|
|
return s.ok(req, encodePreset(rec))
|
|
}
|
|
|
|
func (s *Server) handleUpsertPreset(req Call, project string, update bool) (*zaplib.Message, error) {
|
|
in, err := gen.WrapPresetReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad PresetReq: "+err.Error())
|
|
}
|
|
if in.Name() == "" {
|
|
return s.fail(req, StatusBadRequest, "view name is required")
|
|
}
|
|
var rec *core.Record
|
|
if update {
|
|
rec, err = s.findScoped(CollectionPreset, project, in.PresetId())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "preset not found")
|
|
}
|
|
} else {
|
|
col, cerr := s.app.FindCollectionByNameOrId(CollectionPreset)
|
|
if cerr != nil {
|
|
return s.fail(req, StatusInternal, cerr.Error())
|
|
}
|
|
rec = core.NewRecord(col)
|
|
rec.Set(fProjectId, project)
|
|
rec.Set(fCreatedBy, holderOf(req))
|
|
}
|
|
rec.Set(fName, in.Name())
|
|
rec.Set(fPresetTableName, in.TableName())
|
|
rec.Set(fPresetFilters, jsonOrEmpty(in.Filters(), "[]"))
|
|
rec.Set(fPresetColumnOrder, jsonOrEmpty(in.ColumnOrder(), "[]"))
|
|
rec.Set(fPresetColumnVisibility, jsonOrEmpty(in.ColumnVisibility(), "{}"))
|
|
rec.Set(fPresetSearchQuery, in.SearchQuery())
|
|
rec.Set(fPresetOrderByColumn, in.OrderByColumn())
|
|
rec.Set(fPresetOrderByOrder, in.OrderByOrder())
|
|
if err := s.app.Save(rec); err != nil {
|
|
// The unique (projectId, tableName, name) index surfaces a duplicate as a
|
|
// constraint error → 409, mirroring the console HanzoConflictError.
|
|
return s.fail(req, StatusConflict, "preset with this name already exists: "+err.Error())
|
|
}
|
|
return s.ok(req, encodePreset(rec))
|
|
}
|
|
|
|
func (s *Server) handleUpdatePresetName(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapPresetNameReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad PresetNameReq: "+err.Error())
|
|
}
|
|
rec, err := s.findScoped(CollectionPreset, project, in.PresetId())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "preset not found")
|
|
}
|
|
rec.Set(fName, in.Name())
|
|
rec.Set(fPresetTableName, in.TableName())
|
|
if err := s.app.Save(rec); err != nil {
|
|
return s.fail(req, StatusConflict, "preset name conflict: "+err.Error())
|
|
}
|
|
return s.ok(req, encodePreset(rec))
|
|
}
|
|
|
|
func (s *Server) handleDeletePreset(req Call, project string) (*zaplib.Message, error) {
|
|
return s.deleteScoped(req, CollectionPreset, project)
|
|
}
|
|
|
|
// handleGeneratePermalink builds the deep-link URL to a saved preset. Pure string
|
|
// composition (no I/O) — base/preset/table identity is all that's needed, mirror
|
|
// of TableViewService.generatePermalink.
|
|
func (s *Server) handleGeneratePermalink(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapPermalinkReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad PermalinkReq: "+err.Error())
|
|
}
|
|
if _, err := s.findScoped(CollectionPreset, project, in.PresetId()); err != nil {
|
|
return s.fail(req, StatusNotFound, "preset not found")
|
|
}
|
|
link := in.BaseUrl() + "/project/" + project + "/" + in.TableName() + "?viewId=" + in.PresetId()
|
|
return s.ok(req, gen.NewStringResult(gen.StringResultInput{Value: link}))
|
|
}
|
|
|
|
// ─────────────────────────────── monitorsRouter ────────────────────────────
|
|
|
|
func (s *Server) handleAllMonitors(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapListReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad ListReq: "+err.Error())
|
|
}
|
|
rows, total, err := s.listScoped(CollectionMonitor, project,
|
|
sortExpr(in.OrderByColumn(), in.OrderByOrder()), int(in.Page()), int(in.Limit()))
|
|
if err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
items := make([][]byte, len(rows))
|
|
for i, r := range rows {
|
|
items[i] = encodeMonitor(r)
|
|
}
|
|
return s.ok(req, gen.NewMonitorList(gen.MonitorListInput{Monitors: items, TotalCount: uint32(total)}))
|
|
}
|
|
|
|
func (s *Server) handleGetMonitor(req Call, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapIdReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad IdReq: "+err.Error())
|
|
}
|
|
rec, err := s.findScoped(CollectionMonitor, project, in.Id())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "monitor not found")
|
|
}
|
|
return s.ok(req, encodeMonitor(rec))
|
|
}
|
|
|
|
func (s *Server) handleUpsertMonitor(req Call, project string, update bool) (*zaplib.Message, error) {
|
|
in, err := gen.WrapMonitorReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad MonitorReq: "+err.Error())
|
|
}
|
|
if in.Name() == "" {
|
|
return s.fail(req, StatusBadRequest, "monitor name is required")
|
|
}
|
|
var rec *core.Record
|
|
if update {
|
|
rec, err = s.findScoped(CollectionMonitor, project, in.MonitorId())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "monitor not found")
|
|
}
|
|
} else {
|
|
col, cerr := s.app.FindCollectionByNameOrId(CollectionMonitor)
|
|
if cerr != nil {
|
|
return s.fail(req, StatusInternal, cerr.Error())
|
|
}
|
|
rec = core.NewRecord(col)
|
|
rec.Set(fProjectId, project)
|
|
rec.Set(fCreatedBy, holderOf(req))
|
|
rec.Set(fMonitorSeverity, "unknown")
|
|
rec.Set(fMonitorNextRunAt, "") // owned by the scheduler; seed empty
|
|
}
|
|
rec.Set(fName, in.Name())
|
|
rec.Set(fMonitorView, in.View())
|
|
rec.Set(fMonitorFilters, jsonOrEmpty(in.Filters(), "[]"))
|
|
rec.Set(fMonitorMetric, jsonOrEmpty(in.Metric(), "{}"))
|
|
rec.Set(fMonitorWindow, jsonOrEmpty(in.Window(), "{}"))
|
|
rec.Set(fMonitorThresholdOperator, in.ThresholdOperator())
|
|
rec.Set(fMonitorAlertThreshold, in.AlertThreshold())
|
|
rec.Set(fMonitorWarningThreshold, floatToNullableText(in.WarningThreshold()))
|
|
rec.Set(fMonitorNoData, jsonOrEmpty(in.NoData(), `{"mode":"SILENT"}`))
|
|
rec.Set(fMonitorRenotify, jsonOrEmpty(in.Renotify(), `{"mode":"OFF"}`))
|
|
rec.Set(fMonitorTags, jsonOrEmpty(in.Tags(), "[]"))
|
|
rec.Set(fMonitorStatus, orDefault(in.Status(), "active"))
|
|
if err := s.app.Save(rec); err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
return s.ok(req, encodeMonitor(rec))
|
|
}
|
|
|
|
func (s *Server) handleDeleteMonitor(req Call, project string) (*zaplib.Message, error) {
|
|
return s.deleteScoped(req, CollectionMonitor, project)
|
|
}
|
|
|
|
// ─────────────────────────── shared delete + helpers ───────────────────────
|
|
|
|
// deleteScoped deletes one row by id within the project (the tenant guard) and
|
|
// returns the { success } mutation shape the tRPC delete procedures returned.
|
|
func (s *Server) deleteScoped(req Call, collection, project string) (*zaplib.Message, error) {
|
|
in, err := gen.WrapIdReq(req.Payload)
|
|
if err != nil {
|
|
return s.fail(req, StatusBadRequest, "bad IdReq: "+err.Error())
|
|
}
|
|
rec, err := s.findScoped(collection, project, in.Id())
|
|
if err != nil {
|
|
return s.fail(req, StatusNotFound, "record not found")
|
|
}
|
|
if err := s.app.Delete(rec); err != nil {
|
|
return s.fail(req, StatusInternal, err.Error())
|
|
}
|
|
return s.ok(req, gen.NewMutation(gen.MutationInput{Success: true, Id: in.Id()}))
|
|
}
|
|
|
|
// jsonOrEmpty returns raw if it is non-empty, else the fallback default JSON.
|
|
func jsonOrEmpty(raw, def string) string {
|
|
if raw == "" || raw == "null" {
|
|
return def
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// orDefault returns v if non-empty, else def.
|
|
func orDefault(v, def string) string {
|
|
if v == "" {
|
|
return def
|
|
}
|
|
return v
|
|
}
|
|
|
|
// floatToNullableText encodes a wire f64 into the nullable-number text column:
|
|
// "" for the NaN null sentinel, else the decimal string. nullableTextToFloat is
|
|
// the inverse (in encode.go).
|
|
func floatToNullableText(f float64) string {
|
|
if isNull(f) {
|
|
return ""
|
|
}
|
|
return strconv.FormatFloat(f, 'g', -1, 64)
|
|
}
|
|
|
|
// holderOf returns the hex-encoded 32-byte holder hash of the call's capability
|
|
// as the createdBy attribution — the verified identity of the caller. The cap
|
|
// has already passed authorize(), so Wrap cannot fail here; the guard is belt-
|
|
// and-braces and yields "" rather than panicking.
|
|
func holderOf(req Call) string {
|
|
c, err := zcap.Wrap(req.Cap)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
h := c.Holder()
|
|
return hex.EncodeToString(h[:])
|
|
}
|