From f04182f097974ebf4a5bc5aa9ce303646fe9fb50 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Fri, 22 May 2026 15:00:36 -0700 Subject: [PATCH] add region api routes --- db/queries/queries.sql | 19 +++++++ db/sqlc/queries.sql.go | 93 ++++++++++++++++++++++++++++++++ db/store.go | 49 +++++++++++++++++ internal/api/handlers/regions.go | 4 +- internal/api/reader.go | 25 ++++++++- internal/api/router/router.go | 2 +- 6 files changed, 189 insertions(+), 3 deletions(-) diff --git a/db/queries/queries.sql b/db/queries/queries.sql index bcf4c75..53177b1 100644 --- a/db/queries/queries.sql +++ b/db/queries/queries.sql @@ -277,6 +277,25 @@ WHERE ($1::char(3) IS NULL OR iata = $1) ORDER BY observation_count DESC LIMIT $2; +-- ============================================================ +-- REGIONS +-- ============================================================ + +-- name: ListRegions :many +SELECT id, slug, name +FROM regions +ORDER BY display_order, name; + +-- name: GetRegion :one +SELECT id, slug, name, description, center_lat, center_lng, zoom_level +FROM regions +WHERE id = $1; + +-- name: GetRegionIATAs :many +SELECT iata FROM region_iatas +WHERE region_id = $1 +ORDER BY iata; + -- ============================================================ -- HELPERS -- ============================================================ diff --git a/db/sqlc/queries.sql.go b/db/sqlc/queries.sql.go index 5e97818..5b57155 100644 --- a/db/sqlc/queries.sql.go +++ b/db/sqlc/queries.sql.go @@ -212,6 +212,63 @@ func (q *Queries) GetPacket(ctx context.Context, packetHash []byte) (Packet, err return i, err } +const getRegion = `-- name: GetRegion :one +SELECT id, slug, name, description, center_lat, center_lng, zoom_level +FROM regions +WHERE id = $1 +` + +type GetRegionRow struct { + ID int32 `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` + Description *string `json:"description"` + CenterLat *float64 `json:"center_lat"` + CenterLng *float64 `json:"center_lng"` + ZoomLevel *int32 `json:"zoom_level"` +} + +func (q *Queries) GetRegion(ctx context.Context, id int32) (GetRegionRow, error) { + row := q.db.QueryRow(ctx, getRegion, id) + var i GetRegionRow + err := row.Scan( + &i.ID, + &i.Slug, + &i.Name, + &i.Description, + &i.CenterLat, + &i.CenterLng, + &i.ZoomLevel, + ) + return i, err +} + +const getRegionIATAs = `-- name: GetRegionIATAs :many +SELECT iata FROM region_iatas +WHERE region_id = $1 +ORDER BY iata +` + +func (q *Queries) GetRegionIATAs(ctx context.Context, regionID int32) ([]string, error) { + rows, err := q.db.Query(ctx, getRegionIATAs, regionID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []string{} + for rows.Next() { + var iata string + if err := rows.Scan(&iata); err != nil { + return nil, err + } + items = append(items, iata) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getStatsOverview = `-- name: GetStatsOverview :one SELECT @@ -813,6 +870,42 @@ func (q *Queries) ListPacketsAfterID(ctx context.Context, arg ListPacketsAfterID return items, nil } +const listRegions = `-- name: ListRegions :many + +SELECT id, slug, name +FROM regions +ORDER BY display_order, name +` + +type ListRegionsRow struct { + ID int32 `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` +} + +// ============================================================ +// REGIONS +// ============================================================ +func (q *Queries) ListRegions(ctx context.Context) ([]ListRegionsRow, error) { + rows, err := q.db.Query(ctx, listRegions) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListRegionsRow{} + for rows.Next() { + var i ListRegionsRow + if err := rows.Scan(&i.ID, &i.Slug, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const resolvePathHashes = `-- name: ResolvePathHashes :many SELECT DISTINCT n.id diff --git a/db/store.go b/db/store.go index 78c2847..55494f5 100644 --- a/db/store.go +++ b/db/store.go @@ -246,3 +246,52 @@ func (s *Store) GetIATA(ctx context.Context, iata string) (*api.IATA, error) { Lng: i.ApproxLng, }, nil } + +// ListRegions returns a summary list of all regions ordered by display_order then name. +// Use GetRegion for full detail including associated IATAs. +func (s *Store) ListRegions(ctx context.Context) ([]api.RegionSummary, error) { + rows, err := s.q.ListRegions(ctx) + if err != nil { + return nil, err + } + regions := make([]api.RegionSummary, 0, len(rows)) + for _, v := range rows { + regions = append(regions, api.RegionSummary{ + ID: int(v.ID), + Slug: v.Slug, + Name: v.Name, + }) + } + return regions, nil +} + +// GetRegion returns full detail for a single region including its associated IATA codes. +// Returns nil, pgx.ErrNoRows if the region is not found. +func (s *Store) GetRegion(ctx context.Context, regionID int32) (*api.Region, error) { + region, err := s.q.GetRegion(ctx, regionID) + if err != nil { + return nil, err + } + result := api.Region{ + RegionSummary: api.RegionSummary{ + ID: int(region.ID), + Slug: region.Slug, + Name: region.Name, + }, + Description: region.Description, + CenterLat: region.CenterLat, + CenterLng: region.CenterLng, + } + var zoomLevel *int + if region.ZoomLevel != nil { + z := int(*region.ZoomLevel) + zoomLevel = &z + } + result.ZoomLevel = zoomLevel + iatas, err := s.q.GetRegionIATAs(ctx, regionID) + if err != nil { + return nil, err + } + result.IATAs = iatas + return &result, nil +} diff --git a/internal/api/handlers/regions.go b/internal/api/handlers/regions.go index ac2fa55..103cec4 100644 --- a/internal/api/handlers/regions.go +++ b/internal/api/handlers/regions.go @@ -3,6 +3,8 @@ package handlers import ( "net/http" + "tower/internal/api" + "github.com/go-chi/chi/v5" ) @@ -13,7 +15,7 @@ import ( // // Note: region creation and IATA assignment are managed via the server config // file, not the API (v1). These endpoints are read-only. -func RegionsRouter() http.Handler { +func RegionsRouter(reader api.Reader) http.Handler { r := chi.NewRouter() r.Get("/", ListRegions) diff --git a/internal/api/reader.go b/internal/api/reader.go index 85c2589..983661d 100644 --- a/internal/api/reader.go +++ b/internal/api/reader.go @@ -1,6 +1,23 @@ package api -import "context" +import ( + "context" +) + +type RegionSummary struct { + ID int `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` +} + +type Region struct { + RegionSummary + Description *string `json:"description,omitempty"` + CenterLat *float64 `json:"centerLat,omitempty"` + CenterLng *float64 `json:"centerLng,omitempty"` + ZoomLevel *int `json:"zoomLevel,omitempty"` + IATAs []string `json:"iatas"` +} // IATA represents a known airport/location code used to group observers and packets. // DisplayName, Lat and Lng are optional — they are set via config file override @@ -19,4 +36,10 @@ type Reader interface { // GetIATA returns a single IATA code by its 3-letter identifier. // Returns nil, error if the IATA code is not found. GetIATA(ctx context.Context, iata string) (*IATA, error) + // ListRegions returns a summary list of all regions ordered by display_order then name. + // Use GetRegion for full detail including associated IATAs. + ListRegions(ctx context.Context) ([]RegionSummary, error) + // GetRegion returns full detail for a single region including its associated IATA codes. + // Returns nil, pgx.ErrNoRows if the region is not found. + GetRegion(ctx context.Context, regionID int32) (*Region, error) } diff --git a/internal/api/router/router.go b/internal/api/router/router.go index 310cc9a..654a491 100644 --- a/internal/api/router/router.go +++ b/internal/api/router/router.go @@ -51,8 +51,8 @@ func New(h *hub.Hub, reader api.Reader) http.Handler { r.Mount("/nodes", handlers.NodesRouter()) r.Mount("/observers", handlers.ObserversRouter()) r.Mount("/channels", handlers.ChannelsRouter()) - r.Mount("/regions", handlers.RegionsRouter()) r.Mount("/iatas", handlers.IATAsRouter(reader)) + r.Mount("/regions", handlers.RegionsRouter(reader)) r.Mount("/stats", handlers.StatsRouter()) })