package main import ( "fmt" "net/http" "strconv" "strings" graphqueries "visualizador_instanciados/backend_go/graph_queries" selectionqueries "visualizador_instanciados/backend_go/selection_queries" ) type APIServer struct { cfg Config sparql *AnzoGraphClient snapshots *GraphSnapshotService } func (s *APIServer) handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/api/health", s.handleHealth) mux.HandleFunc("/api/stats", s.handleStats) mux.HandleFunc("/api/sparql", s.handleSparql) mux.HandleFunc("/api/graph", s.handleGraph) mux.HandleFunc("/api/graph_queries", s.handleGraphQueries) mux.HandleFunc("/api/selection_queries", s.handleSelectionQueries) mux.HandleFunc("/api/selection_query", s.handleSelectionQuery) mux.HandleFunc("/api/neighbors", s.handleNeighbors) return s.corsMiddleware(mux) } func (s *APIServer) corsMiddleware(next http.Handler) http.Handler { origins := s.cfg.corsOriginList() allowAll := len(origins) == 1 && origins[0] == "*" allowed := make(map[string]struct{}, len(origins)) for _, o := range origins { allowed[o] = struct{}{} } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin != "" { if allowAll { w.Header().Set("Access-Control-Allow-Origin", "*") } else if _, ok := allowed[origin]; ok { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Add("Vary", "Origin") } w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "*") } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func (s *APIServer) handleHealth(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } writeJSON(w, http.StatusOK, HealthResponse{Status: "ok"}) } func (s *APIServer) handleStats(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } ctx := r.Context() snap, err := s.snapshots.Get(ctx, s.cfg.DefaultNodeLimit, s.cfg.DefaultEdgeLimit, graphqueries.DefaultID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } endpoint := snap.Meta.SparqlEndpoint writeJSON(w, http.StatusOK, StatsResponse{ Backend: snap.Meta.Backend, TTLPath: snap.Meta.TTLPath, SparqlEndpoint: &endpoint, ParsedTriples: len(snap.Edges), Nodes: len(snap.Nodes), Edges: len(snap.Edges), }) } func (s *APIServer) handleSparql(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } var req SparqlQueryRequest if err := decodeJSON(r.Body, &req); err != nil || strings.TrimSpace(req.Query) == "" { writeError(w, http.StatusUnprocessableEntity, "invalid request body") return } raw, err := s.sparql.Query(r.Context(), req.Query) if err != nil { writeError(w, http.StatusBadGateway, err.Error()) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write(raw) } func (s *APIServer) handleGraph(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } nodeLimit, err := intQuery(r, "node_limit", s.cfg.DefaultNodeLimit) if err != nil || nodeLimit < 1 || nodeLimit > s.cfg.MaxNodeLimit { writeError(w, http.StatusUnprocessableEntity, fmt.Sprintf("node_limit must be between 1 and %d", s.cfg.MaxNodeLimit)) return } edgeLimit, err := intQuery(r, "edge_limit", s.cfg.DefaultEdgeLimit) if err != nil || edgeLimit < 1 || edgeLimit > s.cfg.MaxEdgeLimit { writeError(w, http.StatusUnprocessableEntity, fmt.Sprintf("edge_limit must be between 1 and %d", s.cfg.MaxEdgeLimit)) return } graphQueryID := strings.TrimSpace(r.URL.Query().Get("graph_query_id")) if graphQueryID == "" { graphQueryID = graphqueries.DefaultID } if _, ok := graphqueries.Get(graphQueryID); !ok { writeError(w, http.StatusUnprocessableEntity, "unknown graph_query_id") return } snap, err := s.snapshots.Get(r.Context(), nodeLimit, edgeLimit, graphQueryID) if err != nil { if _, ok := err.(*CycleError); ok { writeError(w, http.StatusUnprocessableEntity, err.Error()) return } writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, snap) } func (s *APIServer) handleGraphQueries(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } writeJSON(w, http.StatusOK, graphqueries.List()) } func (s *APIServer) handleSelectionQueries(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } writeJSON(w, http.StatusOK, selectionqueries.List()) } func (s *APIServer) handleSelectionQuery(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } var req SelectionQueryRequest if err := decodeJSON(r.Body, &req); err != nil || strings.TrimSpace(req.QueryID) == "" { writeError(w, http.StatusUnprocessableEntity, "invalid request body") return } if _, ok := selectionqueries.Get(req.QueryID); !ok { writeError(w, http.StatusUnprocessableEntity, "unknown query_id") return } if len(req.SelectedIDs) == 0 { writeJSON(w, http.StatusOK, SelectionQueryResponse{ QueryID: req.QueryID, SelectedIDs: req.SelectedIDs, NeighborIDs: []int{}, }) return } graphQueryID := graphqueries.DefaultID if req.GraphQueryID != nil && strings.TrimSpace(*req.GraphQueryID) != "" { graphQueryID = strings.TrimSpace(*req.GraphQueryID) } if _, ok := graphqueries.Get(graphQueryID); !ok { writeError(w, http.StatusUnprocessableEntity, "unknown graph_query_id") return } nodeLimit := s.cfg.DefaultNodeLimit edgeLimit := s.cfg.DefaultEdgeLimit if req.NodeLimit != nil { nodeLimit = *req.NodeLimit } if req.EdgeLimit != nil { edgeLimit = *req.EdgeLimit } if nodeLimit < 1 || nodeLimit > s.cfg.MaxNodeLimit || edgeLimit < 1 || edgeLimit > s.cfg.MaxEdgeLimit { writeError(w, http.StatusUnprocessableEntity, "invalid node_limit/edge_limit") return } snap, err := s.snapshots.Get(r.Context(), nodeLimit, edgeLimit, graphQueryID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } ids, err := runSelectionQuery(r.Context(), s.sparql, snap, req.QueryID, req.SelectedIDs, s.cfg.IncludeBNodes) if err != nil { writeError(w, http.StatusBadGateway, err.Error()) return } writeJSON(w, http.StatusOK, SelectionQueryResponse{ QueryID: req.QueryID, SelectedIDs: req.SelectedIDs, NeighborIDs: ids, }) } func (s *APIServer) handleNeighbors(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } var req NeighborsRequest if err := decodeJSON(r.Body, &req); err != nil { writeError(w, http.StatusUnprocessableEntity, "invalid request body") return } if len(req.SelectedIDs) == 0 { writeJSON(w, http.StatusOK, NeighborsResponse{SelectedIDs: req.SelectedIDs, NeighborIDs: []int{}}) return } graphQueryID := graphqueries.DefaultID if req.GraphQueryID != nil && strings.TrimSpace(*req.GraphQueryID) != "" { graphQueryID = strings.TrimSpace(*req.GraphQueryID) } if _, ok := graphqueries.Get(graphQueryID); !ok { writeError(w, http.StatusUnprocessableEntity, "unknown graph_query_id") return } nodeLimit := s.cfg.DefaultNodeLimit edgeLimit := s.cfg.DefaultEdgeLimit if req.NodeLimit != nil { nodeLimit = *req.NodeLimit } if req.EdgeLimit != nil { edgeLimit = *req.EdgeLimit } if nodeLimit < 1 || nodeLimit > s.cfg.MaxNodeLimit || edgeLimit < 1 || edgeLimit > s.cfg.MaxEdgeLimit { writeError(w, http.StatusUnprocessableEntity, "invalid node_limit/edge_limit") return } snap, err := s.snapshots.Get(r.Context(), nodeLimit, edgeLimit, graphQueryID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } nbrs, err := runSelectionQuery(r.Context(), s.sparql, snap, "neighbors", req.SelectedIDs, s.cfg.IncludeBNodes) if err != nil { writeError(w, http.StatusBadGateway, err.Error()) return } writeJSON(w, http.StatusOK, NeighborsResponse{SelectedIDs: req.SelectedIDs, NeighborIDs: nbrs}) } func intQuery(r *http.Request, name string, def int) (int, error) { raw := strings.TrimSpace(r.URL.Query().Get(name)) if raw == "" { return def, nil } n, err := strconv.Atoi(raw) if err != nil { return 0, err } return n, nil }