From cba1690470bb7f649bb6d457b2e5b646cecc00b9 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 3 Jun 2026 15:07:36 -0700 Subject: [PATCH] add region filter to observers --- db/queries/queries.sql | 10 +++++----- db/sqlc/queries.sql.go | 12 ++++++------ db/store.go | 5 +++-- docs/docs.go | 20 +++++++++++++++++++- docs/swagger.json | 20 +++++++++++++++++++- docs/swagger.yaml | 14 +++++++++++++- internal/api/handlers/observers.go | 17 ++++++++++++++--- internal/api/reader.go | 4 ++-- 8 files changed, 81 insertions(+), 21 deletions(-) diff --git a/db/queries/queries.sql b/db/queries/queries.sql index 5b2be2c..62c3545 100644 --- a/db/queries/queries.sql +++ b/db/queries/queries.sql @@ -89,11 +89,11 @@ COALESCE(( FROM observers o LEFT JOIN observer_brokers ob ON ob.observer_id = o.id WHERE - ($1 = '' OR ( - SELECT po.iata FROM packet_observations po - WHERE po.observer_id = o.id - ORDER BY po.heard_at DESC LIMIT 1 - ) ILIKE $1) + ($1::text = '' OR ( + SELECT po.iata FROM packet_observations po + WHERE po.observer_id = o.id + ORDER BY po.heard_at DESC LIMIT 1 + ) = ANY(string_to_array($1::text, ','))) AND ($2 = '' OR o.observer_type = $2) AND ($3 = '' OR ob.broker_name = $3) AND ($4 = '' OR CASE diff --git a/db/sqlc/queries.sql.go b/db/sqlc/queries.sql.go index 47af767..040a624 100644 --- a/db/sqlc/queries.sql.go +++ b/db/sqlc/queries.sql.go @@ -1745,11 +1745,11 @@ COALESCE(( FROM observers o LEFT JOIN observer_brokers ob ON ob.observer_id = o.id WHERE - ($1 = '' OR ( - SELECT po.iata FROM packet_observations po - WHERE po.observer_id = o.id - ORDER BY po.heard_at DESC LIMIT 1 - ) ILIKE $1) + ($1::text = '' OR ( + SELECT po.iata FROM packet_observations po + WHERE po.observer_id = o.id + ORDER BY po.heard_at DESC LIMIT 1 + ) = ANY(string_to_array($1::text, ','))) AND ($2 = '' OR o.observer_type = $2) AND ($3 = '' OR ob.broker_name = $3) AND ($4 = '' OR CASE @@ -1764,7 +1764,7 @@ LIMIT $7 ` type ListObserversParams struct { - Column1 interface{} `json:"column_1"` + Column1 string `json:"column_1"` Column2 interface{} `json:"column_2"` Column3 interface{} `json:"column_3"` Column4 interface{} `json:"column_4"` diff --git a/db/store.go b/db/store.go index dfb8c9e..ca52b7f 100644 --- a/db/store.go +++ b/db/store.go @@ -612,13 +612,14 @@ func (s *Store) ListChannelMessagesByHash(ctx context.Context, hash []byte, sinc // status is "online" or "offline" derived from last_status_at recency. // ListObservers returns a paginated list of observers with optional filters. // cursor is last_seen epoch ms of the last observer; pass 0 to start from the beginning. -func (s *Store) ListObservers(ctx context.Context, iata, observerType, broker, status, name string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) { +func (s *Store) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) { var cursorTS pgtype.Timestamptz if cursor > 0 { cursorTS = pgtype.Timestamptz{Time: time.UnixMilli(cursor), Valid: true} } + iataFilter := strings.Join(iatas, ",") params := sqlc.ListObserversParams{ - Column1: iata, + Column1: iataFilter, Column2: observerType, Column3: broker, Column4: status, diff --git a/docs/docs.go b/docs/docs.go index 81a257f..acbe0cc 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -545,10 +545,28 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "Filter by IATA code (case-insensitive)", + "description": "Filter by single IATA code (case-insensitive)", "name": "iata", "in": "query" }, + { + "type": "string", + "description": "Filter by multiple IATA codes, comma-separated e.g. YVR,YYJ", + "name": "iatas", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by region ID, expands to member IATAs", + "name": "regionId", + "in": "query" + }, + { + "type": "string", + "description": "Filter by region slug, expands to member IATAs", + "name": "region", + "in": "query" + }, { "type": "string", "description": "Filter by observer type (e.g. meshcoretomqtt, meshcore-ha)", diff --git a/docs/swagger.json b/docs/swagger.json index 6669fde..aa4f6bb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -543,10 +543,28 @@ "parameters": [ { "type": "string", - "description": "Filter by IATA code (case-insensitive)", + "description": "Filter by single IATA code (case-insensitive)", "name": "iata", "in": "query" }, + { + "type": "string", + "description": "Filter by multiple IATA codes, comma-separated e.g. YVR,YYJ", + "name": "iatas", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by region ID, expands to member IATAs", + "name": "regionId", + "in": "query" + }, + { + "type": "string", + "description": "Filter by region slug, expands to member IATAs", + "name": "region", + "in": "query" + }, { "type": "string", "description": "Filter by observer type (e.g. meshcoretomqtt, meshcore-ha)", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 282d5dc..10c94da 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1029,10 +1029,22 @@ paths: /observers: get: parameters: - - description: Filter by IATA code (case-insensitive) + - description: Filter by single IATA code (case-insensitive) in: query name: iata type: string + - description: Filter by multiple IATA codes, comma-separated e.g. YVR,YYJ + in: query + name: iatas + type: string + - description: Filter by region ID, expands to member IATAs + in: query + name: regionId + type: integer + - description: Filter by region slug, expands to member IATAs + in: query + name: region + type: string - description: Filter by observer type (e.g. meshcoretomqtt, meshcore-ha) in: query name: type diff --git a/internal/api/handlers/observers.go b/internal/api/handlers/observers.go index ea0616d..7b3e5b8 100644 --- a/internal/api/handlers/observers.go +++ b/internal/api/handlers/observers.go @@ -32,7 +32,10 @@ func ObserversRouter(reader api.Reader) http.Handler { // @Summary List observers // @Tags Observers // @Produce json -// @Param iata query string false "Filter by IATA code (case-insensitive)" +// @Param iata query string false "Filter by single IATA code (case-insensitive)" +// @Param iatas query string false "Filter by multiple IATA codes, comma-separated e.g. YVR,YYJ" +// @Param regionId query int false "Filter by region ID, expands to member IATAs" +// @Param region query string false "Filter by region slug, expands to member IATAs" // @Param type query string false "Filter by observer type (e.g. meshcoretomqtt, meshcore-ha)" // @Param broker query string false "Filter by broker name" // @Param status query string false "Filter by status (online or offline)" @@ -45,7 +48,6 @@ func ObserversRouter(reader api.Reader) http.Handler { // @Router /observers [get] func listObservers(reader api.Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - iata := r.URL.Query().Get("iata") observerType := r.URL.Query().Get("type") broker := r.URL.Query().Get("broker") name := r.URL.Query().Get("name") @@ -68,7 +70,16 @@ func listObservers(reader api.Reader) http.HandlerFunc { } limit = int32(l) } - observers, err := reader.ListObservers(r.Context(), iata, observerType, broker, status, name, cursor, limit) + iatas := parseIATAs(r) + if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" { + regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader) + if err != nil { + respondError(w, http.StatusBadRequest, err.Error()) + return + } + iatas = append(iatas, regionIATAs...) + } + observers, err := reader.ListObservers(r.Context(), iatas, observerType, broker, status, name, cursor, limit) if err != nil { respondError(w, http.StatusInternalServerError, "failed to get list of observers") return diff --git a/internal/api/reader.go b/internal/api/reader.go index b435404..25150ca 100644 --- a/internal/api/reader.go +++ b/internal/api/reader.go @@ -396,10 +396,10 @@ type Reader interface { // Pass cursor=0 to start from the beginning. ListChannelMessagesByHash(ctx context.Context, hash []byte, since time.Time, limit int32, iata string, cursor int64) (Page[ChannelMessage], error) // ListObservers returns a paginated list of observers with optional filters. - // All filter params are optional — pass empty string to skip a filter. + // All filter params are optional — pass empty string or nil to skip a filter. // status is "online" or "offline" derived from last_status_at recency. // cursor is last_seen epoch ms of the last observer; pass 0 to start from the beginning. - ListObservers(ctx context.Context, iata, observerType, broker, status, name string, cursor int64, limit int32) (Page[ObserverSummary], error) + ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name string, cursor int64, limit int32) (Page[ObserverSummary], error) // GetObserver returns full detail for a single observer by UUID. // Returns nil, pgx.ErrNoRows if the observer is not found. GetObserver(ctx context.Context, observerID uuid.UUID) (*Observer, error)