Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd3a217cbe | ||
|
|
ff6efec4c5 |
@@ -2922,6 +2922,57 @@ paths:
|
|||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/DefaultProblem"
|
$ref: "#/components/responses/DefaultProblem"
|
||||||
|
|
||||||
|
/v1/communities/{id}/prefixes:
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/TenantId"
|
||||||
|
- $ref: "#/components/parameters/CommunityId"
|
||||||
|
- $ref: "#/components/parameters/Cursor"
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 500
|
||||||
|
maximum: 5000
|
||||||
|
get:
|
||||||
|
tags: [Communities]
|
||||||
|
summary: Префиксы community (latest revision per module)
|
||||||
|
description: |
|
||||||
|
Уникальные materialized-префиксы с данным community_id
|
||||||
|
из последней ревизии каждого модуля tenant.
|
||||||
|
Поле `prefixes` — плоский список для клиентов вроде EvoFirewall.
|
||||||
|
operationId: listCommunityPrefixes
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Успешно.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [items, has_more]
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
required: [prefix]
|
||||||
|
properties:
|
||||||
|
prefix:
|
||||||
|
type: string
|
||||||
|
source:
|
||||||
|
type: string
|
||||||
|
prefixes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
next_cursor:
|
||||||
|
type: ["string", "null"]
|
||||||
|
has_more:
|
||||||
|
type: boolean
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/NotFound"
|
||||||
|
default:
|
||||||
|
$ref: "#/components/responses/DefaultProblem"
|
||||||
|
|
||||||
/v1/communities/{id}:
|
/v1/communities/{id}:
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/TenantId"
|
- $ref: "#/components/parameters/TenantId"
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListCommunityPrefixesByIDAndLabel(t *testing.T) {
|
||||||
|
srv, err := New(Options{
|
||||||
|
InsecureDev: true,
|
||||||
|
SeedDemo: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
tenant, _, _, _, _ := srv.Store().DemoIDs()
|
||||||
|
mustSetTestAPIKeys(t, srv, "vwkey|"+tenant+"|viewer")
|
||||||
|
|
||||||
|
ts := httptest.NewServer(srv.Handler())
|
||||||
|
defer ts.Close()
|
||||||
|
client := ts.Client()
|
||||||
|
base := ts.URL
|
||||||
|
|
||||||
|
reqList, _ := http.NewRequest(http.MethodGet, base+"/v1/communities?limit=10", nil)
|
||||||
|
reqList.Header.Set("Authorization", "Bearer vwkey")
|
||||||
|
respList, err := client.Do(reqList)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = respList.Body.Close() }()
|
||||||
|
if respList.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(respList.Body)
|
||||||
|
t.Fatalf("communities status %d: %s", respList.StatusCode, b)
|
||||||
|
}
|
||||||
|
var listBody struct {
|
||||||
|
Items []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Community string `json:"community"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(respList.Body).Decode(&listBody); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(listBody.Items) == 0 {
|
||||||
|
t.Fatal("expected seeded community")
|
||||||
|
}
|
||||||
|
comm := listBody.Items[0]
|
||||||
|
|
||||||
|
assertPrefixesOK := func(t *testing.T, path string) {
|
||||||
|
t.Helper()
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, base+path, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer vwkey")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
t.Fatalf("%s status %d: %s", path, resp.StatusCode, b)
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Items []map[string]any `json:"items"`
|
||||||
|
Prefixes []string `json:"prefixes"`
|
||||||
|
HasMore bool `json:"has_more"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if body.Items == nil {
|
||||||
|
t.Fatalf("%s: expected items array (got nil)", path)
|
||||||
|
}
|
||||||
|
if body.Prefixes == nil {
|
||||||
|
t.Fatalf("%s: expected prefixes array (got nil)", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UUID id
|
||||||
|
assertPrefixesOK(t, "/v1/communities/"+comm.ID+"/prefixes?limit=100")
|
||||||
|
// Community string (legacy / autocomplete label without title)
|
||||||
|
assertPrefixesOK(t, "/v1/communities/"+url.PathEscape(comm.Community)+"/prefixes?limit=100")
|
||||||
|
if comm.Title != "" {
|
||||||
|
// Full Base UI {value,label} display string
|
||||||
|
label := comm.Community + " · " + comm.Title
|
||||||
|
assertPrefixesOK(t, "/v1/communities/"+url.PathEscape(label)+"/prefixes?limit=100")
|
||||||
|
}
|
||||||
|
|
||||||
|
req404, _ := http.NewRequest(http.MethodGet, base+"/v1/communities/missing-community/prefixes", nil)
|
||||||
|
req404.Header.Set("Authorization", "Bearer vwkey")
|
||||||
|
resp404, err := client.Do(req404)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp404.Body.Close() }()
|
||||||
|
if resp404.StatusCode != http.StatusNotFound {
|
||||||
|
b, _ := io.ReadAll(resp404.Body)
|
||||||
|
t.Fatalf("expected 404, got %d: %s", resp404.StatusCode, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,6 +57,7 @@ func (s *Server) registerCRUDRoutes(m *http.ServeMux) {
|
|||||||
|
|
||||||
m.HandleFunc("GET /communities", s.handleListComm)
|
m.HandleFunc("GET /communities", s.handleListComm)
|
||||||
m.HandleFunc("POST /communities", s.handlePostComm)
|
m.HandleFunc("POST /communities", s.handlePostComm)
|
||||||
|
m.HandleFunc("GET /communities/{id}/prefixes", s.handleListCommPrefixes)
|
||||||
m.HandleFunc("GET /communities/{id}", s.handleGetComm)
|
m.HandleFunc("GET /communities/{id}", s.handleGetComm)
|
||||||
m.HandleFunc("PATCH /communities/{id}", s.handlePatchComm)
|
m.HandleFunc("PATCH /communities/{id}", s.handlePatchComm)
|
||||||
m.HandleFunc("DELETE /communities/{id}", s.handleDeleteComm)
|
m.HandleFunc("DELETE /communities/{id}", s.handleDeleteComm)
|
||||||
@@ -231,6 +232,9 @@ func writePostgresStoreErr(w http.ResponseWriter, err error) bool {
|
|||||||
case "23505":
|
case "23505":
|
||||||
writeProblem(w, http.StatusConflict, "Conflict", "resource already exists")
|
writeProblem(w, http.StatusConflict, "Conflict", "resource already exists")
|
||||||
return true
|
return true
|
||||||
|
case "22P02":
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid id format")
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -945,6 +949,39 @@ func (s *Server) handleGetComm(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, commJSON(x))
|
writeJSON(w, http.StatusOK, commJSON(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleListCommPrefixes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requirePerm(w, a, "bgp:directories:read") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 500
|
||||||
|
}
|
||||||
|
cursor := r.URL.Query().Get("cursor")
|
||||||
|
rows, next, more, err := s.store.ListCommunityPrefixes(a.TenantID, r.PathValue("id"), cursor, limit)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]map[string]any, 0, len(rows))
|
||||||
|
prefixes := make([]string, 0, len(rows))
|
||||||
|
for _, pr := range rows {
|
||||||
|
m := map[string]any{"prefix": pr.Prefix}
|
||||||
|
if pr.Source != "" {
|
||||||
|
m["source"] = pr.Source
|
||||||
|
}
|
||||||
|
items = append(items, m)
|
||||||
|
prefixes = append(prefixes, pr.Prefix)
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
|
"items": items,
|
||||||
|
"prefixes": prefixes,
|
||||||
|
"next_cursor": strPtrOrNull(next),
|
||||||
|
"has_more": more,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handlePostComm(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handlePostComm(w http.ResponseWriter, r *http.Request) {
|
||||||
a, ok := authFromContext(r.Context())
|
a, ok := authFromContext(r.Context())
|
||||||
if !ok || !s.requirePerm(w, a, "bgp:directories:write") {
|
if !ok || !s.requirePerm(w, a, "bgp:directories:write") {
|
||||||
|
|||||||
@@ -1237,12 +1237,32 @@ func (p *Postgres) ListCommunities(tenantID string) ([]*store.Community, error)
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Postgres) GetCommunity(tenantID, id string) (*store.Community, error) {
|
func (p *Postgres) GetCommunity(tenantID, idOrKey string) (*store.Community, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
key := strings.TrimSpace(idOrKey)
|
||||||
|
if key == "" {
|
||||||
|
return nil, store.ErrNotFound
|
||||||
|
}
|
||||||
var c store.Community
|
var c store.Community
|
||||||
c.TenantID = tenantID
|
c.TenantID = tenantID
|
||||||
err := p.pool.QueryRow(ctx, `SELECT id::text, community, title, value_json::text FROM bgp_community WHERE id=$1 AND tenant_id=$2`, id, tenantID).Scan(
|
// Prefer UUID id; fall back to community / title so clients that store the
|
||||||
&c.ID, &c.Community, &c.Title, &c.ValueJSON)
|
// autocomplete label (Base UI {value,label} → label) still resolve.
|
||||||
|
var err error
|
||||||
|
if _, perr := uuid.Parse(key); perr == nil {
|
||||||
|
err = p.pool.QueryRow(ctx, `SELECT id::text, community, title, value_json::text FROM bgp_community WHERE id=$1 AND tenant_id=$2`, key, tenantID).Scan(
|
||||||
|
&c.ID, &c.Community, &c.Title, &c.ValueJSON)
|
||||||
|
} else {
|
||||||
|
err = p.pool.QueryRow(ctx, `
|
||||||
|
SELECT id::text, community, title, value_json::text FROM bgp_community
|
||||||
|
WHERE tenant_id=$1 AND (
|
||||||
|
community = $2
|
||||||
|
OR title = $2
|
||||||
|
OR (NULLIF(trim(title), '') IS NOT NULL AND (community || ' · ' || title) = $2)
|
||||||
|
)
|
||||||
|
ORDER BY community
|
||||||
|
LIMIT 1`, tenantID, key).Scan(
|
||||||
|
&c.ID, &c.Community, &c.Title, &c.ValueJSON)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return nil, store.ErrNotFound
|
return nil, store.ErrNotFound
|
||||||
@@ -1252,6 +1272,91 @@ func (p *Postgres) GetCommunity(tenantID, id string) (*store.Community, error) {
|
|||||||
return &c, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) ListCommunityPrefixes(tenantID, communityID, cursor string, limit int) ([]store.PrefixRow, string, bool, error) {
|
||||||
|
comm, err := p.GetCommunity(tenantID, communityID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
resolvedID := comm.ID
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 500
|
||||||
|
}
|
||||||
|
if limit > 5000 {
|
||||||
|
limit = 5000
|
||||||
|
}
|
||||||
|
off := 0
|
||||||
|
if cursor != "" {
|
||||||
|
if n, err := strconv.Atoi(cursor); err == nil && n >= 0 {
|
||||||
|
off = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
useSnap := prefixSnapshotTableExists(ctx, p.pool)
|
||||||
|
var rows pgx.Rows
|
||||||
|
if useSnap {
|
||||||
|
rows, err = p.pool.Query(ctx, `
|
||||||
|
WITH latest AS (
|
||||||
|
SELECT DISTINCT ON (module_id) id, prefix_snapshot_id
|
||||||
|
FROM config_revision
|
||||||
|
WHERE tenant_id = $1::uuid AND module_id IS NOT NULL
|
||||||
|
ORDER BY module_id, created_at DESC
|
||||||
|
),
|
||||||
|
combined AS (
|
||||||
|
SELECT rmp.prefix::text AS prefix, COALESCE(rmp.source, '') AS source
|
||||||
|
FROM revision_materialized_prefix rmp
|
||||||
|
JOIN latest l ON l.id = rmp.revision_id
|
||||||
|
WHERE l.prefix_snapshot_id IS NULL AND rmp.community_id = $2::uuid
|
||||||
|
UNION
|
||||||
|
SELECT psr.prefix::text, COALESCE(psr.source, '')
|
||||||
|
FROM prefix_snapshot_row psr
|
||||||
|
JOIN latest l ON l.prefix_snapshot_id = psr.snapshot_id
|
||||||
|
WHERE l.prefix_snapshot_id IS NOT NULL AND psr.community_id = $2::uuid
|
||||||
|
)
|
||||||
|
SELECT prefix, source FROM combined
|
||||||
|
ORDER BY prefix
|
||||||
|
LIMIT $3 OFFSET $4`, tenantID, resolvedID, limit+1, off)
|
||||||
|
} else {
|
||||||
|
rows, err = p.pool.Query(ctx, `
|
||||||
|
WITH latest AS (
|
||||||
|
SELECT DISTINCT ON (module_id) id
|
||||||
|
FROM config_revision
|
||||||
|
WHERE tenant_id = $1::uuid AND module_id IS NOT NULL
|
||||||
|
ORDER BY module_id, created_at DESC
|
||||||
|
)
|
||||||
|
SELECT DISTINCT rmp.prefix::text, COALESCE(rmp.source, '')
|
||||||
|
FROM revision_materialized_prefix rmp
|
||||||
|
JOIN latest l ON l.id = rmp.revision_id
|
||||||
|
WHERE rmp.community_id = $2::uuid
|
||||||
|
ORDER BY 1
|
||||||
|
LIMIT $3 OFFSET $4`, tenantID, resolvedID, limit+1, off)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var all []store.PrefixRow
|
||||||
|
for rows.Next() {
|
||||||
|
var pr store.PrefixRow
|
||||||
|
if err := rows.Scan(&pr.Prefix, &pr.Source); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pr.CommunityID = &resolvedID
|
||||||
|
all = append(all, pr)
|
||||||
|
}
|
||||||
|
more := len(all) > limit
|
||||||
|
if more {
|
||||||
|
all = all[:limit]
|
||||||
|
}
|
||||||
|
next := ""
|
||||||
|
if more {
|
||||||
|
next = fmt.Sprintf("%d", off+limit)
|
||||||
|
}
|
||||||
|
if len(all) == 0 {
|
||||||
|
return nil, "", false, nil
|
||||||
|
}
|
||||||
|
return all, next, more, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Postgres) CreateCommunity(tenantID string, in *store.Community) (*store.Community, error) {
|
func (p *Postgres) CreateCommunity(tenantID string, in *store.Community) (*store.Community, error) {
|
||||||
if in == nil {
|
if in == nil {
|
||||||
return nil, store.ErrInvalidInput
|
return nil, store.ErrInvalidInput
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ type Backend interface {
|
|||||||
CreateCommunity(tenantID string, in *Community) (*Community, error)
|
CreateCommunity(tenantID string, in *Community) (*Community, error)
|
||||||
UpdateCommunity(tenantID, id string, patch *CommunityPatch) (*Community, error)
|
UpdateCommunity(tenantID, id string, patch *CommunityPatch) (*Community, error)
|
||||||
DeleteCommunity(tenantID, id string) error
|
DeleteCommunity(tenantID, id string) error
|
||||||
|
// ListCommunityPrefixes returns unique prefixes tagged with community from latest revision per module.
|
||||||
|
ListCommunityPrefixes(tenantID, communityID, cursor string, limit int) (prefixes []PrefixRow, nextCursor string, hasMore bool, err error)
|
||||||
|
|
||||||
// ListPeers returns all BGP peers for a tenant (control plane may paginate in httpapi).
|
// ListPeers returns all BGP peers for a tenant (control plane may paginate in httpapi).
|
||||||
ListPeers(tenantID string) []*BGPPeer
|
ListPeers(tenantID string) []*BGPPeer
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -583,14 +585,97 @@ func (m *Memory) ListCommunities(tenantID string) ([]*Community, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Memory) GetCommunity(tenantID, id string) (*Community, error) {
|
func (m *Memory) GetCommunity(tenantID, idOrKey string) (*Community, error) {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
c, ok := m.communities[id]
|
key := strings.TrimSpace(idOrKey)
|
||||||
if !ok || c.TenantID != tenantID {
|
if key == "" {
|
||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
}
|
}
|
||||||
return c, nil
|
if c, ok := m.communities[key]; ok && c.TenantID == tenantID {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
for _, c := range m.communities {
|
||||||
|
if c.TenantID != tenantID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.Community == key || c.Title == key {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(c.Title) != "" && c.Community+" · "+c.Title == key {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ListCommunityPrefixes(tenantID, communityID, cursor string, limit int) ([]PrefixRow, string, bool, error) {
|
||||||
|
commRow, err := m.GetCommunity(tenantID, communityID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
resolvedID := commRow.ID
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 500
|
||||||
|
}
|
||||||
|
if limit > 5000 {
|
||||||
|
limit = 5000
|
||||||
|
}
|
||||||
|
off := 0
|
||||||
|
if cursor != "" {
|
||||||
|
if n, err := strconv.Atoi(cursor); err == nil && n >= 0 {
|
||||||
|
off = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
latestByModule := map[string]*Revision{}
|
||||||
|
for _, rev := range m.revisions {
|
||||||
|
if rev.TenantID != tenantID || strings.TrimSpace(rev.ModuleID) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cur := latestByModule[rev.ModuleID]
|
||||||
|
if cur == nil || rev.CreatedAt.After(cur.CreatedAt) {
|
||||||
|
latestByModule[rev.ModuleID] = rev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
var all []PrefixRow
|
||||||
|
for _, rev := range latestByModule {
|
||||||
|
for _, pr := range m.revPrefixes[rev.ID] {
|
||||||
|
if pr.CommunityID == nil || *pr.CommunityID != resolvedID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pfx := strings.TrimSpace(pr.Prefix)
|
||||||
|
if pfx == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[pfx]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[pfx] = struct{}{}
|
||||||
|
all = append(all, PrefixRow{Prefix: pfx, CommunityID: &resolvedID, Source: pr.Source})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(all, func(i, j int) bool { return all[i].Prefix < all[j].Prefix })
|
||||||
|
if off > len(all) {
|
||||||
|
return nil, "", false, nil
|
||||||
|
}
|
||||||
|
end := off + limit
|
||||||
|
more := false
|
||||||
|
next := ""
|
||||||
|
if end < len(all) {
|
||||||
|
more = true
|
||||||
|
next = strconv.Itoa(end)
|
||||||
|
all = all[off:end]
|
||||||
|
} else {
|
||||||
|
all = all[off:]
|
||||||
|
}
|
||||||
|
if len(all) == 0 {
|
||||||
|
return nil, "", false, nil
|
||||||
|
}
|
||||||
|
return all, next, more, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Memory) CreateCommunity(tenantID string, in *Community) (*Community, error) {
|
func (m *Memory) CreateCommunity(tenantID string, in *Community) (*Community, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user