add region filter to nodes

This commit is contained in:
Enot (ded) Skelly
2026-06-03 14:59:26 -07:00
parent a49c64b4b5
commit 67e60de76f
8 changed files with 73 additions and 13 deletions
+1 -1
View File
@@ -348,7 +348,7 @@ FROM nodes n
LEFT JOIN node_iatas ni ON ni.node_id = n.id
WHERE
($1 = 0 OR n.node_type = $1)
AND ($2 = '' OR n.id IN (SELECT node_id FROM node_iatas WHERE iata ILIKE $2))
AND ($2::text = '' OR n.id IN (SELECT node_id FROM node_iatas WHERE iata = ANY(string_to_array($2::text, ','))))
AND (
$3::text = 'any'
OR ($3::text = 'true' AND n.supports_multibyte_paths = TRUE)
+2 -2
View File
@@ -1439,7 +1439,7 @@ FROM nodes n
LEFT JOIN node_iatas ni ON ni.node_id = n.id
WHERE
($1 = 0 OR n.node_type = $1)
AND ($2 = '' OR n.id IN (SELECT node_id FROM node_iatas WHERE iata ILIKE $2))
AND ($2::text = '' OR n.id IN (SELECT node_id FROM node_iatas WHERE iata = ANY(string_to_array($2::text, ','))))
AND (
$3::text = 'any'
OR ($3::text = 'true' AND n.supports_multibyte_paths = TRUE)
@@ -1460,7 +1460,7 @@ LIMIT $8
type ListNodesParams struct {
Column1 interface{} `json:"column_1"`
Column2 interface{} `json:"column_2"`
Column2 string `json:"column_2"`
Column3 string `json:"column_3"`
Column4 string `json:"column_4"`
Column5 []byte `json:"column_5"`
+3 -2
View File
@@ -876,14 +876,15 @@ func (s *Store) ListObserverAdverts(ctx context.Context, observerID uuid.UUID, c
// ListNodes returns a paginated list of nodes with optional filters.
// Pass 0 for nodeType, empty string for iata/name, nil for pubkey to skip those filters.
// cursor is last_seen epoch ms; pass 0 to start from the beginning.
func (s *Store) ListNodes(ctx context.Context, nodeType int16, iata string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name string, cursor int64, limit int32) (api.Page[api.NodeSummary], error) {
func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name string, cursor int64, limit int32) (api.Page[api.NodeSummary], error) {
var cursorTS pgtype.Timestamptz
if cursor > 0 {
cursorTS = pgtype.Timestamptz{Time: time.UnixMilli(cursor), Valid: true}
}
iataFilter := strings.Join(iatas, ",")
rows, err := s.q.ListNodes(ctx, sqlc.ListNodesParams{
Column1: nodeType,
Column2: iata,
Column2: iataFilter,
Column3: tristate(supportsMultibytePaths),
Column4: tristate(supportsMultibyteTraces),
Column5: pubkey,
+19 -1
View File
@@ -360,10 +360,28 @@ const docTemplate = `{
},
{
"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": "Partial case-insensitive name match",
+19 -1
View File
@@ -358,10 +358,28 @@
},
{
"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": "Partial case-insensitive name match",
+13 -1
View File
@@ -908,10 +908,22 @@ paths:
in: query
name: typeName
type: string
- 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: Partial case-insensitive name match
in: query
name: name
+14 -3
View File
@@ -32,7 +32,10 @@ func NodesRouter(reader api.Reader) http.Handler {
// @Produce json
// @Param type query int false "Node type integer (1=companion, 2=repeater, 3=room_server, 4=sensor)"
// @Param typeName query string false "Node type name (companion, repeater, room_server, sensor)"
// @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 name query string false "Partial case-insensitive name match"
// @Param pubkey query string false "Exact public key match (hex)"
// @Param supportsMultibytePaths query bool false "Filter by multibyte path support (true/false); omit for no filter"
@@ -83,7 +86,15 @@ func listNodes(reader api.Reader) http.HandlerFunc {
}
pubkey = b
}
iata := r.URL.Query().Get("iata")
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...)
}
name := r.URL.Query().Get("name")
var supportsMultibytePaths *bool
if v := r.URL.Query().Get("supportsMultibytePaths"); v != "" {
@@ -103,7 +114,7 @@ func listNodes(reader api.Reader) http.HandlerFunc {
}
supportsMultibyteTraces = &b
}
nodes, err := reader.ListNodes(r.Context(), nodeType, iata, supportsMultibytePaths, supportsMultibyteTraces, pubkey, name, cursor, limit)
nodes, err := reader.ListNodes(r.Context(), nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, name, cursor, limit)
if err != nil {
respondError(w, http.StatusInternalServerError, "internal server error")
return
+2 -2
View File
@@ -412,9 +412,9 @@ type Reader interface {
// Pass cursor=0 to start from the beginning.
ListObserverAdverts(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (Page[AdvertObservation], error)
// ListNodes returns a paginated list of nodes with optional filters.
// Pass 0 for nodeType, empty string for iata, nil for pubkey to skip those filters.
// Pass 0 for nodeType, nil iatas, nil for pubkey to skip those filters.
// cursor is last_seen epoch ms; pass 0 to start from the beginning.
ListNodes(ctx context.Context, nodeType int16, iata string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name string, cursor int64, limit int32) (Page[NodeSummary], error)
ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name string, cursor int64, limit int32) (Page[NodeSummary], error)
// GetNode returns full detail for a single node by UUID.
// Returns nil, pgx.ErrNoRows if the node is not found.