Files
sqlite_api/handlers_projects.go
T
Egutierrez af13fd849c feat: endpoints de mutacion y de projects
- 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>
2026-04-28 22:05:08 +02:00

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,
},
})
}