package main // REST handlers (read-only) for data_factory.db (issue 0097). // // Endpoints: // GET /api/datafactory/nodes?kind= // GET /api/datafactory/runs?node_id=&limit=N // GET /api/datafactory/databases // // All endpoints lazy-open data_factory.db on first request (creating the // file + applying migrations if missing). If the open fails, returns 503. // POST trigger is intentionally NOT implemented — sqlite_api is read-only; // run insertion is done out-of-band by DAG steps / function wrappers. import ( "context" "database/sql" "net/http" "strconv" ) // dataFactoryNode is the JSON row for /api/datafactory/nodes. type dataFactoryNode struct { ID string `json:"id"` Kind string `json:"kind"` Name string `json:"name"` FunctionID string `json:"function_id"` Description string `json:"description"` ConfigJSON string `json:"config_json"` ScheduleCron string `json:"schedule_cron"` Enabled bool `json:"enabled"` TagsCSV string `json:"tags_csv"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // dataFactoryRun is the JSON row for /api/datafactory/runs. type dataFactoryRun struct { ID string `json:"id"` NodeID string `json:"node_id"` StartedAt string `json:"started_at"` FinishedAt string `json:"finished_at"` Status string `json:"status"` RowsIn int64 `json:"rows_in"` RowsOut int64 `json:"rows_out"` KbIn int64 `json:"kb_in"` KbOut int64 `json:"kb_out"` DurationMS int64 `json:"duration_ms"` Trigger string `json:"trigger"` Error string `json:"error"` Notes string `json:"notes"` } // dataFactoryDatabase is the JSON row for /api/datafactory/databases. type dataFactoryDatabase struct { ID string `json:"id"` Kind string `json:"kind"` Label string `json:"label"` URI string `json:"uri"` Description string `json:"description"` TagsCSV string `json:"tags_csv"` LastSeenAt string `json:"last_seen_at"` TableCount int64 `json:"table_count"` SizeBytes int64 `json:"size_bytes"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } func (s *Server) handleDataFactoryNodes(w http.ResponseWriter, r *http.Request) { db, err := s.dataFactoryDB() if err != nil { writeError(w, http.StatusServiceUnavailable, "data_factory.db unavailable: "+err.Error()) return } ctx, cancel := context.WithTimeout(r.Context(), queryTimeout) defer cancel() kind := r.URL.Query().Get("kind") var rows *sql.Rows if kind != "" { rows, err = db.QueryContext(ctx, ` SELECT id, kind, name, function_id, description, config_json, schedule_cron, enabled, tags_csv, created_at, updated_at FROM nodes WHERE kind = ? ORDER BY kind, name`, kind) } else { rows, err = db.QueryContext(ctx, ` SELECT id, kind, name, function_id, description, config_json, schedule_cron, enabled, tags_csv, created_at, updated_at FROM nodes ORDER BY kind, name`) } if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } defer rows.Close() nodes := make([]dataFactoryNode, 0, 16) for rows.Next() { var n dataFactoryNode var enabled int if err := rows.Scan(&n.ID, &n.Kind, &n.Name, &n.FunctionID, &n.Description, &n.ConfigJSON, &n.ScheduleCron, &enabled, &n.TagsCSV, &n.CreatedAt, &n.UpdatedAt); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } n.Enabled = enabled != 0 nodes = append(nodes, n) } writeJSON(w, http.StatusOK, map[string]any{"nodes": nodes, "count": len(nodes)}) } func (s *Server) handleDataFactoryRuns(w http.ResponseWriter, r *http.Request) { db, err := s.dataFactoryDB() if err != nil { writeError(w, http.StatusServiceUnavailable, "data_factory.db unavailable: "+err.Error()) return } ctx, cancel := context.WithTimeout(r.Context(), queryTimeout) defer cancel() nodeID := r.URL.Query().Get("node_id") limit := 100 if l := r.URL.Query().Get("limit"); l != "" { if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 1000 { limit = n } } var rows *sql.Rows if nodeID != "" { rows, err = db.QueryContext(ctx, ` SELECT id, node_id, started_at, COALESCE(finished_at,''), status, rows_in, rows_out, kb_in, kb_out, duration_ms, trigger, error, notes FROM runs WHERE node_id = ? ORDER BY started_at DESC LIMIT ?`, nodeID, limit) } else { rows, err = db.QueryContext(ctx, ` SELECT id, node_id, started_at, COALESCE(finished_at,''), status, rows_in, rows_out, kb_in, kb_out, duration_ms, trigger, error, notes FROM runs ORDER BY started_at DESC LIMIT ?`, limit) } if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } defer rows.Close() runs := make([]dataFactoryRun, 0, 16) for rows.Next() { var rr dataFactoryRun if err := rows.Scan(&rr.ID, &rr.NodeID, &rr.StartedAt, &rr.FinishedAt, &rr.Status, &rr.RowsIn, &rr.RowsOut, &rr.KbIn, &rr.KbOut, &rr.DurationMS, &rr.Trigger, &rr.Error, &rr.Notes); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } runs = append(runs, rr) } writeJSON(w, http.StatusOK, map[string]any{"runs": runs, "count": len(runs)}) } func (s *Server) handleDataFactoryDatabases(w http.ResponseWriter, r *http.Request) { db, err := s.dataFactoryDB() if err != nil { writeError(w, http.StatusServiceUnavailable, "data_factory.db unavailable: "+err.Error()) return } ctx, cancel := context.WithTimeout(r.Context(), queryTimeout) defer cancel() rows, err := db.QueryContext(ctx, ` SELECT id, kind, label, uri, description, tags_csv, last_seen_at, table_count, size_bytes, created_at, updated_at FROM databases ORDER BY kind, label`) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } defer rows.Close() out := make([]dataFactoryDatabase, 0, 16) for rows.Next() { var d dataFactoryDatabase if err := rows.Scan(&d.ID, &d.Kind, &d.Label, &d.URI, &d.Description, &d.TagsCSV, &d.LastSeenAt, &d.TableCount, &d.SizeBytes, &d.CreatedAt, &d.UpdatedAt); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } out = append(out, d) } writeJSON(w, http.StatusOK, map[string]any{"databases": out, "count": len(out)}) }