add region api routes

This commit is contained in:
Enot (ded) Skelly
2026-05-22 15:00:36 -07:00
parent 1f99278ff4
commit f04182f097
6 changed files with 189 additions and 3 deletions
+19
View File
@@ -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
-- ============================================================
+93
View File
@@ -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
+49
View File
@@ -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
}
+3 -1
View File
@@ -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)
+24 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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())
})