Files
EvoBGP/internal/repository/postgres_entities.go
T
Denozordec dc803bcb34
quality / commitlint (push) Skipped
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 46s
quality / web (push) Successful in 1m16s
quality / go (push) Successful in 2m42s
quality / bird2 (push) Successful in 16s
CD / quality (push) Successful in 5m19s
CD / publish (push) Successful in 7m19s
refactor(web): remove deprecated dashboard components and enhance KPI grid
- Deleted unused components: `DashboardActivityTimeline`, `DashboardFramePanel`, `DashboardModulesGrid`, `DashboardRecentJobsGrid`, and `DashboardRecentRevisionsGrid` to streamline the dashboard.
- Updated `DashboardKpiGrid` to improve KPI display logic, including progress indicators and enhanced badge functionality.
- Refactored `DashboardNetworkHealth` to provide better status representation based on loading states and network conditions.
- Introduced new properties for KPI cards to support progress tracking and improved visual feedback.

This cleanup aims to enhance performance and maintainability of the dashboard while providing a better user experience.
2026-08-31 10:15:59 +07:00

661 lines
19 KiB
Go

package repository
import (
"context"
"encoding/json"
"errors"
"strings"
"time"
"evobgp/internal/store"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
func (p *Postgres) ListCDNSources(tenantID, moduleID string) ([]*store.CDNSource, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "CDN_CIDRS" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `
SELECT id::text, source_kind, url, COALESCE(prefix_path,''), COALESCE(etag,''), refresh_interval_sec, community_id::text, last_refreshed_at
FROM module_cdn_source WHERE module_id=$1 ORDER BY url`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.CDNSource
for rows.Next() {
var s store.CDNSource
s.ModuleID = moduleID
var ri *int32
var comm *string
var last *time.Time
if err := rows.Scan(&s.ID, &s.SourceKind, &s.URL, &s.PrefixPath, &s.Etag, &ri, &comm, &last); err != nil {
continue
}
if ri != nil {
v := int(*ri)
s.RefreshIntervalSec = &v
}
s.CommunityID = strOrNil(comm)
if last != nil {
t := last.UTC()
s.LastRefreshedAt = &t
}
out = append(out, &s)
}
return out, nil
}
func (p *Postgres) CreateCDNSource(tenantID, moduleID string, in *store.CDNSource) (*store.CDNSource, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "CDN_CIDRS" {
return nil, store.ErrInvalidInput
}
if in == nil || strings.TrimSpace(in.URL) == "" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_cdn_source (id, module_id, source_kind, url, prefix_path, etag, refresh_interval_sec, community_id, last_refreshed_at)
VALUES ($1,$2,$3,$4,$5,$6,$7, NULLIF($8::uuid, '00000000-0000-0000-0000-000000000000'::uuid), $9)`,
id, moduleID, in.SourceKind, strings.TrimSpace(in.URL), strings.TrimSpace(in.PrefixPath), in.Etag, nullInt32Ptr(in.RefreshIntervalSec), uuidOrNilPtr(in.CommunityID), nullTimePtr(in.LastRefreshedAt))
if err != nil {
return nil, err
}
p.invalidateModuleInputHash(tenantID, moduleID)
return p.getCDNSource(ctx, moduleID, id)
}
func (p *Postgres) getCDNSource(ctx context.Context, moduleID, id string) (*store.CDNSource, error) {
var s store.CDNSource
s.ModuleID = moduleID
var ri *int32
var comm *string
var last *time.Time
err := p.pool.QueryRow(ctx, `
SELECT id::text, source_kind, url, COALESCE(prefix_path,''), COALESCE(etag,''), refresh_interval_sec, community_id::text, last_refreshed_at
FROM module_cdn_source WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&s.ID, &s.SourceKind, &s.URL, &s.PrefixPath, &s.Etag, &ri, &comm, &last)
if err != nil {
return nil, err
}
if ri != nil {
v := int(*ri)
s.RefreshIntervalSec = &v
}
s.CommunityID = strOrNil(comm)
if last != nil {
t := last.UTC()
s.LastRefreshedAt = &t
}
return &s, nil
}
func uuidOrNilPtr(s *string) any {
if s == nil || strings.TrimSpace(*s) == "" {
return nil
}
return strings.TrimSpace(*s)
}
func (p *Postgres) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *store.CDNSourcePatch) (*store.CDNSource, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getCDNSource(context.Background(), moduleID, sourceID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
if patch.SourceKind != nil {
cur.SourceKind = *patch.SourceKind
}
if patch.URL != nil {
cur.URL = strings.TrimSpace(*patch.URL)
}
if patch.PrefixPath != nil {
cur.PrefixPath = strings.TrimSpace(*patch.PrefixPath)
}
if patch.Etag != nil {
cur.Etag = *patch.Etag
}
if patch.RefreshIntervalSec != nil {
cur.RefreshIntervalSec = patch.RefreshIntervalSec
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
if patch.LastRefreshedAt != nil {
t := patch.LastRefreshedAt.UTC()
cur.LastRefreshedAt = &t
}
ctx := context.Background()
_, err = p.pool.Exec(ctx, `
UPDATE module_cdn_source SET source_kind=$3, url=$4, prefix_path=$5, etag=$6, refresh_interval_sec=$7,
community_id=NULLIF($8::uuid, '00000000-0000-0000-0000-000000000000'::uuid), last_refreshed_at=$9, updated_at=now()
WHERE id=$1 AND module_id=$2`,
sourceID, moduleID, cur.SourceKind, cur.URL, cur.PrefixPath, cur.Etag, nullInt32Ptr(cur.RefreshIntervalSec), uuidOrNilPtr(cur.CommunityID), nullTimePtr(cur.LastRefreshedAt))
if err != nil {
return nil, err
}
if store.CDNPatchAffectsInputHash(patch) {
p.invalidateModuleInputHash(tenantID, moduleID)
}
return p.getCDNSource(ctx, moduleID, sourceID)
}
func (p *Postgres) DeleteCDNSource(tenantID, moduleID, sourceID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_cdn_source WHERE id=$1 AND module_id=$2`, sourceID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
p.invalidateModuleInputHash(tenantID, moduleID)
return nil
}
func (p *Postgres) ListASEntries(tenantID, moduleID string) ([]*store.ASEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "AS_PREFIXES" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `
SELECT id::text, asn, community_id::text, asn_name, prefix_count, asn_resolved_at
FROM module_as_entry WHERE module_id=$1 ORDER BY asn`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.ASEntry
for rows.Next() {
var e store.ASEntry
e.ModuleID = moduleID
var comm, asnName *string
var pc *int64
var at *time.Time
if err := rows.Scan(&e.ID, &e.ASN, &comm, &asnName, &pc, &at); err != nil {
continue
}
e.CommunityID = strOrNil(comm)
if asnName != nil {
e.ASNName = *asnName
}
e.PrefixCount = pc
e.ASNResolvedAt = at
out = append(out, &e)
}
return out, nil
}
func (p *Postgres) CreateASEntry(tenantID, moduleID string, in *store.ASEntry) (*store.ASEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "AS_PREFIXES" {
return nil, store.ErrInvalidInput
}
if in == nil || !store.ValidASN(in.ASN) {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_as_entry (id, module_id, asn, community_id)
VALUES ($1,$2,$3,NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
id, moduleID, in.ASN, uuidOrNilPtr(in.CommunityID))
if err != nil {
return nil, err
}
p.invalidateModuleInputHash(tenantID, moduleID)
return p.getASEntry(ctx, moduleID, id)
}
func (p *Postgres) getASEntry(ctx context.Context, moduleID, id string) (*store.ASEntry, error) {
var e store.ASEntry
e.ModuleID = moduleID
var comm, asnName *string
var pc *int64
var at *time.Time
err := p.pool.QueryRow(ctx, `
SELECT id::text, asn, community_id::text, asn_name, prefix_count, asn_resolved_at
FROM module_as_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(
&e.ID, &e.ASN, &comm, &asnName, &pc, &at)
if err != nil {
return nil, err
}
e.CommunityID = strOrNil(comm)
if asnName != nil {
e.ASNName = *asnName
}
e.PrefixCount = pc
e.ASNResolvedAt = at
return &e, nil
}
func (p *Postgres) UpdateASEntry(tenantID, moduleID, entryID string, patch *store.ASEntryPatch) (*store.ASEntry, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getASEntry(context.Background(), moduleID, entryID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
prevASN := cur.ASN
if patch.ASN != nil {
cur.ASN = *patch.ASN
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
if !store.ValidASN(cur.ASN) {
return nil, store.ErrInvalidInput
}
clearResolve := patch.ASN != nil && cur.ASN != prevASN
ctx := context.Background()
if clearResolve {
_, err = p.pool.Exec(ctx, `
UPDATE module_as_entry SET asn=$3, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid),
asn_name=NULL, prefix_count=NULL, asn_resolved_at=NULL, updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.ASN, uuidOrNilPtr(cur.CommunityID))
} else {
_, err = p.pool.Exec(ctx, `
UPDATE module_as_entry SET asn=$3, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.ASN, uuidOrNilPtr(cur.CommunityID))
}
if err != nil {
return nil, err
}
p.invalidateModuleInputHash(tenantID, moduleID)
return p.getASEntry(ctx, moduleID, entryID)
}
func (p *Postgres) UpdateASEntryResolveMeta(tenantID, moduleID, entryID string, asnName string, prefixCount int64, resolvedAt time.Time) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
var nameArg any
sn := strings.TrimSpace(asnName)
if sn == "" {
nameArg = nil
} else {
nameArg = sn
}
tag, err := p.pool.Exec(ctx, `
UPDATE module_as_entry SET asn_name=$3, prefix_count=$4, asn_resolved_at=$5, updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, nameArg, prefixCount, resolvedAt.UTC())
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return nil
}
func (p *Postgres) UpdateASEntryResolveMetaBatch(tenantID, moduleID string, updates []store.ASEntryResolveMetaUpdate, resolvedAt time.Time) error {
if len(updates) == 0 {
return nil
}
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
batch := &pgx.Batch{}
for _, u := range updates {
var nameArg any
sn := strings.TrimSpace(u.ASNName)
if sn == "" {
nameArg = nil
} else {
nameArg = sn
}
batch.Queue(`
UPDATE module_as_entry SET asn_name=$3, prefix_count=$4, asn_resolved_at=$5, updated_at=now()
WHERE id=$1 AND module_id=$2`,
u.EntryID, moduleID, nameArg, u.PrefixCount, resolvedAt.UTC())
}
br := p.pool.SendBatch(ctx, batch)
defer func() { _ = br.Close() }()
for range updates {
if _, err := br.Exec(); err != nil {
return err
}
}
return nil
}
func (p *Postgres) DeleteASEntry(tenantID, moduleID, entryID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_as_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
p.invalidateModuleInputHash(tenantID, moduleID)
return nil
}
func (p *Postgres) ListDomainEntries(tenantID, moduleID string) ([]*store.DomainEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "DOMAINS" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `SELECT id::text, fqdn, community_id::text FROM module_domain_entry WHERE module_id=$1`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.DomainEntry
for rows.Next() {
var e store.DomainEntry
e.ModuleID = moduleID
var comm *string
if err := rows.Scan(&e.ID, &e.FQDN, &comm); err != nil {
continue
}
e.CommunityID = strOrNil(comm)
out = append(out, &e)
}
return out, nil
}
func (p *Postgres) CreateDomainEntry(tenantID, moduleID string, in *store.DomainEntry) (*store.DomainEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "DOMAINS" || in == nil || strings.TrimSpace(in.FQDN) == "" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_domain_entry (id, module_id, fqdn, community_id)
VALUES ($1,$2,$3, NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
id, moduleID, strings.TrimSpace(in.FQDN), uuidOrNilPtr(in.CommunityID))
if err != nil {
return nil, err
}
p.invalidateModuleInputHash(tenantID, moduleID)
return p.getDomainEntry(ctx, moduleID, id)
}
func (p *Postgres) getDomainEntry(ctx context.Context, moduleID, id string) (*store.DomainEntry, error) {
var e store.DomainEntry
e.ModuleID = moduleID
var comm *string
err := p.pool.QueryRow(ctx, `SELECT id::text, fqdn, community_id::text FROM module_domain_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&e.ID, &e.FQDN, &comm)
if err != nil {
return nil, err
}
e.CommunityID = strOrNil(comm)
return &e, nil
}
func (p *Postgres) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *store.DomainEntryPatch) (*store.DomainEntry, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getDomainEntry(context.Background(), moduleID, entryID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
if patch.FQDN != nil {
cur.FQDN = strings.TrimSpace(*patch.FQDN)
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
ctx := context.Background()
_, err = p.pool.Exec(ctx, `
UPDATE module_domain_entry SET fqdn=$3, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.FQDN, uuidOrNilPtr(cur.CommunityID))
if err != nil {
return nil, err
}
p.invalidateModuleInputHash(tenantID, moduleID)
return p.getDomainEntry(ctx, moduleID, entryID)
}
func (p *Postgres) DeleteDomainEntry(tenantID, moduleID, entryID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_domain_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
p.invalidateModuleInputHash(tenantID, moduleID)
return nil
}
func (p *Postgres) ListIPRangeEntries(tenantID, moduleID string) ([]*store.IPRangeEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "IP_RANGES" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `SELECT id::text, prefix::text, community_id::text FROM module_ip_range_entry WHERE module_id=$1`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.IPRangeEntry
for rows.Next() {
var e store.IPRangeEntry
e.ModuleID = moduleID
var comm *string
if err := rows.Scan(&e.ID, &e.Prefix, &comm); err != nil {
continue
}
e.CommunityID = strOrNil(comm)
out = append(out, &e)
}
return out, nil
}
func (p *Postgres) CreateIPRangeEntry(tenantID, moduleID string, in *store.IPRangeEntry) (*store.IPRangeEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "IP_RANGES" || in == nil || strings.TrimSpace(in.Prefix) == "" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_ip_range_entry (id, module_id, prefix, community_id)
VALUES ($1,$2,$3::cidr, NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
id, moduleID, strings.TrimSpace(in.Prefix), uuidOrNilPtr(in.CommunityID))
if err != nil {
return nil, err
}
p.invalidateModuleInputHash(tenantID, moduleID)
return p.getIPRangeEntry(ctx, moduleID, id)
}
func (p *Postgres) getIPRangeEntry(ctx context.Context, moduleID, id string) (*store.IPRangeEntry, error) {
var e store.IPRangeEntry
e.ModuleID = moduleID
var comm *string
err := p.pool.QueryRow(ctx, `SELECT id::text, prefix::text, community_id::text FROM module_ip_range_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&e.ID, &e.Prefix, &comm)
if err != nil {
return nil, err
}
e.CommunityID = strOrNil(comm)
return &e, nil
}
func (p *Postgres) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *store.IPRangePatch) (*store.IPRangeEntry, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getIPRangeEntry(context.Background(), moduleID, entryID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
if patch.Prefix != nil {
cur.Prefix = strings.TrimSpace(*patch.Prefix)
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
ctx := context.Background()
_, err = p.pool.Exec(ctx, `
UPDATE module_ip_range_entry SET prefix=$3::cidr, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.Prefix, uuidOrNilPtr(cur.CommunityID))
if err != nil {
return nil, err
}
p.invalidateModuleInputHash(tenantID, moduleID)
return p.getIPRangeEntry(ctx, moduleID, entryID)
}
func (p *Postgres) DeleteIPRangeEntry(tenantID, moduleID, entryID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_ip_range_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
p.invalidateModuleInputHash(tenantID, moduleID)
return nil
}
func (p *Postgres) ListGlobalSettings(tenantID string) (map[string]any, error) {
if _, err := uuid.Parse(tenantID); err != nil {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `SELECT key, value_json FROM global_settings WHERE tenant_id=$1`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[string]any)
for rows.Next() {
var k string
var vj []byte
if err := rows.Scan(&k, &vj); err != nil {
continue
}
var v any
_ = json.Unmarshal(vj, &v)
out[k] = v
}
return out, nil
}
func (p *Postgres) PatchGlobalSettings(tenantID string, patch map[string]any) error {
if _, err := uuid.Parse(tenantID); err != nil {
return store.ErrInvalidInput
}
if patch == nil {
return nil
}
ctx := context.Background()
for k, v := range patch {
if strings.TrimSpace(k) == "" {
continue
}
if v == nil {
_, _ = p.pool.Exec(ctx, `DELETE FROM global_settings WHERE tenant_id=$1 AND key=$2`, tenantID, k)
continue
}
b, err := json.Marshal(v)
if err != nil {
return err
}
_, err = p.pool.Exec(ctx, `
INSERT INTO global_settings (tenant_id, key, value_json) VALUES ($1,$2,$3::jsonb)
ON CONFLICT (tenant_id, key) DO UPDATE SET value_json = EXCLUDED.value_json, updated_at = now()`,
tenantID, k, string(b))
if err != nil {
return err
}
}
return nil
}
var _ store.Backend = (*Postgres)(nil)