Netpulse_SasS/server/internal/store/store.go
byrsapty 65b37a28ff
Some checks are pending
CI / web (push) Waiting to run
CI / server (push) Waiting to run
CI / agent (push) Waiting to run
Git-двигун NCM: версіювання конфігів, переливання історії, довільне порівняння
go-git, чистий Go без cgo. Голий репозиторій на тенанта, гілка на
пристрій за ідентифікатором (ім'я змінюють, історія не має від цього
розсипатись), файл за іменем. Однаковий вміст нового коміту не створює.

netpulse-gitsync переливає накопичену історію й відтворює втрачений
репозиторій із бази — тіла конфігів там і так лежать зашифрованими.

У вебі з'явився вибір версії, з якою порівнювати: сервер це вмів
(?from=), інтерфейс — ні.

Дорогою виправлено чотири тести, які CI запустив уперше з базою.
Серед них справжня помилка: машинний токен не міг читати мапи —
ACL мап отримував порожній рядок замість uuid і давав 500.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:14 +03:00

101 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package store — доступ до PostgreSQL/TimescaleDB.
//
// Головне правило шару: жоден запит не виконується без tenant_id.
// Ізоляція забезпечується двома незалежними механізмами, і це навмисно:
//
// 1. RLS у БД — політика tenant_isolation на кожній звичайній таблиці
// з колонкою tenant_id. Вмикається через SET LOCAL app.tenant_id.
// 2. Явний предикат tenant_id у кожному запиті цього пакета.
//
// Дублювання потрібне, бо RLS НЕ працює на гіпертаблях: TimescaleDB
// не поєднує row level security зі стисненням. Саме туди йде вся
// телеметрія, тому для неї другий механізм — єдиний.
package store
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/netpulse/netpulse/server/internal/gitstore"
)
type Store struct {
pool *pgxpool.Pool
// Версіювання конфігів. Не обов'язкове: без нього NCM працює як і
// раніше — тіла лежать зашифрованими в базі, а замість sha коміту
// пишеться хеш вмісту. Порожнє значення тут — робочий стан, а не
// недороблена ініціалізація.
git *gitstore.Store
}
// UseGit під'єднує сховище версій. Окремим викликом, а не параметром
// New: конструктор потрібен і там, де Git не потрібен зовсім, — у
// міграторі, в утилітах командного рядка, у тестах.
func (s *Store) UseGit(g *gitstore.Store) { s.git = g }
func New(ctx context.Context, dsn string) (*Store, error) {
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("розбір DSN: %w", err)
}
cfg.MaxConns = 16
cfg.MinConns = 2
cfg.MaxConnLifetime = time.Hour
cfg.MaxConnIdleTime = 10 * time.Minute
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
return nil, err
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping: %w", err)
}
return &Store{pool: pool}, nil
}
func (s *Store) Close() { s.pool.Close() }
func (s *Store) Pool() *pgxpool.Pool { return s.pool }
// InTenantTx виконує fn у транзакції з виставленим app.tenant_id.
//
// SET LOCAL, а не SET: значення живе рівно до кінця транзакції й не
// протікає на наступний запит, який візьме те саме з'єднання з пулу.
// Протікання тут означало б показ чужих даних, тому це не оптимізація,
// а вимога.
func (s *Store) InTenantTx(ctx context.Context, tenantID string, fn func(pgx.Tx) error) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
if _, err := tx.Exec(ctx, "SELECT set_config('app.tenant_id', $1, true)", tenantID); err != nil {
return fmt.Errorf("встановити app.tenant_id: %w", err)
}
if err := fn(tx); err != nil {
return err
}
return tx.Commit(ctx)
}
// nullUUID перетворює порожній рядок на NULL — protobuf не має
// nullable-рядків, тому "" з дроту означає «не задано».
func nullUUID(s string) any {
if s == "" {
return nil
}
return s
}
func nullString(s string) any {
if s == "" {
return nil
}
return s
}