diff --git a/db/nodes.go b/db/nodes.go index 61e50d7..26a40c0 100644 --- a/db/nodes.go +++ b/db/nodes.go @@ -186,6 +186,24 @@ func (s *Store) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error return node, nil } +func (s *Store) GetNodesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*api.ResolvedNode, error) { + rows, err := s.q.GetNodesByIDs(ctx, ids) + if err != nil { + return nil, err + } + result := make(map[uuid.UUID]*api.ResolvedNode, len(rows)) + for _, r := range rows { + result[r.ID] = &api.ResolvedNode{ + ID: r.ID, + Name: r.Name, + PublicKey: hex.EncodeToString(r.PublicKey), + Latitude: r.Latitude, + Longitude: r.Longitude, + } + } + return result, nil +} + func (s *Store) GetNodeNeighbors(ctx context.Context, nodeID uuid.UUID) ([]api.NodeNeighbor, error) { rows, err := s.q.GetNodeNeighbors(ctx, nodeID) if err != nil { diff --git a/db/queries/queries.sql b/db/queries/queries.sql index 292de5e..7971830 100644 --- a/db/queries/queries.sql +++ b/db/queries/queries.sql @@ -473,6 +473,10 @@ FROM nodes n LEFT JOIN transport_scopes ts ON ts.id = n.default_scope_id WHERE n.id = $1; +-- name: GetNodesByIDs :many +SELECT id, public_key, name, latitude, longitude +FROM nodes +WHERE id = ANY($1::uuid[]); -- name: ListNodes :many SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_seen, diff --git a/db/routes.go b/db/routes.go index 14e7f62..45da773 100644 --- a/db/routes.go +++ b/db/routes.go @@ -34,7 +34,12 @@ func (s *Store) ListKnownRoutes(ctx context.Context, iata string, hopCount int32 if err != nil { return nil, err } - return toKnownRoutes(rows), nil + ids := collectNodeIDs(rows) + nodes, err := s.GetNodesByIDs(ctx, ids) + if err != nil { + return nil, err + } + return toKnownRoutes(rows, nodes), nil } func (s *Store) SearchKnownRoutes(ctx context.Context, iata, fromHash, toHash string) ([]api.KnownRoute, error) { @@ -54,9 +59,13 @@ func (s *Store) SearchKnownRoutes(ctx context.Context, iata, fromHash, toHash st if err != nil { return nil, err } + ids := collectNodeIDs(rows) + nodes, err := s.GetNodesByIDs(ctx, ids) + if err != nil { + return nil, err + } items := make([]api.KnownRoute, 0, len(rows)) for _, r := range rows { - // find positions and slice to the subsequence fromPos, toPos := -1, -1 for i, h := range r.HashPrefix { if fromPos == -1 && hex.EncodeToString(h) == fromHash { @@ -74,7 +83,10 @@ func (s *Store) SearchKnownRoutes(ctx context.Context, iata, fromHash, toHash st hashPrefix := r.HashPrefix[fromPos : toPos+1] hops := make([]api.RouteHop, 0, len(nodeIDs)) for i, nodeID := range nodeIDs { - hop := api.RouteHop{NodeID: nodeID} + hop := api.RouteHop{ + NodeID: nodeID, + Node: nodes[nodeID], + } if i < len(hashPrefix) { hop.HashBytes = hex.EncodeToString(hashPrefix[i]) } @@ -101,7 +113,12 @@ func (s *Store) GetKnownRoutesByNode(ctx context.Context, iata string, nodeID uu if err != nil { return nil, err } - return toKnownRoutes(rows), nil + ids := collectNodeIDs(rows) + nodes, err := s.GetNodesByIDs(ctx, ids) + if err != nil { + return nil, err + } + return toKnownRoutes(rows, nodes), nil } func (s *Store) GetCrossIATANeighbors(ctx context.Context, nodeID uuid.UUID, iata string) ([]api.NodeNeighbor, error) { @@ -249,13 +266,14 @@ func extractFromNode(hops []api.RouteHop, nodeID uuid.UUID) []api.RouteHop { return hops } -func toKnownRoutes(rows []sqlc.KnownRoute) []api.KnownRoute { +func toKnownRoutes(rows []sqlc.KnownRoute, nodes map[uuid.UUID]*api.ResolvedNode) []api.KnownRoute { items := make([]api.KnownRoute, 0, len(rows)) for _, r := range rows { hops := make([]api.RouteHop, 0, len(r.NodeIds)) for i, nodeID := range r.NodeIds { hop := api.RouteHop{ NodeID: nodeID, + Node: nodes[nodeID], } if i < len(r.HashPrefix) { hop.HashBytes = hex.EncodeToString(r.HashPrefix[i]) @@ -274,3 +292,17 @@ func toKnownRoutes(rows []sqlc.KnownRoute) []api.KnownRoute { } return items } + +func collectNodeIDs(rows []sqlc.KnownRoute) []uuid.UUID { + seen := make(map[uuid.UUID]struct{}) + var ids []uuid.UUID + for _, r := range rows { + for _, id := range r.NodeIds { + if _, ok := seen[id]; !ok { + seen[id] = struct{}{} + ids = append(ids, id) + } + } + } + return ids +} diff --git a/db/sqlc/queries.sql.go b/db/sqlc/queries.sql.go index 2185dc5..3360aa0 100644 --- a/db/sqlc/queries.sql.go +++ b/db/sqlc/queries.sql.go @@ -513,6 +513,46 @@ func (q *Queries) GetNodeNeighbors(ctx context.Context, nodeID uuid.UUID) ([]Get return items, nil } +const getNodesByIDs = `-- name: GetNodesByIDs :many +SELECT id, public_key, name, latitude, longitude +FROM nodes +WHERE id = ANY($1::uuid[]) +` + +type GetNodesByIDsRow struct { + ID uuid.UUID `json:"id"` + PublicKey []byte `json:"public_key"` + Name *string `json:"name"` + Latitude *float64 `json:"latitude"` + Longitude *float64 `json:"longitude"` +} + +func (q *Queries) GetNodesByIDs(ctx context.Context, dollar_1 []uuid.UUID) ([]GetNodesByIDsRow, error) { + rows, err := q.db.Query(ctx, getNodesByIDs, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetNodesByIDsRow{} + for rows.Next() { + var i GetNodesByIDsRow + if err := rows.Scan( + &i.ID, + &i.PublicKey, + &i.Name, + &i.Latitude, + &i.Longitude, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getObserverBrokers = `-- name: GetObserverBrokers :many SELECT broker_name, last_seen, last_packet_at FROM observer_brokers diff --git a/internal/api/reader.go b/internal/api/reader.go index 2c8e795..3e214a5 100644 --- a/internal/api/reader.go +++ b/internal/api/reader.go @@ -86,6 +86,8 @@ type Reader interface { // GetNode returns full detail for a single node by UUID. // Returns nil, pgx.ErrNoRows if the node is not found. GetNode(ctx context.Context, nodeID uuid.UUID) (*Node, error) + // GetNodesByIDs returns a map of node ID to resolved node details for the given IDs. + GetNodesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*ResolvedNode, error) // ListNodeObservations returns a paginated list of packet observations originating from a node. // Pass cursor=0 to start from the beginning. ListNodeObservations(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (Page[PacketObservationSummary], error)