diff --git a/db/queries/queries.sql b/db/queries/queries.sql index a244dc9..689a403 100644 --- a/db/queries/queries.sql +++ b/db/queries/queries.sql @@ -318,14 +318,18 @@ WHERE id = $1 AND supports_multibyte_traces = FALSE; -- name: GetNodeByPubkey :one SELECT *, 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 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 nodes n WHERE n.public_key = $1; -- name: GetNodeByID :one SELECT *, 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 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 nodes n WHERE n.id = $1; @@ -333,7 +337,7 @@ WHERE n.id = $1; -- name: ListNodes :many SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_seen, n.radio_freq_mhz, n.radio_sf, n.radio_bw_khz, - array_remove(array_agg(DISTINCT ni.iata ORDER BY ni.iata), NULL)::text[] AS iatas, + 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 FROM nodes n diff --git a/db/sqlc/queries.sql.go b/db/sqlc/queries.sql.go index 88e726e..727f47e 100644 --- a/db/sqlc/queries.sql.go +++ b/db/sqlc/queries.sql.go @@ -208,7 +208,9 @@ func (q *Queries) GetIATA(ctx context.Context, iata string) (IataCode, error) { const getNodeByID = `-- name: GetNodeByID :one SELECT id, public_key, node_type, name, latitude, longitude, location_source, last_advert_at, supports_multibyte_paths, supports_multibyte_traces, min_firmware_version, first_seen, last_seen, radio_freq_mhz, radio_sf, radio_bw_khz, metadata, 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 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 nodes n WHERE n.id = $1 ` @@ -233,6 +235,7 @@ type GetNodeByIDRow struct { Metadata []byte `json:"metadata"` IsObserver bool `json:"is_observer"` ObserverID uuid.UUID `json:"observer_id"` + Iatas []byte `json:"iatas"` } func (q *Queries) GetNodeByID(ctx context.Context, id uuid.UUID) (GetNodeByIDRow, error) { @@ -258,6 +261,7 @@ func (q *Queries) GetNodeByID(ctx context.Context, id uuid.UUID) (GetNodeByIDRow &i.Metadata, &i.IsObserver, &i.ObserverID, + &i.Iatas, ) return i, err } @@ -265,7 +269,9 @@ func (q *Queries) GetNodeByID(ctx context.Context, id uuid.UUID) (GetNodeByIDRow const getNodeByPubkey = `-- name: GetNodeByPubkey :one SELECT id, public_key, node_type, name, latitude, longitude, location_source, last_advert_at, supports_multibyte_paths, supports_multibyte_traces, min_firmware_version, first_seen, last_seen, radio_freq_mhz, radio_sf, radio_bw_khz, metadata, 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 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 nodes n WHERE n.public_key = $1 ` @@ -290,6 +296,7 @@ type GetNodeByPubkeyRow struct { Metadata []byte `json:"metadata"` IsObserver bool `json:"is_observer"` ObserverID uuid.UUID `json:"observer_id"` + Iatas []byte `json:"iatas"` } func (q *Queries) GetNodeByPubkey(ctx context.Context, publicKey []byte) (GetNodeByPubkeyRow, error) { @@ -315,6 +322,7 @@ func (q *Queries) GetNodeByPubkey(ctx context.Context, publicKey []byte) (GetNod &i.Metadata, &i.IsObserver, &i.ObserverID, + &i.Iatas, ) return i, err } @@ -1355,7 +1363,7 @@ func (q *Queries) ListNodeObservations(ctx context.Context, arg ListNodeObservat const listNodes = `-- name: ListNodes :many SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_seen, n.radio_freq_mhz, n.radio_sf, n.radio_bw_khz, - array_remove(array_agg(DISTINCT ni.iata ORDER BY ni.iata), NULL)::text[] AS iatas, + 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 FROM nodes n @@ -1395,7 +1403,7 @@ type ListNodesRow struct { RadioFreqMhz *float32 `json:"radio_freq_mhz"` RadioSf *int16 `json:"radio_sf"` RadioBwKhz *float32 `json:"radio_bw_khz"` - Iatas []string `json:"iatas"` + Iatas []byte `json:"iatas"` IsObserver bool `json:"is_observer"` ObserverID uuid.UUID `json:"observer_id"` } diff --git a/db/store.go b/db/store.go index 955651c..af28e16 100644 --- a/db/store.go +++ b/db/store.go @@ -7,6 +7,7 @@ import ( "context" "encoding/binary" "encoding/hex" + "encoding/json" "errors" "fmt" "log" @@ -849,10 +850,15 @@ func (s *Store) ListNodes(ctx context.Context, nodeType int16, iata string, supp Name: v.Name, Latitude: v.Latitude, Longitude: v.Longitude, - IATAs: v.Iatas, IsObserver: v.IsObserver, ObvserverID: nullableUUID(v.ObserverID), } + if len(v.Iatas) > 0 { + if err := json.Unmarshal(v.Iatas, &node.IATAs); err != nil { + log.Printf("store: failed to unmarshal node iatas: %v", err) + node.IATAs = []api.NodeIATA{} + } + } if v.RadioFreqMhz != nil && v.RadioSf != nil && v.RadioBwKhz != nil { s := fmt.Sprintf("%.1f,%g,%d", *v.RadioFreqMhz, *v.RadioBwKhz, *v.RadioSf) node.Radio = &s @@ -878,10 +884,6 @@ func (s *Store) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error if err != nil { return nil, err } - iatas, err := s.q.GetNodeIATAs(ctx, nodeID) - if err != nil { - return nil, err - } node := &api.Node{ NodeSummary: api.NodeSummary{ ID: row.ID, @@ -891,7 +893,6 @@ func (s *Store) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error Name: row.Name, Latitude: row.Latitude, Longitude: row.Longitude, - IATAs: iatas, IsObserver: row.IsObserver, ObvserverID: nullableUUID(row.ObserverID), }, @@ -903,6 +904,12 @@ func (s *Store) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error LastSeen: row.LastSeen.Time.UnixMilli(), Metadata: row.Metadata, } + if len(row.Iatas) > 0 { + if err := json.Unmarshal(row.Iatas, &node.IATAs); err != nil { + log.Printf("store: failed to unmarshal node iatas: %v", err) + node.IATAs = []api.NodeIATA{} + } + } if row.RadioFreqMhz != nil && row.RadioSf != nil && row.RadioBwKhz != nil { s := fmt.Sprintf("%.1f,%g,%d", *row.RadioFreqMhz, *row.RadioBwKhz, *row.RadioSf) node.Radio = &s diff --git a/internal/api/reader.go b/internal/api/reader.go index a574e82..5f60122 100644 --- a/internal/api/reader.go +++ b/internal/api/reader.go @@ -180,6 +180,12 @@ type ObserverTelemetry struct { Points []ObserverTelemetryPoint `json:"points"` } +// NodeIATA represents a single IATA code and the last time the node was heard there. +type NodeIATA struct { + IATA string `json:"iata"` + LastHeard int64 `json:"lastHeard"` // epoch ms +} + // NodeSummary is the minimal node representation used in list responses. type NodeSummary struct { ID uuid.UUID `json:"id"` @@ -192,7 +198,7 @@ type NodeSummary struct { Latitude *float64 `json:"lat,omitempty"` Longitude *float64 `json:"lng,omitempty"` Radio *string `json:"radio,omitempty"` - IATAs []string `json:"iatas"` + IATAs []NodeIATA `json:"iatas"` } // Node is the full node representation including firmware capability flags,