feat: allow filter by not supporting multibyte path/trace

depends on path resolution so will not return true yet for any nodes
This commit is contained in:
Enot (ded) Skelly
2026-06-01 16:23:56 -07:00
parent e3084ce444
commit beadf5dc62
6 changed files with 63 additions and 19 deletions
+10 -2
View File
@@ -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)
+12 -4
View File
@@ -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"`
+13 -3
View File
@@ -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"
}
+24 -7
View File
@@ -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]
+3 -2
View File
@@ -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) {
+1 -1
View File
@@ -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.