From beadf5dc62f70b5fa0da13729acfd867d95ad829 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Mon, 1 Jun 2026 16:23:56 -0700 Subject: [PATCH] feat: allow filter by not supporting multibyte path/trace depends on path resolution so will not return true yet for any nodes --- db/queries/queries.sql | 12 ++++++++++-- db/sqlc/queries.sql.go | 16 ++++++++++++---- db/store.go | 16 +++++++++++++--- internal/api/handlers/nodes.go | 31 ++++++++++++++++++++++++------- internal/api/handlers/packets.go | 5 +++-- internal/api/reader.go | 2 +- 6 files changed, 63 insertions(+), 19 deletions(-) diff --git a/db/queries/queries.sql b/db/queries/queries.sql index 689a403..03ff186 100644 --- a/db/queries/queries.sql +++ b/db/queries/queries.sql @@ -344,8 +344,16 @@ FROM nodes n LEFT JOIN node_iatas ni ON ni.node_id = n.id WHERE ($1 = 0 OR n.node_type = $1) - AND (NOT $3 OR n.supports_multibyte_paths = TRUE) - AND (NOT $4 OR n.supports_multibyte_traces = TRUE) + AND ( + $3::text = 'any' + OR ($3::text = 'true' AND n.supports_multibyte_paths = TRUE) + OR ($3::text = 'false' AND n.supports_multibyte_paths = FALSE) + ) + AND ( + $4::text = 'any' + OR ($4::text = 'true' AND n.supports_multibyte_traces = TRUE) + OR ($4::text = 'false' AND n.supports_multibyte_traces = FALSE) + ) AND ($5::bytea IS NULL OR n.public_key = $5) AND ($6 = '' OR n.name ILIKE '%' || $6 || '%') AND ($7::timestamptz IS NULL OR n.last_seen < $7) diff --git a/db/sqlc/queries.sql.go b/db/sqlc/queries.sql.go index 727f47e..f2b1121 100644 --- a/db/sqlc/queries.sql.go +++ b/db/sqlc/queries.sql.go @@ -1370,8 +1370,16 @@ FROM nodes n LEFT JOIN node_iatas ni ON ni.node_id = n.id WHERE ($1 = 0 OR n.node_type = $1) - AND (NOT $3 OR n.supports_multibyte_paths = TRUE) - AND (NOT $4 OR n.supports_multibyte_traces = TRUE) + AND ( + $3::text = 'any' + OR ($3::text = 'true' AND n.supports_multibyte_paths = TRUE) + OR ($3::text = 'false' AND n.supports_multibyte_paths = FALSE) + ) + AND ( + $4::text = 'any' + OR ($4::text = 'true' AND n.supports_multibyte_traces = TRUE) + OR ($4::text = 'false' AND n.supports_multibyte_traces = FALSE) + ) AND ($5::bytea IS NULL OR n.public_key = $5) AND ($6 = '' OR n.name ILIKE '%' || $6 || '%') AND ($7::timestamptz IS NULL OR n.last_seen < $7) @@ -1384,8 +1392,8 @@ LIMIT $8 type ListNodesParams struct { Column1 interface{} `json:"column_1"` Column2 interface{} `json:"column_2"` - Column3 interface{} `json:"column_3"` - Column4 interface{} `json:"column_4"` + Column3 string `json:"column_3"` + Column4 string `json:"column_4"` Column5 []byte `json:"column_5"` Column6 interface{} `json:"column_6"` Column7 pgtype.Timestamptz `json:"column_7"` diff --git a/db/store.go b/db/store.go index af28e16..6662605 100644 --- a/db/store.go +++ b/db/store.go @@ -818,7 +818,7 @@ 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, iata 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} @@ -826,8 +826,8 @@ func (s *Store) ListNodes(ctx context.Context, nodeType int16, iata string, supp rows, err := s.q.ListNodes(ctx, sqlc.ListNodesParams{ Column1: nodeType, Column2: iata, - Column3: supportsMultibytePaths, - Column4: supportsMultibyteTraces, + Column3: tristate(supportsMultibytePaths), + Column4: tristate(supportsMultibyteTraces), Column5: pubkey, Column6: name, Column7: cursorTS, @@ -1219,3 +1219,13 @@ func nullableUUID(id uuid.UUID) *uuid.UUID { } return &id } + +func tristate(b *bool) string { + if b == nil { + return "any" + } + if *b { + return "true" + } + return "false" +} diff --git a/internal/api/handlers/nodes.go b/internal/api/handlers/nodes.go index d3735ac..7fecf77 100644 --- a/internal/api/handlers/nodes.go +++ b/internal/api/handlers/nodes.go @@ -32,14 +32,14 @@ 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 IATA code (case-insensitive); comma-separated for multiple" // @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 to nodes with firmware >= 1.14.0" -// @Param supportsMultibyteTraces query bool false "Filter to nodes with firmware >= 1.11.0" +// @Param supportsMultibytePaths query string false "Multibyte path support filter: any (default), true, false" +// @Param supportsMultibyteTraces query string false "Multibyte trace support filter: any (default), true, false" // @Param cursor query int false "last_seen epoch ms of last item for pagination" // @Param limit query int false "Max results (default 50)" -// @Success 200 {object} object +// @Success 200 {object} api.Page[api.NodeSummary] // @Failure 500 {object} handlers.APIError // @Router /nodes [get] func listNodes(reader api.Reader) http.HandlerFunc { @@ -84,8 +84,24 @@ func listNodes(reader api.Reader) http.HandlerFunc { } iata := r.URL.Query().Get("iata") name := r.URL.Query().Get("name") - supportsMultibytePaths := r.URL.Query().Get("supportsMultibytePaths") == "true" - supportsMultibyteTraces := r.URL.Query().Get("supportsMultibyteTraces") == "true" + var supportsMultibytePaths *bool + if v := r.URL.Query().Get("supportsMultibytePaths"); v != "" { + b, err := strconv.ParseBool(v) + if err != nil { + respondError(w, http.StatusBadRequest, "invalid supportsMultibytePaths value") + return + } + supportsMultibytePaths = &b + } + var supportsMultibyteTraces *bool + if v := r.URL.Query().Get("supportsMultibyteTraces"); v != "" { + b, err := strconv.ParseBool(v) + if err != nil { + respondError(w, http.StatusBadRequest, "invalid supportsMultibyteTraces value") + return + } + supportsMultibytePaths = &b + } nodes, err := reader.ListNodes(r.Context(), nodeType, iata, supportsMultibytePaths, supportsMultibyteTraces, pubkey, name, cursor, limit) if err != nil { respondError(w, http.StatusInternalServerError, "internal server error") @@ -104,6 +120,7 @@ func listNodes(reader api.Reader) http.HandlerFunc { // @Success 200 {object} api.Node // @Failure 400 {object} handlers.APIError // @Failure 404 {object} handlers.APIError +// @Failure 500 {object} handlers.APIError // @Router /nodes/{nodeId} [get] func getNode(reader api.Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -129,7 +146,7 @@ func getNode(reader api.Reader) http.HandlerFunc { // @Param nodeId path string true "Node UUID" // @Param cursor query int false "Observation ID of last item for pagination" // @Param limit query int false "Max results (default 50)" -// @Success 200 {object} object +// @Success 200 {object} api.Page[api.NodeObservation] // @Failure 400 {object} handlers.APIError // @Failure 500 {object} handlers.APIError // @Router /nodes/{nodeId}/observations [get] diff --git a/internal/api/handlers/packets.go b/internal/api/handlers/packets.go index 24e32a3..d7564c0 100644 --- a/internal/api/handlers/packets.go +++ b/internal/api/handlers/packets.go @@ -29,12 +29,12 @@ func PacketsRouter(reader api.Reader) http.Handler { // @Param payloadType query int false "Filter by payload type integer" // @Param payloadTypeName query string false "Filter by payload type name (advert, grp_txt, txt_msg, trace, anon_req)" // @Param routeType query int false "Filter by route type (0=transport_flood, 1=flood, 2=direct, 3=transport_direct)" -// @Param iata query string false "Filter by latest observation IATA (case-insensitive)" +// @Param iata query string false "Filter by IATA code" // @Param since query int false "Filter by first_heard_at >= since (epoch ms)" // @Param until query int false "Filter by first_heard_at <= until (epoch ms)" // @Param cursor query int false "last_heard_at epoch ms of last item for pagination" // @Param limit query int false "Max results (default 50)" -// @Success 200 {object} object +// @Success 200 {object} api.Page[api.PacketSummary] // @Failure 500 {object} handlers.APIError // @Router /packets [get] func listPackets(reader api.Reader) http.HandlerFunc { @@ -116,6 +116,7 @@ func listPackets(reader api.Reader) http.HandlerFunc { // @Success 200 {object} api.Packet // @Failure 400 {object} handlers.APIError // @Failure 404 {object} handlers.APIError +// @Failure 500 {object} handlers.APIError // @Router /packets/{packetHash} [get] func getPacket(reader api.Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/reader.go b/internal/api/reader.go index 5f60122..383dc60 100644 --- a/internal/api/reader.go +++ b/internal/api/reader.go @@ -380,7 +380,7 @@ type Reader interface { // 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. // 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, iata 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.