From e9b896004862679a78c362523ab91345b9fdbb8f Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Thu, 2 Jul 2026 21:48:41 -0400 Subject: [PATCH] api: add server and implement getBacklinksCount --- cmd/asterism/main.go | 9 +++++++++ internal/api/backlinks.go | 32 ++++++++++++++++++++++++++++++++ internal/api/server.go | 18 ++++++++++++++++++ internal/api/source.go | 27 +++++++++++++++++++++++++++ internal/store/query.go | 20 ++++++++++++++++++++ 5 files changed, 106 insertions(+) create mode 100644 internal/api/backlinks.go create mode 100644 internal/api/server.go create mode 100644 internal/api/source.go create mode 100644 internal/store/query.go diff --git a/cmd/asterism/main.go b/cmd/asterism/main.go index 8efeceb..764deeb 100644 --- a/cmd/asterism/main.go +++ b/cmd/asterism/main.go @@ -9,6 +9,7 @@ import ( "github.com/gorilla/websocket" + "github.com/alyraffauf/asterism/internal/api" "github.com/alyraffauf/asterism/internal/firehose" "github.com/alyraffauf/asterism/internal/store" ) @@ -50,6 +51,14 @@ func main() { panic(err) } + server := &api.Server{Store: linkStore} + go func() { + if err := server.Run(":8081"); err != nil { + panic(err) + } + }() + defer conn.Close() + consumer := &firehose.Consumer{ WantedCollections: parseCollections(*collectionsFlag), Store: linkStore, diff --git a/internal/api/backlinks.go b/internal/api/backlinks.go new file mode 100644 index 0000000..2820a6d --- /dev/null +++ b/internal/api/backlinks.go @@ -0,0 +1,32 @@ +package api + +import ( + "encoding/json" + "log" + "net/http" +) + +type backlinksCountResponse struct { + Total uint64 `json:"total"` +} + +func (s *Server) GetBacklinksCount(w http.ResponseWriter, r *http.Request) { + subject := r.URL.Query().Get("subject") + source := r.URL.Query().Get("source") + + collection, path, err := parseSource(source) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + total, err := s.Store.CountBacklinks(r.Context(), subject, collection, path) + if err != nil { + log.Println("count backlinks:", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(backlinksCountResponse{Total: total}) +} diff --git a/internal/api/server.go b/internal/api/server.go new file mode 100644 index 0000000..6f6f1ba --- /dev/null +++ b/internal/api/server.go @@ -0,0 +1,18 @@ +package api + +import ( + "net/http" + + "github.com/alyraffauf/asterism/internal/store" +) + +type Server struct { + Store *store.Store +} + +func (s *Server) Run(addr string) error { + mux := http.NewServeMux() + mux.HandleFunc("GET /xrpc/blue.microcosm.links.getBacklinksCount", s.GetBacklinksCount) + + return http.ListenAndServe(addr, mux) +} diff --git a/internal/api/source.go b/internal/api/source.go new file mode 100644 index 0000000..17f437a --- /dev/null +++ b/internal/api/source.go @@ -0,0 +1,27 @@ +package api + +import ( + "fmt" + "strings" +) + +func parseSource(raw string) (collection string, path string, err error) { + collection, rawPath, ok := strings.Cut(raw, ":") + if !ok { + return "", "", fmt.Errorf("source must contain a ':' separator") + } + if collection == "" { + return "", "", fmt.Errorf("source is missing a collection before ':'") + } + + switch { + case rawPath == "": + return "", "", fmt.Errorf("source is missing a path after ':'") + case rawPath == ".": + return collection, ".", nil + case strings.HasPrefix(rawPath, "."): + return "", "", fmt.Errorf("source path must not start with '.'") + default: + return collection, "." + rawPath, nil + } +} diff --git a/internal/store/query.go b/internal/store/query.go new file mode 100644 index 0000000..5c00aba --- /dev/null +++ b/internal/store/query.go @@ -0,0 +1,20 @@ +package store + +import ( + "context" + "fmt" +) + +func (s *Store) CountBacklinks(ctx context.Context, target, collection, fieldPath string) (uint64, error) { + var total uint64 + + err := s.db.QueryRowContext(ctx, + `SELECT COUNT(*) FROM links WHERE target = ? AND collection = ? AND field_path = ?`, + target, collection, fieldPath, + ).Scan(&total) + if err != nil { + return 0, fmt.Errorf("count backlinks: %w", err) + } + + return total, nil +} -- 2.51.2