Files
OpenFlare/internal/apps/openflare/node/list.go
T
ArthurKoba 3c7b347d7b perf(nodes): paginate node lists and fetch details directly (#54)
* perf(nodes): paginate node lists and fetch details directly

* fix(nodes): preserve Unicode search and page transition state

* fix(nodes): validate search using the UTF-8 byte limit
2026-11-07 21:29:52 +08:00

104 lines
2.7 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package node
import (
"context"
"errors"
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
)
const (
defaultNodePageSize = 20
maxNodePageSize = 100
maxNodeSearchLength = 256
)
// ListQuery filters the bounded node list.
type ListQuery struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
Search string `form:"search"`
Status string `form:"status"`
NodeType string `form:"node_type"`
}
// ListResult contains one node page and its filtered total.
type ListResult struct {
Total int64 `json:"total"`
Results []*View `json:"results"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
func normalizeListQuery(query ListQuery) (ListQuery, error) {
if query.Page < 0 || query.PageSize < 0 {
return query, errors.New(errInvalidNodeListQuery)
}
if query.Page == 0 {
query.Page = 1
}
if query.PageSize == 0 {
query.PageSize = defaultNodePageSize
}
if query.PageSize > maxNodePageSize {
query.PageSize = maxNodePageSize
}
if query.Page-1 > int(^uint(0)>>1)/query.PageSize {
return query, errors.New(errInvalidNodeListQuery)
}
query.Search = strings.TrimSpace(query.Search)
if len(query.Search) > maxNodeSearchLength {
return query, errors.New(errInvalidNodeListQuery)
}
switch query.Status {
case "", nodeStatusPending, nodeStatusOnline, nodeStatusOffline:
default:
return query, errors.New(errInvalidNodeListQuery)
}
switch query.NodeType {
case "", nodeTypeEdgeNode, nodeTypeTunnelRelay, nodeTypeTunnelClient:
default:
return query, errors.New(errInvalidNodeListQuery)
}
return query, nil
}
// ListPage returns only the requested node page and latest apply metadata for its nodes.
func ListPage(ctx context.Context, input ListQuery) (*ListResult, error) {
query, err := normalizeListQuery(input)
if err != nil {
return nil, err
}
now := time.Now()
nodes, total, err := repository.ListOpenFlareNodePage(ctx, model.OpenFlareNodeListQuery{
Page: query.Page, PageSize: query.PageSize, Search: query.Search, Status: query.Status, NodeType: query.NodeType, Now: now,
})
if err != nil {
return nil, err
}
views, err := buildNodeViews(ctx, nodes, now)
if err != nil {
return nil, err
}
return &ListResult{Total: total, Results: views, Page: query.Page, PageSize: query.PageSize}, nil
}
// GetNode returns a single node with the same latest apply metadata as the list.
func GetNode(ctx context.Context, id uint) (*View, error) {
node, err := repository.GetOpenFlareNodeByID(ctx, id)
if err != nil {
return nil, err
}
views, err := buildNodeViews(ctx, []model.OpenFlareNode{*node}, time.Now())
if err != nil {
return nil, err
}
return views[0], nil
}