af13fd849c
- handlers_mutations.go: POST add_app/add_analysis/add_vault/reindex - handlers_projects.go: GET projects y project detail (apps/analysis/vaults nested) - handlers.go + main.go: cablear nuevas rutas - handlers_test.go: ajustes minimos - app.md: documentar endpoints v0.2 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
205 lines
5.2 KiB
Go
205 lines
5.2 KiB
Go
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,
|
|
},
|
|
})
|
|
}
|