mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-09-01 16:48:19 +00:00
feat(nodes): add ?neighbors to list nodes
inlcudes neighbor IDs in node details list
This commit is contained in:
+12
-10
@@ -80,22 +80,23 @@ func (s *Store) SetNodeDefaultScope(ctx context.Context, nodeID uuid.UUID, scope
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32) (api.Page[api.NodeSummary], error) {
|
||||
func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) {
|
||||
var cursorTS pgtype.Timestamptz
|
||||
if cursor > 0 {
|
||||
cursorTS = pgtype.Timestamptz{Time: time.UnixMilli(cursor), Valid: true}
|
||||
}
|
||||
iataFilter := strings.Join(iatas, ",")
|
||||
rows, err := s.q.ListNodes(ctx, sqlc.ListNodesParams{
|
||||
Column1: nodeType,
|
||||
Column2: iataFilter,
|
||||
Column3: tristate(supportsMultibytePaths),
|
||||
Column4: tristate(supportsMultibyteTraces),
|
||||
Column5: pubkey,
|
||||
Column6: name,
|
||||
Column7: cursorTS,
|
||||
Limit: limit + 1,
|
||||
Column9: scope,
|
||||
Column1: nodeType,
|
||||
Column2: iataFilter,
|
||||
Column3: tristate(supportsMultibytePaths),
|
||||
Column4: tristate(supportsMultibyteTraces),
|
||||
Column5: pubkey,
|
||||
Column6: name,
|
||||
Column7: cursorTS,
|
||||
Limit: limit + 1,
|
||||
Column9: scope,
|
||||
Column10: includeNeighbors,
|
||||
})
|
||||
if err != nil {
|
||||
return api.Page[api.NodeSummary]{}, err
|
||||
@@ -117,6 +118,7 @@ func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, s
|
||||
IsObserver: v.IsObserver,
|
||||
ObserverID: nullableUUID(v.ObserverID),
|
||||
KnownNeighborCount: v.KnownNeighborCount,
|
||||
NeighborIDs: v.NeighborIds,
|
||||
}
|
||||
if len(v.Iatas) > 0 {
|
||||
if err := json.Unmarshal(v.Iatas, &node.IATAs); err != nil {
|
||||
|
||||
+60
-3
@@ -134,7 +134,7 @@ func TestListNodes_Pagination(t *testing.T) {
|
||||
Return(rows, nil)
|
||||
|
||||
store := &Store{q: mock}
|
||||
page, err := store.ListNodes(context.Background(), 0, []string{"YVR"}, nil, nil, nil, "", "", 0, 2)
|
||||
page, err := store.ListNodes(context.Background(), 0, []string{"YVR"}, nil, nil, nil, "", "", 0, 2, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func TestListNodes_IATAsUnmarshal(t *testing.T) {
|
||||
}, nil)
|
||||
|
||||
store := &Store{q: mock}
|
||||
page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", 0, 10)
|
||||
page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", 0, 10, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -201,7 +201,7 @@ func TestListNodes_RadioStringFormatting(t *testing.T) {
|
||||
}, nil)
|
||||
|
||||
store := &Store{q: mock}
|
||||
page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", 0, 10)
|
||||
page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", 0, 10, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -369,3 +369,60 @@ func TestGetNodeNeighbors_DBError(t *testing.T) {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNodes_IncludeNeighbors_PassesFlagAndMapsIDs(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mock := mockdb.NewMockQuerier(ctrl)
|
||||
|
||||
nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
neighborID := uuid.MustParse("00000000-0000-0000-0000-000000000002")
|
||||
|
||||
mock.EXPECT().
|
||||
ListNodes(gomock.Any(), gomock.Eq(sqlc.ListNodesParams{
|
||||
Column1: int16(0), Column2: "", Column3: "any", Column4: "any",
|
||||
Column5: nil, Column6: "", Column7: pgtype.Timestamptz{},
|
||||
Limit: 11, Column9: "", Column10: true,
|
||||
})).
|
||||
Return([]sqlc.ListNodesRow{
|
||||
{
|
||||
ID: nodeID,
|
||||
PublicKey: []byte{0x01},
|
||||
NeighborIds: []uuid.UUID{neighborID},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
store := &Store{q: mock}
|
||||
page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", 0, 10, true)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(page.Items[0].NeighborIDs) != 1 || page.Items[0].NeighborIDs[0] != neighborID {
|
||||
t.Errorf("expected NeighborIDs [%s], got %v", neighborID, page.Items[0].NeighborIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNodes_ExcludeNeighbors_LeavesIDsNil(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mock := mockdb.NewMockQuerier(ctrl)
|
||||
|
||||
nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
|
||||
mock.EXPECT().
|
||||
ListNodes(gomock.Any(), gomock.Eq(sqlc.ListNodesParams{
|
||||
Column1: int16(0), Column2: "", Column3: "any", Column4: "any",
|
||||
Column5: nil, Column6: "", Column7: pgtype.Timestamptz{},
|
||||
Limit: 11, Column9: "", Column10: false,
|
||||
})).
|
||||
Return([]sqlc.ListNodesRow{
|
||||
{ID: nodeID, PublicKey: []byte{0x01}, NeighborIds: nil},
|
||||
}, nil)
|
||||
|
||||
store := &Store{q: mock}
|
||||
page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", 0, 10, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if page.Items[0].NeighborIDs != nil {
|
||||
t.Errorf("expected NeighborIDs to stay nil when includeNeighbors is false, got %v", page.Items[0].NeighborIDs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,7 +494,12 @@ SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_
|
||||
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 COUNT(DISTINCT nn.neighbor_id) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count
|
||||
(SELECT COUNT(DISTINCT nn.neighbor_id) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count,
|
||||
-- CASE short-circuits: the array_agg subquery only runs when $10 is true,
|
||||
-- so requests that don't ask for neighbor IDs don't pay for it.
|
||||
(CASE WHEN $10::bool THEN
|
||||
(SELECT COALESCE(array_agg(DISTINCT nn.neighbor_id), '{}'::uuid[]) FROM node_neighbors nn WHERE nn.node_id = n.id)
|
||||
ELSE NULL END)::uuid[] AS neighbor_ids
|
||||
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
|
||||
|
||||
+19
-10
@@ -2039,7 +2039,12 @@ SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_
|
||||
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 COUNT(DISTINCT nn.neighbor_id) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count
|
||||
(SELECT COUNT(DISTINCT nn.neighbor_id) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count,
|
||||
-- CASE short-circuits: the array_agg subquery only runs when $10 is true,
|
||||
-- so requests that don't ask for neighbor IDs don't pay for it.
|
||||
(CASE WHEN $10::bool THEN
|
||||
(SELECT COALESCE(array_agg(DISTINCT nn.neighbor_id), '{}'::uuid[]) FROM node_neighbors nn WHERE nn.node_id = n.id)
|
||||
ELSE NULL END)::uuid[] AS neighbor_ids
|
||||
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
|
||||
@@ -2066,15 +2071,16 @@ LIMIT $8
|
||||
`
|
||||
|
||||
type ListNodesParams struct {
|
||||
Column1 interface{} `json:"column_1"`
|
||||
Column2 string `json:"column_2"`
|
||||
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"`
|
||||
Limit int32 `json:"limit"`
|
||||
Column9 string `json:"column_9"`
|
||||
Column1 interface{} `json:"column_1"`
|
||||
Column2 string `json:"column_2"`
|
||||
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"`
|
||||
Limit int32 `json:"limit"`
|
||||
Column9 string `json:"column_9"`
|
||||
Column10 bool `json:"column_10"`
|
||||
}
|
||||
|
||||
type ListNodesRow struct {
|
||||
@@ -2093,6 +2099,7 @@ type ListNodesRow struct {
|
||||
IsObserver bool `json:"is_observer"`
|
||||
ObserverID uuid.UUID `json:"observer_id"`
|
||||
KnownNeighborCount int64 `json:"known_neighbor_count"`
|
||||
NeighborIds []uuid.UUID `json:"neighbor_ids"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListNodes(ctx context.Context, arg ListNodesParams) ([]ListNodesRow, error) {
|
||||
@@ -2106,6 +2113,7 @@ func (q *Queries) ListNodes(ctx context.Context, arg ListNodesParams) ([]ListNod
|
||||
arg.Column7,
|
||||
arg.Limit,
|
||||
arg.Column9,
|
||||
arg.Column10,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2130,6 +2138,7 @@ func (q *Queries) ListNodes(ctx context.Context, arg ListNodesParams) ([]ListNod
|
||||
&i.IsObserver,
|
||||
&i.ObserverID,
|
||||
&i.KnownNeighborCount,
|
||||
&i.NeighborIds,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -521,6 +521,12 @@ const docTemplate = `{
|
||||
"name": "supportsMultibyteTraces",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Include each node's known neighbor IDs (neighborIds field). Bare ?neighbors or ?neighbors=true enables it; omit/false for none",
|
||||
"name": "neighbors",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "last_seen epoch ms of last item for pagination",
|
||||
@@ -2341,6 +2347,13 @@ const docTemplate = `{
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"neighborIds": {
|
||||
"description": "only populated when the list request opts in; see ?neighbors=true",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"neighbors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -2466,6 +2479,13 @@ const docTemplate = `{
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"neighborIds": {
|
||||
"description": "only populated when the list request opts in; see ?neighbors=true",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"nodeType": {
|
||||
"description": "1=companion, 2=repeater, 3=room_server, 4=sensor",
|
||||
"type": "integer"
|
||||
|
||||
@@ -519,6 +519,12 @@
|
||||
"name": "supportsMultibyteTraces",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Include each node's known neighbor IDs (neighborIds field). Bare ?neighbors or ?neighbors=true enables it; omit/false for none",
|
||||
"name": "neighbors",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "last_seen epoch ms of last item for pagination",
|
||||
@@ -2339,6 +2345,13 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"neighborIds": {
|
||||
"description": "only populated when the list request opts in; see ?neighbors=true",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"neighbors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -2464,6 +2477,13 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"neighborIds": {
|
||||
"description": "only populated when the list request opts in; see ?neighbors=true",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"nodeType": {
|
||||
"description": "1=companion, 2=repeater, 3=room_server, 4=sensor",
|
||||
"type": "integer"
|
||||
|
||||
@@ -212,6 +212,11 @@ definitions:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
neighborIds:
|
||||
description: only populated when the list request opts in; see ?neighbors=true
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
neighbors:
|
||||
items:
|
||||
$ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.NodeNeighbor'
|
||||
@@ -299,6 +304,11 @@ definitions:
|
||||
type: number
|
||||
name:
|
||||
type: string
|
||||
neighborIds:
|
||||
description: only populated when the list request opts in; see ?neighbors=true
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
nodeType:
|
||||
description: 1=companion, 2=repeater, 3=room_server, 4=sensor
|
||||
type: integer
|
||||
@@ -1339,6 +1349,11 @@ paths:
|
||||
in: query
|
||||
name: supportsMultibyteTraces
|
||||
type: boolean
|
||||
- description: Include each node's known neighbor IDs (neighborIds field). Bare
|
||||
?neighbors or ?neighbors=true enables it; omit/false for none
|
||||
in: query
|
||||
name: neighbors
|
||||
type: boolean
|
||||
- description: last_seen epoch ms of last item for pagination
|
||||
in: query
|
||||
name: cursor
|
||||
|
||||
@@ -46,6 +46,7 @@ func NodesRouter(reader api.Reader) http.Handler {
|
||||
// @Param pubkey query string false "Exact public key match (hex)"
|
||||
// @Param supportsMultibytePaths query bool false "Filter by multibyte path support (true/false); omit for no filter"
|
||||
// @Param supportsMultibyteTraces query bool false "Filter by multibyte trace support (true/false); omit for no filter"
|
||||
// @Param neighbors query bool false "Include each node's known neighbor IDs (neighborIds field). Bare ?neighbors or ?neighbors=true enables it; omit/false for none"
|
||||
// @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} api.Page[api.NodeSummary]
|
||||
@@ -121,7 +122,21 @@ func listNodes(reader api.Reader) http.HandlerFunc {
|
||||
}
|
||||
supportsMultibyteTraces = &b
|
||||
}
|
||||
nodes, err := reader.ListNodes(r.Context(), nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, name, scope, cursor, limit)
|
||||
var includeNeighbors bool
|
||||
if vals, ok := r.URL.Query()["neighbors"]; ok {
|
||||
v := vals[0] // bare `?neighbors` (no `=`) parses as ""
|
||||
if v == "" {
|
||||
includeNeighbors = true
|
||||
} else {
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid neighbors value")
|
||||
return
|
||||
}
|
||||
includeNeighbors = b
|
||||
}
|
||||
}
|
||||
nodes, err := reader.ListNodes(r.Context(), nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, name, scope, cursor, limit, includeNeighbors)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MeshCore-Beacon/beacon-server/internal/api"
|
||||
@@ -128,7 +129,7 @@ func TestListNodes_OK(t *testing.T) {
|
||||
nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
r := chi.NewRouter()
|
||||
r.Get("/nodes", listNodes(stubReader{
|
||||
listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _ string, _ int64, _ int32) (api.Page[api.NodeSummary], error) {
|
||||
listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _ string, _ int64, _ int32, _ bool) (api.Page[api.NodeSummary], error) {
|
||||
return api.Page[api.NodeSummary]{Items: []api.NodeSummary{{ID: nodeID}}}, nil
|
||||
},
|
||||
}))
|
||||
@@ -140,6 +141,62 @@ func TestListNodes_OK(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNodes_NeighborsParam_PassedThrough(t *testing.T) {
|
||||
nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
neighborID := uuid.MustParse("00000000-0000-0000-0000-000000000002")
|
||||
var gotIncludeNeighbors bool
|
||||
r := chi.NewRouter()
|
||||
r.Get("/nodes", listNodes(stubReader{
|
||||
listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _ string, _ int64, _ int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) {
|
||||
gotIncludeNeighbors = includeNeighbors
|
||||
return api.Page[api.NodeSummary]{Items: []api.NodeSummary{{ID: nodeID, NeighborIDs: []uuid.UUID{neighborID}}}}, nil
|
||||
},
|
||||
}))
|
||||
req := httptest.NewRequest(http.MethodGet, "/nodes?neighbors=true", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if !gotIncludeNeighbors {
|
||||
t.Error("expected neighbors=true query param to be passed through as includeNeighbors=true")
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "neighborIds") {
|
||||
t.Errorf("expected response body to include neighborIds, got %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNodes_NeighborsParam_InvalidValue(t *testing.T) {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/nodes", listNodes(stubReader{}))
|
||||
req := httptest.NewRequest(http.MethodGet, "/nodes?neighbors=notabool", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNodes_NeighborsParam_BareFlagMeansTrue(t *testing.T) {
|
||||
var gotIncludeNeighbors bool
|
||||
r := chi.NewRouter()
|
||||
r.Get("/nodes", listNodes(stubReader{
|
||||
listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _ string, _ int64, _ int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) {
|
||||
gotIncludeNeighbors = includeNeighbors
|
||||
return api.Page[api.NodeSummary]{}, nil
|
||||
},
|
||||
}))
|
||||
req := httptest.NewRequest(http.MethodGet, "/nodes?neighbors", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if !gotIncludeNeighbors {
|
||||
t.Error("expected bare ?neighbors (no value) to be treated as true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNode_OK(t *testing.T) {
|
||||
nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
r := chi.NewRouter()
|
||||
|
||||
@@ -31,7 +31,7 @@ type stubReader struct {
|
||||
getObserverTelemetryBucketed func(ctx context.Context, observerID uuid.UUID, since, until time.Time, bucketHours int32) ([]api.ObserverTelemetryPoint, error)
|
||||
getObserverScopes func(ctx context.Context, observerID uuid.UUID) ([]string, error)
|
||||
listObserverAdverts func(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (api.Page[api.AdvertObservation], error)
|
||||
listNodes func(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32) (api.Page[api.NodeSummary], error)
|
||||
listNodes func(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error)
|
||||
getNode func(ctx context.Context, nodeID uuid.UUID) (*api.Node, error)
|
||||
getNodeNeighbors func(ctx context.Context, nodeID uuid.UUID) ([]api.NodeNeighbor, error)
|
||||
listNodeObservations func(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (api.Page[api.PacketObservationSummary], error)
|
||||
@@ -65,246 +65,287 @@ func (s stubReader) ListIATAs(ctx context.Context) ([]api.IATA, error) {
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetIATA(ctx context.Context, iata string) (*api.IATA, error) {
|
||||
if s.getIATA != nil {
|
||||
return s.getIATA(ctx, iata)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListRegions(ctx context.Context) ([]api.RegionSummary, error) {
|
||||
if s.listRegions != nil {
|
||||
return s.listRegions(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetRegion(ctx context.Context, regionID int32) (*api.Region, error) {
|
||||
if s.getRegion != nil {
|
||||
return s.getRegion(ctx, regionID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetRegionBySlug(ctx context.Context, slug string) (*api.Region, error) {
|
||||
if s.getRegionBySlug != nil {
|
||||
return s.getRegionBySlug(ctx, slug)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListChannels(ctx context.Context, limit int32, hash []byte, iata string, cursor int64) (api.Page[api.ChannelSummary], error) {
|
||||
if s.listChannels != nil {
|
||||
return s.listChannels(ctx, limit, hash, iata, cursor)
|
||||
}
|
||||
return api.Page[api.ChannelSummary]{}, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetChannel(ctx context.Context, channelID int32) (*api.Channel, error) {
|
||||
if s.getChannel != nil {
|
||||
return s.getChannel(ctx, channelID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListChannelMessages(ctx context.Context, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
|
||||
if s.listChannelMessages != nil {
|
||||
return s.listChannelMessages(ctx, channelID, since, limit, iatas, scope, cursor)
|
||||
}
|
||||
return api.Page[api.ChannelMessage]{}, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListChannelMessagesByHash(ctx context.Context, hash []byte, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
|
||||
if s.listChannelMessagesByHash != nil {
|
||||
return s.listChannelMessagesByHash(ctx, hash, since, limit, iatas, scope, cursor)
|
||||
}
|
||||
return api.Page[api.ChannelMessage]{}, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListMessagesAfterID(ctx context.Context, afterID int64, iatas []string, scope string, limit int32) ([]api.ChannelMessage, error) {
|
||||
if s.listMessagesAfterID != nil {
|
||||
return s.listMessagesAfterID(ctx, afterID, iatas, scope, limit)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) {
|
||||
if s.listObservers != nil {
|
||||
return s.listObservers(ctx, iatas, observerType, broker, status, name, scope, cursor, limit)
|
||||
}
|
||||
return api.Page[api.ObserverSummary]{}, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetObserver(ctx context.Context, observerID uuid.UUID) (*api.Observer, error) {
|
||||
if s.getObserver != nil {
|
||||
return s.getObserver(ctx, observerID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetObserverTelemetry(ctx context.Context, observerID uuid.UUID, since, until time.Time, afterID int64) (*api.ObserverTelemetry, error) {
|
||||
if s.getObserverTelemetry != nil {
|
||||
return s.getObserverTelemetry(ctx, observerID, since, until, afterID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetObserverTelemetryBucketed(ctx context.Context, observerID uuid.UUID, since, until time.Time, bucketHours int32) ([]api.ObserverTelemetryPoint, error) {
|
||||
if s.getObserverTelemetryBucketed != nil {
|
||||
return s.getObserverTelemetryBucketed(ctx, observerID, since, until, bucketHours)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetObserverScopes(ctx context.Context, observerID uuid.UUID) ([]string, error) {
|
||||
if s.getObserverScopes != nil {
|
||||
return s.getObserverScopes(ctx, observerID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListObserverAdverts(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (api.Page[api.AdvertObservation], error) {
|
||||
if s.listObserverAdverts != nil {
|
||||
return s.listObserverAdverts(ctx, observerID, cursor, limit)
|
||||
}
|
||||
return api.Page[api.AdvertObservation]{}, nil
|
||||
}
|
||||
func (s stubReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32) (api.Page[api.NodeSummary], error) {
|
||||
|
||||
func (s stubReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) {
|
||||
if s.listNodes != nil {
|
||||
return s.listNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, name, scope, cursor, limit)
|
||||
return s.listNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, name, scope, cursor, limit, includeNeighbors)
|
||||
}
|
||||
return api.Page[api.NodeSummary]{}, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error) {
|
||||
if s.getNode != nil {
|
||||
return s.getNode(ctx, nodeID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetNodeNeighbors(ctx context.Context, nodeID uuid.UUID) ([]api.NodeNeighbor, error) {
|
||||
if s.getNodeNeighbors != nil {
|
||||
return s.getNodeNeighbors(ctx, nodeID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListNodeObservations(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (api.Page[api.PacketObservationSummary], error) {
|
||||
if s.listNodeObservations != nil {
|
||||
return s.listNodeObservations(ctx, nodeID, cursor, limit)
|
||||
}
|
||||
return api.Page[api.PacketObservationSummary]{}, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListPackets(ctx context.Context, payloadType, routeType int16, iatas []string, scope string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) {
|
||||
if s.listPackets != nil {
|
||||
return s.listPackets(ctx, payloadType, routeType, iatas, scope, since, until, cursor, limit)
|
||||
}
|
||||
return api.Page[api.PacketSummary]{}, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListPacketsAfterID(ctx context.Context, afterObservationID int64, payloadType, routeType int16, iatas []string, scope string, limit int32) ([]api.PacketSummary, error) {
|
||||
if s.listPacketsAfterID != nil {
|
||||
return s.listPacketsAfterID(ctx, afterObservationID, payloadType, routeType, iatas, scope, limit)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetPacket(ctx context.Context, packetHash []byte) (*api.Packet, error) {
|
||||
if s.getPacket != nil {
|
||||
return s.getPacket(ctx, packetHash)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetRadioPresets(ctx context.Context, preset string, iatas []string) ([]api.RadioPreset, error) {
|
||||
if s.getRadioPresets != nil {
|
||||
return s.getRadioPresets(ctx, preset, iatas)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetStatsOverview(ctx context.Context, iatas []string) (*api.StatsOverview, error) {
|
||||
if s.getStatsOverview != nil {
|
||||
return s.getStatsOverview(ctx, iatas)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetStatsObservations(ctx context.Context, iatas []string, since time.Time) ([]api.ObservationPoint, error) {
|
||||
if s.getStatsObservations != nil {
|
||||
return s.getStatsObservations(ctx, iatas, since)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetStatsPayloadBreakdown(ctx context.Context, iatas []string, since time.Time) ([]api.PayloadBreakdownItem, error) {
|
||||
if s.getStatsPayloadBreakdown != nil {
|
||||
return s.getStatsPayloadBreakdown(ctx, iatas, since)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetStatsTopNodes(ctx context.Context, iatas []string, limit int32) ([]api.TopNode, error) {
|
||||
if s.getStatsTopNodes != nil {
|
||||
return s.getStatsTopNodes(ctx, iatas, limit)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetStatsTopObservers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopObserver, error) {
|
||||
if s.getStatsTopObservers != nil {
|
||||
return s.getStatsTopObservers(ctx, iatas, since, limit)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetScopeStats(ctx context.Context) ([]api.ScopeStats, error) {
|
||||
if s.getScopeStats != nil {
|
||||
return s.getScopeStats(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetStatsNodeTypes(ctx context.Context, iatas []string) ([]api.NodeTypeCount, error) {
|
||||
if s.getStatsNodeTypes != nil {
|
||||
return s.getStatsNodeTypes(ctx, iatas)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetScopeNames(ctx context.Context) ([]string, error) {
|
||||
if s.getScopeNames != nil {
|
||||
return s.getScopeNames(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetScopesByIATAs(ctx context.Context, iatas []string) ([]api.ScopeSummary, error) {
|
||||
if s.getScopesByIATAs != nil {
|
||||
return s.getScopesByIATAs(ctx, iatas)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetScopeByName(ctx context.Context, name string) (*api.ScopeDetail, error) {
|
||||
if s.getScopeByName != nil {
|
||||
return s.getScopeByName(ctx, name)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListTraceTags(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) {
|
||||
if s.listTraceTags != nil {
|
||||
return s.listTraceTags(ctx, iatas, scope, traceType, since, until, cursor, limit)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetTraceByTag(ctx context.Context, tag string) (*api.TraceDetail, error) {
|
||||
if s.getTraceByTag != nil {
|
||||
return s.getTraceByTag(ctx, tag)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) ListKnownRoutes(ctx context.Context, iata string, hopCount int32, cursor time.Time, limit int32) ([]api.KnownRoute, error) {
|
||||
if s.listKnownRoutes != nil {
|
||||
return s.listKnownRoutes(ctx, iata, hopCount, cursor, limit)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) SearchKnownRoutes(ctx context.Context, iata, fromHash, toHash string) ([]api.KnownRoute, error) {
|
||||
if s.searchKnownRoutes != nil {
|
||||
return s.searchKnownRoutes(ctx, iata, fromHash, toHash)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetKnownRoutesByNode(ctx context.Context, iata string, nodeID uuid.UUID) ([]api.KnownRoute, error) {
|
||||
if s.getKnownRoutesByNode != nil {
|
||||
return s.getKnownRoutesByNode(ctx, iata, nodeID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetCrossIATANeighbors(ctx context.Context, nodeID uuid.UUID, iata string) ([]api.NodeNeighbor, error) {
|
||||
if s.getCrossIATANeighbors != nil {
|
||||
return s.getCrossIATANeighbors(ctx, nodeID, iata)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) SearchCrossIATARoutes(ctx context.Context, fromHash, fromIATA, toHash, toIATA string) ([]api.CrossIATARoute, error) {
|
||||
if s.searchCrossIATARoutes != nil {
|
||||
return s.searchCrossIATARoutes(ctx, fromHash, fromIATA, toHash, toIATA)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetNodesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*api.ResolvedNode, error) {
|
||||
if s.getNodesByIDs != nil {
|
||||
return s.getNodesByIDs(ctx, ids)
|
||||
|
||||
+14
-13
@@ -34,19 +34,20 @@ 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"
|
||||
KnownNeighborCount int64 `json:"knownNeighborCount"`
|
||||
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"`
|
||||
NeighborIDs []uuid.UUID `json:"neighborIds,omitempty"` // only populated when the list request opts in; see ?neighbors=true
|
||||
}
|
||||
|
||||
// Node is the full node representation including firmware capability flags,
|
||||
|
||||
@@ -97,9 +97,10 @@ type Reader interface {
|
||||
ListObserverAdverts(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (Page[AdvertObservation], error)
|
||||
|
||||
// ListNodes returns a paginated list of nodes with optional filters.
|
||||
// Pass 0 for nodeType, nil iatas, 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, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32) (Page[NodeSummary], error)
|
||||
// When includeNeighbors is true, each NodeSummary's NeighborIDs field is
|
||||
// populated with the distinct set of neighbor node IDs (across all
|
||||
// IATAs); otherwise it's left nil to avoid the extra aggregation.
|
||||
ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32, includeNeighbors bool) (Page[NodeSummary], error)
|
||||
|
||||
// GetNode returns full detail for a single node by UUID.
|
||||
// Returns nil, pgx.ErrNoRows if the node is not found.
|
||||
|
||||
Vendored
+1
-1
@@ -136,7 +136,7 @@ func (s *stubReader) ListMessagesAfterID(_ context.Context, _ int64, _ []string,
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *stubReader) ListNodes(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _ string, _ int64, _ int32) (api.Page[api.NodeSummary], error) {
|
||||
func (s *stubReader) ListNodes(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _ string, _ int64, _ int32, _ bool) (api.Page[api.NodeSummary], error) {
|
||||
return api.Page[api.NodeSummary]{}, nil
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -344,8 +344,8 @@ func (cr *CachedReader) ListMessagesAfterID(ctx context.Context, afterID int64,
|
||||
}
|
||||
|
||||
// ListNodes implements [api.Reader].
|
||||
func (cr *CachedReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32) (api.Page[api.NodeSummary], error) {
|
||||
return cr.inner.ListNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, name, scope, cursor, limit)
|
||||
func (cr *CachedReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) {
|
||||
return cr.inner.ListNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, name, scope, cursor, limit, includeNeighbors)
|
||||
}
|
||||
|
||||
// ListNodeObservations implements [api.Reader].
|
||||
|
||||
Reference in New Issue
Block a user