package main import ( "context" "database/sql" "net/http" ) // handleProjects lista proyectos con conteos nested (apps/analyses/vaults) // + conteos de huerfanas. // GET /api/projects func (s *Server) handleProjects(w http.ResponseWriter, r *http.Request) { db, err := s.pool.Get("registry") if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } ctx, cancel := context.WithTimeout(r.Context(), queryTimeout) defer cancel() const q = ` SELECT p.id, p.name, p.description, p.tags, p.dir_path, (SELECT COUNT(*) FROM apps WHERE project_id = p.id) AS apps_count, (SELECT COUNT(*) FROM analysis WHERE project_id = p.id) AS analyses_count, (SELECT COUNT(*) FROM vaults WHERE project_id = p.id) AS vaults_count FROM projects p ORDER BY p.name` rows, err := db.QueryContext(ctx, q) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } defer rows.Close() type ProjectRow struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Tags string `json:"tags"` DirPath string `json:"dir_path"` AppsCount int `json:"apps_count"` AnalysesCount int `json:"analyses_count"` VaultsCount int `json:"vaults_count"` } var out []ProjectRow for rows.Next() { var p ProjectRow if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.Tags, &p.DirPath, &p.AppsCount, &p.AnalysesCount, &p.VaultsCount); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } out = append(out, p) } if out == nil { out = []ProjectRow{} } type Orphans struct { Apps int `json:"apps"` Analyses int `json:"analyses"` Vaults int `json:"vaults"` } var orphans Orphans _ = db.QueryRowContext(ctx, `SELECT (SELECT COUNT(*) FROM apps WHERE project_id = '' OR project_id IS NULL), (SELECT COUNT(*) FROM analysis WHERE project_id = '' OR project_id IS NULL), (SELECT COUNT(*) FROM vaults WHERE project_id = '' OR project_id IS NULL)`, ).Scan(&orphans.Apps, &orphans.Analyses, &orphans.Vaults) writeJSON(w, http.StatusOK, map[string]any{ "projects": out, "orphans": orphans, }) } // scanAll corre una query con 0 o 1 arg y devuelve rows genericas [[any]]. func scanAll(ctx context.Context, db *sql.DB, query string, arg any) ([][]any, []string, error) { var rows *sql.Rows var err error if arg == nil { rows, err = db.QueryContext(ctx, query) } else { rows, err = db.QueryContext(ctx, query, arg) } if err != nil { return nil, nil, err } defer rows.Close() cols, err := rows.Columns() if err != nil { return nil, nil, err } var out [][]any for rows.Next() { vals := make([]any, len(cols)) ptrs := make([]any, len(cols)) for i := range vals { ptrs[i] = &vals[i] } if err := rows.Scan(ptrs...); err != nil { return nil, nil, err } for i, v := range vals { if b, ok := v.([]byte); ok { vals[i] = string(b) } } out = append(out, vals) } if out == nil { out = [][]any{} } return out, cols, nil } // handleProjectDetail devuelve apps/analyses/vaults de un proyecto. // Si el id es "orphans", devuelve las entidades con project_id vacio. // GET /api/projects/{id} func (s *Server) handleProjectDetail(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if id == "" { writeError(w, http.StatusBadRequest, "project id required") return } db, err := s.pool.Get("registry") if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } ctx, cancel := context.WithTimeout(r.Context(), queryTimeout) defer cancel() var whereExpr string var arg any if id == "orphans" { whereExpr = "project_id = '' OR project_id IS NULL" arg = nil } else { whereExpr = "project_id = ?" arg = id } apps, appsCols, err := scanAll(ctx, db, `SELECT id, name, lang, domain, framework, description, dir_path FROM apps WHERE `+whereExpr+` ORDER BY name`, arg) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } analyses, analysesCols, err := scanAll(ctx, db, `SELECT id, name, lang, domain, description, dir_path FROM analysis WHERE `+whereExpr+` ORDER BY name`, arg) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } vaults, vaultsCols, err := scanAll(ctx, db, `SELECT id, name, path, symlink, description, tags FROM vaults WHERE `+whereExpr+` ORDER BY name`, arg) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } var projectMeta map[string]any if id != "orphans" { var name, desc, tags, dirPath string if err := db.QueryRowContext(ctx, `SELECT name, description, tags, dir_path FROM projects WHERE id = ?`, id, ).Scan(&name, &desc, &tags, &dirPath); err == nil { projectMeta = map[string]any{ "id": id, "name": name, "description": desc, "tags": tags, "dir_path": dirPath, } } } writeJSON(w, http.StatusOK, map[string]any{ "project": projectMeta, "apps": map[string]any{ "columns": appsCols, "rows": apps, }, "analyses": map[string]any{ "columns": analysesCols, "rows": analyses, }, "vaults": map[string]any{ "columns": vaultsCols, "rows": vaults, }, }) }