Files
EvoBGP/internal/store/memory_crud.go
T
Denozordec ee364c8b6d
CI / changes (push) Successful in 7s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 1m56s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 15s
CI / docker-go-prime (push) Successful in 26s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m0s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m15s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m31s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m32s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m40s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m23s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m23s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m20s
feat: update collectModulePrefixRows to support prior snapshots and enhance CDN/domain prefix collection
Modified the collectModulePrefixRows function to accept an optional priorSnapshot parameter, allowing for more efficient data retrieval by skipping unnecessary CDN fetches. Refactored the logic for collecting prefix rows from AS, CDN, and domain sources to utilize dedicated functions, improving code organization and maintainability. Additionally, introduced caching for module prefix snapshots to optimize performance during refresh operations.
2026-05-19 10:22:33 +07:00

841 lines
19 KiB
Go

package store
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/google/uuid"
)
var _ Backend = (*Memory)(nil)
func (m *Memory) CreateModule(tenantID string, in *Module) (*Module, error) {
if in == nil || strings.TrimSpace(in.Type) == "" || strings.TrimSpace(in.Name) == "" {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.tenants[tenantID]; !ok {
return nil, ErrTenantScope
}
id := uuid.NewString()
mod := &Module{
ID: id,
TenantID: tenantID,
Type: in.Type,
Name: strings.TrimSpace(in.Name),
Enabled: in.Enabled,
Priority: in.Priority,
RefreshIntervalSec: in.RefreshIntervalSec,
CronExpr: in.CronExpr,
DefaultCommunityID: in.DefaultCommunityID,
DohProfileID: in.DohProfileID,
LastRefreshedAt: in.LastRefreshedAt,
}
m.modules[id] = mod
return mod, nil
}
func (m *Memory) UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*Module, error) {
if patch == nil {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
mod, ok := m.modules[moduleID]
if !ok || mod.DeletedAt != nil || mod.TenantID != tenantID {
return nil, ErrNotFound
}
if patch.Name != nil {
mod.Name = strings.TrimSpace(*patch.Name)
}
if patch.Enabled != nil {
mod.Enabled = *patch.Enabled
}
if patch.Priority != nil {
mod.Priority = *patch.Priority
}
if patch.RefreshIntervalSec != nil {
mod.RefreshIntervalSec = *patch.RefreshIntervalSec
}
if patch.CronExpr != nil {
mod.CronExpr = *patch.CronExpr
}
if patch.DefaultCommunityID != nil {
v := strings.TrimSpace(*patch.DefaultCommunityID)
if v == "" {
mod.DefaultCommunityID = nil
} else {
mod.DefaultCommunityID = &v
}
}
if patch.DohProfileID != nil {
v := strings.TrimSpace(*patch.DohProfileID)
if v == "" {
mod.DohProfileID = nil
} else {
mod.DohProfileID = &v
}
}
if patch.LastRefreshedAt != nil {
t := patch.LastRefreshedAt.UTC()
mod.LastRefreshedAt = &t
}
return mod, nil
}
func (m *Memory) SoftDeleteModule(tenantID, moduleID string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.moduleSnapshots != nil {
delete(m.moduleSnapshots, moduleSnapshotKey(tenantID, moduleID))
}
mod, ok := m.modules[moduleID]
if !ok || mod.TenantID != tenantID {
return ErrNotFound
}
now := time.Now().UTC()
mod.DeletedAt = &now
return nil
}
func (m *Memory) moduleWriteOK(tenantID, moduleID string) (*Module, error) {
mod, ok := m.modules[moduleID]
if !ok || mod.DeletedAt != nil || mod.TenantID != tenantID {
return nil, ErrNotFound
}
return mod, nil
}
func (m *Memory) ListCDNSources(tenantID, moduleID string) ([]*CDNSource, error) {
m.mu.RLock()
defer m.mu.RUnlock()
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "CDN_CIDRS" {
return nil, ErrInvalidInput
}
var out []*CDNSource
for _, s := range m.cdnSources {
if s.ModuleID == moduleID {
out = append(out, s)
}
}
return out, nil
}
func (m *Memory) CreateCDNSource(tenantID, moduleID string, in *CDNSource) (*CDNSource, error) {
if in == nil || strings.TrimSpace(in.URL) == "" {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "CDN_CIDRS" {
return nil, ErrInvalidInput
}
id := uuid.NewString()
s := &CDNSource{
ID: id,
ModuleID: moduleID,
SourceKind: in.SourceKind,
URL: strings.TrimSpace(in.URL),
PrefixPath: strings.TrimSpace(in.PrefixPath),
Etag: in.Etag,
RefreshIntervalSec: in.RefreshIntervalSec,
CommunityID: in.CommunityID,
LastRefreshedAt: in.LastRefreshedAt,
}
m.cdnSources[id] = s
return s, nil
}
func (m *Memory) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *CDNSourcePatch) (*CDNSource, error) {
if patch == nil {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
return nil, err
}
s, ok := m.cdnSources[sourceID]
if !ok || s.ModuleID != moduleID {
return nil, ErrNotFound
}
if patch.SourceKind != nil {
s.SourceKind = *patch.SourceKind
}
if patch.URL != nil {
s.URL = strings.TrimSpace(*patch.URL)
}
if patch.PrefixPath != nil {
s.PrefixPath = strings.TrimSpace(*patch.PrefixPath)
}
if patch.Etag != nil {
s.Etag = *patch.Etag
}
if patch.RefreshIntervalSec != nil {
s.RefreshIntervalSec = patch.RefreshIntervalSec
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
s.CommunityID = nil
} else {
s.CommunityID = &v
}
}
if patch.LastRefreshedAt != nil {
t := patch.LastRefreshedAt.UTC()
s.LastRefreshedAt = &t
}
return s, nil
}
func (m *Memory) DeleteCDNSource(tenantID, moduleID, sourceID string) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
return err
}
s, ok := m.cdnSources[sourceID]
if !ok || s.ModuleID != moduleID {
return ErrNotFound
}
delete(m.cdnSources, sourceID)
return nil
}
func (m *Memory) ListASEntries(tenantID, moduleID string) ([]*ASEntry, error) {
m.mu.RLock()
defer m.mu.RUnlock()
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "AS_PREFIXES" {
return nil, ErrInvalidInput
}
var out []*ASEntry
for _, e := range m.asEntries {
if e.ModuleID == moduleID {
out = append(out, e)
}
}
return out, nil
}
func (m *Memory) CreateASEntry(tenantID, moduleID string, in *ASEntry) (*ASEntry, error) {
if in == nil || !ValidASN(in.ASN) {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "AS_PREFIXES" {
return nil, ErrInvalidInput
}
id := uuid.NewString()
e := &ASEntry{ID: id, ModuleID: moduleID, ASN: in.ASN, CommunityID: in.CommunityID}
m.asEntries[id] = e
return e, nil
}
func (m *Memory) UpdateASEntry(tenantID, moduleID, entryID string, patch *ASEntryPatch) (*ASEntry, error) {
if patch == nil {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
return nil, err
}
e, ok := m.asEntries[entryID]
if !ok || e.ModuleID != moduleID {
return nil, ErrNotFound
}
prevASN := e.ASN
if patch.ASN != nil {
e.ASN = *patch.ASN
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
e.CommunityID = nil
} else {
e.CommunityID = &v
}
}
if !ValidASN(e.ASN) {
return nil, ErrInvalidInput
}
if patch.ASN != nil && e.ASN != prevASN {
e.ASNName = ""
e.PrefixCount = nil
e.ASNResolvedAt = nil
}
return e, nil
}
func (m *Memory) UpdateASEntryResolveMeta(tenantID, moduleID, entryID string, asnName string, prefixCount int64, resolvedAt time.Time) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
return err
}
e, ok := m.asEntries[entryID]
if !ok || e.ModuleID != moduleID {
return ErrNotFound
}
e.ASNName = strings.TrimSpace(asnName)
pc := prefixCount
e.PrefixCount = &pc
t := resolvedAt.UTC()
e.ASNResolvedAt = &t
return nil
}
func (m *Memory) DeleteASEntry(tenantID, moduleID, entryID string) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
return err
}
e, ok := m.asEntries[entryID]
if !ok || e.ModuleID != moduleID {
return ErrNotFound
}
delete(m.asEntries, entryID)
return nil
}
func (m *Memory) ListDomainEntries(tenantID, moduleID string) ([]*DomainEntry, error) {
m.mu.RLock()
defer m.mu.RUnlock()
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "DOMAINS" {
return nil, ErrInvalidInput
}
var out []*DomainEntry
for _, e := range m.domainEnt {
if e.ModuleID == moduleID {
out = append(out, e)
}
}
return out, nil
}
func (m *Memory) CreateDomainEntry(tenantID, moduleID string, in *DomainEntry) (*DomainEntry, error) {
if in == nil || strings.TrimSpace(in.FQDN) == "" {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "DOMAINS" {
return nil, ErrInvalidInput
}
id := uuid.NewString()
e := &DomainEntry{ID: id, ModuleID: moduleID, FQDN: strings.TrimSpace(in.FQDN), CommunityID: in.CommunityID}
m.domainEnt[id] = e
return e, nil
}
func (m *Memory) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *DomainEntryPatch) (*DomainEntry, error) {
if patch == nil {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
return nil, err
}
e, ok := m.domainEnt[entryID]
if !ok || e.ModuleID != moduleID {
return nil, ErrNotFound
}
if patch.FQDN != nil {
e.FQDN = strings.TrimSpace(*patch.FQDN)
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
e.CommunityID = nil
} else {
e.CommunityID = &v
}
}
return e, nil
}
func (m *Memory) DeleteDomainEntry(tenantID, moduleID, entryID string) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
return err
}
e, ok := m.domainEnt[entryID]
if !ok || e.ModuleID != moduleID {
return ErrNotFound
}
delete(m.domainEnt, entryID)
return nil
}
func (m *Memory) ListIPRangeEntries(tenantID, moduleID string) ([]*IPRangeEntry, error) {
m.mu.RLock()
defer m.mu.RUnlock()
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "IP_RANGES" {
return nil, ErrInvalidInput
}
var out []*IPRangeEntry
for _, e := range m.ipRanges {
if e.ModuleID == moduleID {
out = append(out, e)
}
}
return out, nil
}
func (m *Memory) CreateIPRangeEntry(tenantID, moduleID string, in *IPRangeEntry) (*IPRangeEntry, error) {
if in == nil || strings.TrimSpace(in.Prefix) == "" {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "IP_RANGES" {
return nil, ErrInvalidInput
}
id := uuid.NewString()
e := &IPRangeEntry{ID: id, ModuleID: moduleID, Prefix: strings.TrimSpace(in.Prefix), CommunityID: in.CommunityID}
m.ipRanges[id] = e
return e, nil
}
func (m *Memory) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *IPRangePatch) (*IPRangeEntry, error) {
if patch == nil {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
return nil, err
}
e, ok := m.ipRanges[entryID]
if !ok || e.ModuleID != moduleID {
return nil, ErrNotFound
}
if patch.Prefix != nil {
e.Prefix = strings.TrimSpace(*patch.Prefix)
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
e.CommunityID = nil
} else {
e.CommunityID = &v
}
}
return e, nil
}
func (m *Memory) DeleteIPRangeEntry(tenantID, moduleID, entryID string) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
return err
}
e, ok := m.ipRanges[entryID]
if !ok || e.ModuleID != moduleID {
return ErrNotFound
}
delete(m.ipRanges, entryID)
return nil
}
func (m *Memory) ListDohProfiles(tenantID string) ([]*DohProfile, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var out []*DohProfile
for _, p := range m.dohProfiles {
if p.TenantID == tenantID {
out = append(out, p)
}
}
return out, nil
}
func (m *Memory) GetDohProfile(tenantID, id string) (*DohProfile, error) {
m.mu.RLock()
defer m.mu.RUnlock()
p, ok := m.dohProfiles[id]
if !ok || p.TenantID != tenantID {
return nil, ErrNotFound
}
return p, nil
}
func (m *Memory) CreateDohProfile(tenantID string, in *DohProfile) (*DohProfile, error) {
if in == nil || strings.TrimSpace(in.URL) == "" {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.tenants[tenantID]; !ok {
return nil, ErrTenantScope
}
id := uuid.NewString()
p := &DohProfile{
ID: id,
TenantID: tenantID,
Name: in.Name,
URL: strings.TrimSpace(in.URL),
TimeoutMs: in.TimeoutMs,
SecretRef: in.SecretRef,
}
m.dohProfiles[id] = p
return p, nil
}
func (m *Memory) UpdateDohProfile(tenantID, id string, patch *DohProfilePatch) (*DohProfile, error) {
if patch == nil {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
p, ok := m.dohProfiles[id]
if !ok || p.TenantID != tenantID {
return nil, ErrNotFound
}
if patch.Name != nil {
p.Name = *patch.Name
}
if patch.URL != nil {
p.URL = strings.TrimSpace(*patch.URL)
}
if patch.TimeoutMs != nil {
p.TimeoutMs = patch.TimeoutMs
}
if patch.SecretRef != nil {
p.SecretRef = patch.SecretRef
}
return p, nil
}
func (m *Memory) DeleteDohProfile(tenantID, id string) error {
m.mu.Lock()
defer m.mu.Unlock()
p, ok := m.dohProfiles[id]
if !ok || p.TenantID != tenantID {
return ErrNotFound
}
for _, mod := range m.modules {
if mod.DohProfileID != nil && *mod.DohProfileID == id {
return ErrInvalidInput
}
}
delete(m.dohProfiles, id)
return nil
}
func (m *Memory) ListCommunities(tenantID string) ([]*Community, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var out []*Community
for _, c := range m.communities {
if c.TenantID == tenantID {
out = append(out, c)
}
}
return out, nil
}
func (m *Memory) GetCommunity(tenantID, id string) (*Community, error) {
m.mu.RLock()
defer m.mu.RUnlock()
c, ok := m.communities[id]
if !ok || c.TenantID != tenantID {
return nil, ErrNotFound
}
return c, nil
}
func (m *Memory) CreateCommunity(tenantID string, in *Community) (*Community, error) {
if in == nil || strings.TrimSpace(in.Community) == "" {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.tenants[tenantID]; !ok {
return nil, ErrTenantScope
}
id := uuid.NewString()
vj := in.ValueJSON
if strings.TrimSpace(vj) == "" {
vj = "{}"
}
c := &Community{ID: id, TenantID: tenantID, Community: strings.TrimSpace(in.Community), Title: strings.TrimSpace(in.Title), ValueJSON: vj}
m.communities[id] = c
return c, nil
}
func (m *Memory) UpdateCommunity(tenantID, id string, patch *CommunityPatch) (*Community, error) {
if patch == nil {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
c, ok := m.communities[id]
if !ok || c.TenantID != tenantID {
return nil, ErrNotFound
}
if patch.Community != nil {
c.Community = strings.TrimSpace(*patch.Community)
}
if patch.Title != nil {
c.Title = strings.TrimSpace(*patch.Title)
}
if patch.ValueJSON != nil {
c.ValueJSON = *patch.ValueJSON
}
return c, nil
}
func (m *Memory) DeleteCommunity(tenantID, id string) error {
m.mu.Lock()
defer m.mu.Unlock()
c, ok := m.communities[id]
if !ok || c.TenantID != tenantID {
return ErrNotFound
}
_ = c
// weak check: modules default_community
for _, mod := range m.modules {
if mod.DefaultCommunityID != nil && *mod.DefaultCommunityID == id {
return ErrInvalidInput
}
}
delete(m.communities, id)
return nil
}
func (m *Memory) GetPeer(tenantID, id string) (*BGPPeer, error) {
m.mu.RLock()
defer m.mu.RUnlock()
p, ok := m.peers[id]
if !ok || p.TenantID != tenantID {
return nil, ErrNotFound
}
return p, nil
}
func (m *Memory) CreatePeer(tenantID string, in *BGPPeer) (*BGPPeer, error) {
if in == nil || in.RemoteASN == 0 {
return nil, ErrInvalidInput
}
neighbor, ok := NormalizePeerNeighborString(in.Neighbor)
if !ok {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.tenants[tenantID]; !ok {
return nil, ErrTenantScope
}
id := uuid.NewString()
p := &BGPPeer{
ID: id, TenantID: tenantID, SpeakerID: in.SpeakerID, Name: in.Name,
Neighbor: neighbor, RemoteASN: in.RemoteASN,
Enabled: EffectivePeerEnabledOnCreate(in.Enabled, in.SessionState),
SessionState: in.SessionState, PoliciesJSON: in.PoliciesJSON,
}
m.peers[id] = p
return p, nil
}
func (m *Memory) UpdatePeer(tenantID, id string, patch *PeerPatch) (*BGPPeer, error) {
if patch == nil {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
p, ok := m.peers[id]
if !ok || p.TenantID != tenantID {
return nil, ErrNotFound
}
if patch.Neighbor != nil {
n, ok := NormalizePeerNeighborString(*patch.Neighbor)
if !ok {
return nil, ErrInvalidInput
}
p.Neighbor = n
}
if patch.RemoteASN != nil {
p.RemoteASN = *patch.RemoteASN
}
if patch.SpeakerID != nil {
v := strings.TrimSpace(*patch.SpeakerID)
if v == "" {
p.SpeakerID = nil
} else {
p.SpeakerID = &v
}
}
if patch.Enabled != nil {
p.Enabled = *patch.Enabled
}
if patch.Name != nil {
p.Name = *patch.Name
}
if patch.SessionState != nil {
p.SessionState = *patch.SessionState
}
if patch.PoliciesJSON != nil {
p.PoliciesJSON = *patch.PoliciesJSON
}
return p, nil
}
func (m *Memory) DeletePeer(tenantID, id string) error {
m.mu.Lock()
defer m.mu.Unlock()
p, ok := m.peers[id]
if !ok || p.TenantID != tenantID {
return ErrNotFound
}
delete(m.peers, id)
_ = p
return nil
}
func (m *Memory) CreateSpeaker(tenantID string, in *Speaker) (*Speaker, error) {
if in == nil || strings.TrimSpace(in.Role) == "" {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.tenants[tenantID]; !ok {
return nil, ErrTenantScope
}
id := uuid.NewString()
sp := &Speaker{ID: id, TenantID: tenantID, Role: strings.TrimSpace(in.Role), Endpoint: in.Endpoint, MetaJSON: in.MetaJSON}
m.speakers[id] = sp
return sp, nil
}
func (m *Memory) UpdateSpeaker(tenantID, id string, patch *SpeakerPatch) (*Speaker, error) {
if patch == nil {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
sp, ok := m.speakers[id]
if !ok || sp.TenantID != tenantID {
return nil, ErrNotFound
}
if patch.Role != nil {
sp.Role = strings.TrimSpace(*patch.Role)
}
if patch.Endpoint != nil {
sp.Endpoint = *patch.Endpoint
}
if patch.MetaJSON != nil {
sp.MetaJSON = *patch.MetaJSON
}
return sp, nil
}
func (m *Memory) ListRevisionPrefixes(tenantID, revisionID string, cursor string, limit int) ([]PrefixRow, string, bool) {
if limit <= 0 {
limit = 50
}
m.mu.RLock()
defer m.mu.RUnlock()
if _, err := m.getRevisionLocked(tenantID, revisionID); err != nil {
return nil, "", false
}
all := m.revPrefixes[revisionID]
off := 0
if cursor != "" {
if n, err := strconv.Atoi(cursor); err == nil && n >= 0 {
off = n
}
}
end := off + limit
next := ""
more := false
if end > len(all) {
end = len(all)
} else {
more = true
next = fmt.Sprintf("%d", end)
}
if off >= len(all) {
return nil, "", false
}
return all[off:end], next, more
}
func (m *Memory) ListGlobalSettings(tenantID string) (map[string]any, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.settings[tenantID] == nil {
return map[string]any{}, nil
}
out := make(map[string]any, len(m.settings[tenantID]))
for k, v := range m.settings[tenantID] {
out[k] = v
}
return out, nil
}
func (m *Memory) PatchGlobalSettings(tenantID string, patch map[string]any) error {
if patch == nil {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.tenants[tenantID]; !ok {
return ErrTenantScope
}
if m.settings[tenantID] == nil {
m.settings[tenantID] = make(map[string]any)
}
for key, val := range patch {
if strings.TrimSpace(key) == "" {
continue
}
if val == nil {
delete(m.settings[tenantID], key)
continue
}
m.settings[tenantID][key] = val
}
return nil
}