Add UUID/FK channel checks, agent whitelist/push APIs, bindings, super-admin LWW audit with rollback, reconcile rate limits, and sync docs. Default customers stay opt-in; company conflict UI is removed. Co-authored-by: Cursor <cursoragent@cursor.com>
144 lines
3.3 KiB
Go
144 lines
3.3 KiB
Go
package dbsync
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// Binding 本机库 ↔ 线上库映射(P1;不挡推送,供登记/查询)。
|
||
type Binding struct {
|
||
ID string `json:"id"`
|
||
TenantID int64 `json:"tenant_id"`
|
||
UserID int64 `json:"user_id,omitempty"`
|
||
LocalDatabaseID string `json:"local_database_id"`
|
||
OnlineDBID string `json:"online_db_id"`
|
||
ChannelID string `json:"channel_id,omitempty"`
|
||
Note string `json:"note,omitempty"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
func (s *FileStore) bindingPath() string {
|
||
return filepath.Join(s.dir, "bindings.json")
|
||
}
|
||
|
||
func (s *FileStore) EnsureBinding(b Binding) (Binding, error) {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
list, err := s.readBindingsUnlocked()
|
||
if err != nil {
|
||
return b, err
|
||
}
|
||
b.LocalDatabaseID = strings.TrimSpace(b.LocalDatabaseID)
|
||
b.OnlineDBID = strings.TrimSpace(b.OnlineDBID)
|
||
if b.TenantID <= 0 {
|
||
return b, fmt.Errorf("tenant_id required")
|
||
}
|
||
if b.LocalDatabaseID == "" || b.OnlineDBID == "" {
|
||
return b, fmt.Errorf("local_database_id and online_db_id required")
|
||
}
|
||
now := time.Now().UTC()
|
||
for i := range list {
|
||
if list[i].TenantID == b.TenantID && list[i].LocalDatabaseID == b.LocalDatabaseID {
|
||
if b.UserID > 0 && list[i].UserID > 0 && list[i].UserID != b.UserID {
|
||
continue
|
||
}
|
||
list[i].OnlineDBID = b.OnlineDBID
|
||
if b.ChannelID != "" {
|
||
list[i].ChannelID = b.ChannelID
|
||
}
|
||
if b.Note != "" {
|
||
list[i].Note = b.Note
|
||
}
|
||
if b.UserID > 0 {
|
||
list[i].UserID = b.UserID
|
||
}
|
||
list[i].UpdatedAt = now
|
||
if err := s.writeBindingsUnlocked(list); err != nil {
|
||
return b, err
|
||
}
|
||
return list[i], nil
|
||
}
|
||
}
|
||
if b.ID == "" {
|
||
b.ID = uuid.NewString()
|
||
}
|
||
b.CreatedAt = now
|
||
b.UpdatedAt = now
|
||
list = append(list, b)
|
||
if err := s.writeBindingsUnlocked(list); err != nil {
|
||
return b, err
|
||
}
|
||
return b, nil
|
||
}
|
||
|
||
func (s *FileStore) ListBindings(tenantID int64, localDatabaseID string) ([]Binding, error) {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
list, err := s.readBindingsUnlocked()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out := make([]Binding, 0)
|
||
for _, b := range list {
|
||
if b.TenantID != tenantID {
|
||
continue
|
||
}
|
||
if localDatabaseID != "" && b.LocalDatabaseID != localDatabaseID {
|
||
continue
|
||
}
|
||
out = append(out, b)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (s *FileStore) GetBinding(tenantID int64, localDatabaseID string) (*Binding, error) {
|
||
list, err := s.ListBindings(tenantID, localDatabaseID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(list) == 0 {
|
||
return nil, fmt.Errorf("binding not found")
|
||
}
|
||
cp := list[0]
|
||
return &cp, nil
|
||
}
|
||
|
||
func (s *FileStore) readBindingsUnlocked() ([]Binding, error) {
|
||
path := s.bindingPath()
|
||
b, err := os.ReadFile(path)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return []Binding{}, nil
|
||
}
|
||
return nil, err
|
||
}
|
||
if len(b) == 0 {
|
||
return []Binding{}, nil
|
||
}
|
||
var list []Binding
|
||
if err := json.Unmarshal(b, &list); err != nil {
|
||
return nil, err
|
||
}
|
||
return list, nil
|
||
}
|
||
|
||
func (s *FileStore) writeBindingsUnlocked(list []Binding) error {
|
||
path := s.bindingPath()
|
||
raw, err := json.MarshalIndent(list, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
tmp := path + ".tmp"
|
||
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
|
||
return err
|
||
}
|
||
return os.Rename(tmp, path)
|
||
}
|