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>
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user