From ad8e41085638a4d67d669ea2f6ea3a762f5d54a3 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Thu, 11 Jun 2026 13:47:58 -0700 Subject: [PATCH] feat(api): add knownNeighborCount to nodesummary and publicKey to node neighbor --- db/nodes.go | 54 +++++++++++++++++++++++++++--------------- db/queries/queries.sql | 8 ++++--- db/sqlc/queries.sql.go | 42 +++++++++++++++++++------------- docs/docs.go | 9 +++++++ docs/swagger.json | 9 +++++++ docs/swagger.yaml | 6 +++++ internal/api/nodes.go | 26 ++++++++++---------- 7 files changed, 103 insertions(+), 51 deletions(-) diff --git a/db/nodes.go b/db/nodes.go index ff8a162..fb4353c 100644 --- a/db/nodes.go +++ b/db/nodes.go @@ -106,15 +106,16 @@ func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, s items := make([]api.NodeSummary, 0, len(rows)) for _, v := range rows { node := api.NodeSummary{ - ID: v.ID, - PublicKey: hex.EncodeToString(v.PublicKey), - NodeType: v.NodeType, - NodeTypeName: api.NodeTypeName(v.NodeType), - Name: v.Name, - Latitude: v.Latitude, - Longitude: v.Longitude, - IsObserver: v.IsObserver, - ObserverID: nullableUUID(v.ObserverID), + ID: v.ID, + PublicKey: hex.EncodeToString(v.PublicKey), + NodeType: v.NodeType, + NodeTypeName: api.NodeTypeName(v.NodeType), + Name: v.Name, + Latitude: v.Latitude, + Longitude: v.Longitude, + IsObserver: v.IsObserver, + ObserverID: nullableUUID(v.ObserverID), + KnownNeighborCount: v.KnownNeighborCount, } if len(v.Iatas) > 0 { if err := json.Unmarshal(v.Iatas, &node.IATAs); err != nil { @@ -147,16 +148,17 @@ func (s *Store) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error } node := &api.Node{ NodeSummary: api.NodeSummary{ - ID: row.ID, - PublicKey: hex.EncodeToString(row.PublicKey), - NodeType: row.NodeType, - NodeTypeName: api.NodeTypeName(row.NodeType), - Name: row.Name, - Latitude: row.Latitude, - Longitude: row.Longitude, - IsObserver: row.IsObserver, - ObserverID: nullableUUID(row.ObserverID), - DefaultScope: row.DefaultScopeName, + ID: row.ID, + PublicKey: hex.EncodeToString(row.PublicKey), + NodeType: row.NodeType, + NodeTypeName: api.NodeTypeName(row.NodeType), + Name: row.Name, + Latitude: row.Latitude, + Longitude: row.Longitude, + IsObserver: row.IsObserver, + ObserverID: nullableUUID(row.ObserverID), + DefaultScope: row.DefaultScopeName, + KnownNeighborCount: row.KnownNeighborCount, }, LocationSource: row.LocationSource, SupportsMultibytePaths: row.SupportsMultibytePaths, @@ -212,11 +214,25 @@ func (s *Store) GetNodeNeighbors(ctx context.Context, nodeID uuid.UUID) ([]api.N if err != nil { return nil, err } + seen := make(map[uuid.UUID]int) items := make([]api.NodeNeighbor, 0, len(rows)) for _, r := range rows { + if idx, ok := seen[r.ID]; ok { + items[idx].ObservationCount += r.ObservationCount + if r.LastSeen.Time.After(time.UnixMilli(items[idx].LastSeen)) { + items[idx].LastSeen = r.LastSeen.Time.UnixMilli() + items[idx].IATA = r.Iata + } + if r.FirstSeen.Time.Before(time.UnixMilli(items[idx].FirstSeen)) { + items[idx].FirstSeen = r.FirstSeen.Time.UnixMilli() + } + continue + } + seen[r.ID] = len(items) items = append(items, api.NodeNeighbor{ ID: r.ID, Name: r.Name, + PublicKey: hex.EncodeToString(r.PublicKey), NodeType: r.NodeType, NodeTypeName: api.NodeTypeName(r.NodeType), Latitude: r.Latitude, diff --git a/db/queries/queries.sql b/db/queries/queries.sql index 99c1e63..5ef8e78 100644 --- a/db/queries/queries.sql +++ b/db/queries/queries.sql @@ -472,7 +472,8 @@ SELECT n.*, ts.name AS default_scope_name, EXISTS (SELECT 1 FROM observers o WHERE o.public_key = n.public_key) AS is_observer, (SELECT o.id FROM observers o WHERE o.public_key = n.public_key LIMIT 1) AS observer_id, (SELECT json_agg(json_build_object('iata', ni.iata, 'lastHeard', (extract(epoch from ni.last_heard) * 1000)::bigint) ORDER BY ni.last_heard DESC) - FROM node_iatas ni WHERE ni.node_id = n.id) AS iatas + FROM node_iatas ni WHERE ni.node_id = n.id) AS iatas, + (SELECT COUNT(*) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count FROM nodes n LEFT JOIN transport_scopes ts ON ts.id = n.default_scope_id WHERE n.id = $1; @@ -488,7 +489,8 @@ SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_ ts.name AS default_scope_name, json_agg(json_build_object('iata', ni.iata, 'lastHeard', (extract(epoch from ni.last_heard) * 1000)::bigint) ORDER BY ni.last_heard DESC) FILTER (WHERE ni.iata IS NOT NULL) AS iatas, EXISTS (SELECT 1 FROM observers o WHERE o.public_key = n.public_key) AS is_observer, - (SELECT o.id FROM observers o WHERE o.public_key = n.public_key LIMIT 1) AS observer_id + (SELECT o.id FROM observers o WHERE o.public_key = n.public_key LIMIT 1) AS observer_id, + (SELECT COUNT(*) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count FROM nodes n LEFT JOIN node_iatas ni ON ni.node_id = n.id LEFT JOIN transport_scopes ts ON ts.id = n.default_scope_id @@ -880,7 +882,7 @@ ON CONFLICT (node_id, neighbor_id, iata) DO UPDATE SET -- name: GetNodeNeighbors :many -- Returns the neighbors of a node with details, ordered by most recently seen. SELECT - n.id, n.name, n.node_type, n.latitude, n.longitude, + n.id, n.public_key, n.name, n.node_type, n.latitude, n.longitude, nn.iata, nn.observation_count, nn.first_seen, nn.last_seen FROM node_neighbors nn JOIN nodes n ON n.id = nn.neighbor_id diff --git a/db/sqlc/queries.sql.go b/db/sqlc/queries.sql.go index cd0cf46..cad26cc 100644 --- a/db/sqlc/queries.sql.go +++ b/db/sqlc/queries.sql.go @@ -216,7 +216,8 @@ SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.locat EXISTS (SELECT 1 FROM observers o WHERE o.public_key = n.public_key) AS is_observer, (SELECT o.id FROM observers o WHERE o.public_key = n.public_key LIMIT 1) AS observer_id, (SELECT json_agg(json_build_object('iata', ni.iata, 'lastHeard', (extract(epoch from ni.last_heard) * 1000)::bigint) ORDER BY ni.last_heard DESC) - FROM node_iatas ni WHERE ni.node_id = n.id) AS iatas + FROM node_iatas ni WHERE ni.node_id = n.id) AS iatas, + (SELECT COUNT(*) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count FROM nodes n LEFT JOIN transport_scopes ts ON ts.id = n.default_scope_id WHERE n.id = $1 @@ -245,6 +246,7 @@ type GetNodeByIDRow struct { IsObserver bool `json:"is_observer"` ObserverID uuid.UUID `json:"observer_id"` Iatas []byte `json:"iatas"` + KnownNeighborCount int64 `json:"known_neighbor_count"` } func (q *Queries) GetNodeByID(ctx context.Context, id uuid.UUID) (GetNodeByIDRow, error) { @@ -273,13 +275,14 @@ func (q *Queries) GetNodeByID(ctx context.Context, id uuid.UUID) (GetNodeByIDRow &i.IsObserver, &i.ObserverID, &i.Iatas, + &i.KnownNeighborCount, ) return i, err } const getNodeNeighbors = `-- name: GetNodeNeighbors :many SELECT - n.id, n.name, n.node_type, n.latitude, n.longitude, + n.id, n.public_key, n.name, n.node_type, n.latitude, n.longitude, nn.iata, nn.observation_count, nn.first_seen, nn.last_seen FROM node_neighbors nn JOIN nodes n ON n.id = nn.neighbor_id @@ -289,6 +292,7 @@ ORDER BY nn.last_seen DESC type GetNodeNeighborsRow struct { ID uuid.UUID `json:"id"` + PublicKey []byte `json:"public_key"` Name *string `json:"name"` NodeType int16 `json:"node_type"` Latitude *float64 `json:"latitude"` @@ -311,6 +315,7 @@ func (q *Queries) GetNodeNeighbors(ctx context.Context, nodeID uuid.UUID) ([]Get var i GetNodeNeighborsRow if err := rows.Scan( &i.ID, + &i.PublicKey, &i.Name, &i.NodeType, &i.Latitude, @@ -2018,7 +2023,8 @@ SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_ ts.name AS default_scope_name, json_agg(json_build_object('iata', ni.iata, 'lastHeard', (extract(epoch from ni.last_heard) * 1000)::bigint) ORDER BY ni.last_heard DESC) FILTER (WHERE ni.iata IS NOT NULL) AS iatas, EXISTS (SELECT 1 FROM observers o WHERE o.public_key = n.public_key) AS is_observer, - (SELECT o.id FROM observers o WHERE o.public_key = n.public_key LIMIT 1) AS observer_id + (SELECT o.id FROM observers o WHERE o.public_key = n.public_key LIMIT 1) AS observer_id, + (SELECT COUNT(*) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count FROM nodes n LEFT JOIN node_iatas ni ON ni.node_id = n.id LEFT JOIN transport_scopes ts ON ts.id = n.default_scope_id @@ -2057,20 +2063,21 @@ type ListNodesParams struct { } type ListNodesRow struct { - ID uuid.UUID `json:"id"` - PublicKey []byte `json:"public_key"` - NodeType int16 `json:"node_type"` - Name *string `json:"name"` - Latitude *float64 `json:"latitude"` - Longitude *float64 `json:"longitude"` - LastSeen pgtype.Timestamptz `json:"last_seen"` - RadioFreqMhz *float32 `json:"radio_freq_mhz"` - RadioSf *int16 `json:"radio_sf"` - RadioBwKhz *float32 `json:"radio_bw_khz"` - DefaultScopeName *string `json:"default_scope_name"` - Iatas []byte `json:"iatas"` - IsObserver bool `json:"is_observer"` - ObserverID uuid.UUID `json:"observer_id"` + ID uuid.UUID `json:"id"` + PublicKey []byte `json:"public_key"` + NodeType int16 `json:"node_type"` + Name *string `json:"name"` + Latitude *float64 `json:"latitude"` + Longitude *float64 `json:"longitude"` + LastSeen pgtype.Timestamptz `json:"last_seen"` + RadioFreqMhz *float32 `json:"radio_freq_mhz"` + RadioSf *int16 `json:"radio_sf"` + RadioBwKhz *float32 `json:"radio_bw_khz"` + DefaultScopeName *string `json:"default_scope_name"` + Iatas []byte `json:"iatas"` + IsObserver bool `json:"is_observer"` + ObserverID uuid.UUID `json:"observer_id"` + KnownNeighborCount int64 `json:"known_neighbor_count"` } func (q *Queries) ListNodes(ctx context.Context, arg ListNodesParams) ([]ListNodesRow, error) { @@ -2107,6 +2114,7 @@ func (q *Queries) ListNodes(ctx context.Context, arg ListNodesParams) ([]ListNod &i.Iatas, &i.IsObserver, &i.ObserverID, + &i.KnownNeighborCount, ); err != nil { return nil, err } diff --git a/docs/docs.go b/docs/docs.go index acb29be..8a95cd9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2302,6 +2302,9 @@ const docTemplate = `{ "description": "true if this node is also a known observer", "type": "boolean" }, + "knownNeighborCount": { + "type": "integer" + }, "lastAdvertAt": { "description": "epoch ms, nil if no advert received", "type": "integer" @@ -2413,6 +2416,9 @@ const docTemplate = `{ }, "observationCount": { "type": "integer" + }, + "publicKey": { + "type": "string" } } }, @@ -2437,6 +2443,9 @@ const docTemplate = `{ "description": "true if this node is also a known observer", "type": "boolean" }, + "knownNeighborCount": { + "type": "integer" + }, "lat": { "description": "decimal degrees, from advert AppData", "type": "number" diff --git a/docs/swagger.json b/docs/swagger.json index 63a8e8b..775bdc5 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2300,6 +2300,9 @@ "description": "true if this node is also a known observer", "type": "boolean" }, + "knownNeighborCount": { + "type": "integer" + }, "lastAdvertAt": { "description": "epoch ms, nil if no advert received", "type": "integer" @@ -2411,6 +2414,9 @@ }, "observationCount": { "type": "integer" + }, + "publicKey": { + "type": "string" } } }, @@ -2435,6 +2441,9 @@ "description": "true if this node is also a known observer", "type": "boolean" }, + "knownNeighborCount": { + "type": "integer" + }, "lat": { "description": "decimal degrees, from advert AppData", "type": "number" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d3fed06..df915ed 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -188,6 +188,8 @@ definitions: isObserver: description: true if this node is also a known observer type: boolean + knownNeighborCount: + type: integer lastAdvertAt: description: epoch ms, nil if no advert received type: integer @@ -267,6 +269,8 @@ definitions: type: string observationCount: type: integer + publicKey: + type: string type: object github_com_MeshCore-Beacon_beacon-server_internal_api.NodeSummary: properties: @@ -283,6 +287,8 @@ definitions: isObserver: description: true if this node is also a known observer type: boolean + knownNeighborCount: + type: integer lat: description: decimal degrees, from advert AppData type: number diff --git a/internal/api/nodes.go b/internal/api/nodes.go index 3e86a89..2302bf9 100644 --- a/internal/api/nodes.go +++ b/internal/api/nodes.go @@ -14,6 +14,7 @@ import ( type NodeNeighbor struct { ID uuid.UUID `json:"id"` Name *string `json:"name,omitempty"` + PublicKey string `json:"publicKey"` NodeType int16 `json:"nodeType"` NodeTypeName string `json:"nodeTypeName"` Latitude *float64 `json:"lat,omitempty"` @@ -32,18 +33,19 @@ type NodeIATA struct { // NodeSummary is the minimal node representation used in list responses. type NodeSummary struct { - ID uuid.UUID `json:"id"` - PublicKey string `json:"publicKey"` // hex-encoded Ed25519 public key - NodeType int16 `json:"nodeType"` // 1=companion, 2=repeater, 3=room_server, 4=sensor - NodeTypeName string `json:"nodeTypeName"` - Name *string `json:"name,omitempty"` - IsObserver bool `json:"isObserver"` // true if this node is also a known observer - ObserverID *uuid.UUID `json:"observerId,omitempty"` // UUID of the associated observer row, if any - Latitude *float64 `json:"lat,omitempty"` // decimal degrees, from advert AppData - Longitude *float64 `json:"lng,omitempty"` // decimal degrees, from advert AppData - Radio *string `json:"radio,omitempty"` // shorthand: "freqMhz,bwKhz,sf" e.g. "910.5,62.5,7" - IATAs []NodeIATA `json:"iatas"` // IATAs where this node has been heard, with last heard timestamps - DefaultScope *string `json:"defaultScope,omitempty"` // most recently matched transport scope name e.g. "#bc" + ID uuid.UUID `json:"id"` + PublicKey string `json:"publicKey"` // hex-encoded Ed25519 public key + NodeType int16 `json:"nodeType"` // 1=companion, 2=repeater, 3=room_server, 4=sensor + NodeTypeName string `json:"nodeTypeName"` + Name *string `json:"name,omitempty"` + IsObserver bool `json:"isObserver"` // true if this node is also a known observer + ObserverID *uuid.UUID `json:"observerId,omitempty"` // UUID of the associated observer row, if any + Latitude *float64 `json:"lat,omitempty"` // decimal degrees, from advert AppData + Longitude *float64 `json:"lng,omitempty"` // decimal degrees, from advert AppData + Radio *string `json:"radio,omitempty"` // shorthand: "freqMhz,bwKhz,sf" e.g. "910.5,62.5,7" + IATAs []NodeIATA `json:"iatas"` // IATAs where this node has been heard, with last heard timestamps + DefaultScope *string `json:"defaultScope,omitempty"` // most recently matched transport scope name e.g. "#bc" + KnownNeighborCount int64 `json:"knownNeighborCount"` } // Node is the full node representation including firmware capability flags,