This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

174 lines
4.3 KiB
Go

package db
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// Pool wraps a pgxpool.Pool with SDK-specific methods
type Pool struct {
*pgxpool.Pool
}
// SDKState represents the state stored in the database
type SDKState struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
UserID string `json:"user_id,omitempty"`
State json.RawMessage `json:"state"`
Version int `json:"version"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// NewPostgresPool creates a new database connection pool
func NewPostgresPool(connectionString string) (*Pool, error) {
config, err := pgxpool.ParseConfig(connectionString)
if err != nil {
return nil, fmt.Errorf("failed to parse connection string: %w", err)
}
config.MaxConns = 10
config.MinConns = 2
config.MaxConnLifetime = 1 * time.Hour
config.MaxConnIdleTime = 30 * time.Minute
pool, err := pgxpool.NewWithConfig(context.Background(), config)
if err != nil {
return nil, fmt.Errorf("failed to create connection pool: %w", err)
}
// Test connection
if err := pool.Ping(context.Background()); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return &Pool{Pool: pool}, nil
}
// GetState retrieves state for a tenant
func (p *Pool) GetState(ctx context.Context, tenantID string) (*SDKState, error) {
query := `
SELECT id, tenant_id, user_id, state, version, created_at, updated_at
FROM sdk_states
WHERE tenant_id = $1
`
var state SDKState
err := p.QueryRow(ctx, query, tenantID).Scan(
&state.ID,
&state.TenantID,
&state.UserID,
&state.State,
&state.Version,
&state.CreatedAt,
&state.UpdatedAt,
)
if err != nil {
return nil, err
}
return &state, nil
}
// SaveState saves or updates state for a tenant with optimistic locking
func (p *Pool) SaveState(ctx context.Context, tenantID string, userID string, state json.RawMessage, expectedVersion *int) (*SDKState, error) {
query := `
INSERT INTO sdk_states (tenant_id, user_id, state, version)
VALUES ($1, $2, $3, 1)
ON CONFLICT (tenant_id) DO UPDATE SET
state = $3,
user_id = COALESCE($2, sdk_states.user_id),
version = sdk_states.version + 1,
updated_at = NOW()
WHERE ($4::int IS NULL OR sdk_states.version = $4)
RETURNING id, tenant_id, user_id, state, version, created_at, updated_at
`
var result SDKState
err := p.QueryRow(ctx, query, tenantID, userID, state, expectedVersion).Scan(
&result.ID,
&result.TenantID,
&result.UserID,
&result.State,
&result.Version,
&result.CreatedAt,
&result.UpdatedAt,
)
if err != nil {
return nil, err
}
return &result, nil
}
// DeleteState deletes state for a tenant
func (p *Pool) DeleteState(ctx context.Context, tenantID string) error {
query := `DELETE FROM sdk_states WHERE tenant_id = $1`
_, err := p.Exec(ctx, query, tenantID)
return err
}
// InMemoryStore provides an in-memory fallback when database is not available
type InMemoryStore struct {
states map[string]*SDKState
}
// NewInMemoryStore creates a new in-memory store
func NewInMemoryStore() *InMemoryStore {
return &InMemoryStore{
states: make(map[string]*SDKState),
}
}
// GetState retrieves state from memory
func (s *InMemoryStore) GetState(tenantID string) (*SDKState, error) {
state, ok := s.states[tenantID]
if !ok {
return nil, fmt.Errorf("state not found")
}
return state, nil
}
// SaveState saves state to memory
func (s *InMemoryStore) SaveState(tenantID string, userID string, state json.RawMessage, expectedVersion *int) (*SDKState, error) {
existing, exists := s.states[tenantID]
// Optimistic locking check
if expectedVersion != nil && exists && existing.Version != *expectedVersion {
return nil, fmt.Errorf("version conflict")
}
now := time.Now()
version := 1
createdAt := now
if exists {
version = existing.Version + 1
createdAt = existing.CreatedAt
}
newState := &SDKState{
ID: fmt.Sprintf("%s-%d", tenantID, time.Now().UnixNano()),
TenantID: tenantID,
UserID: userID,
State: state,
Version: version,
CreatedAt: createdAt,
UpdatedAt: now,
}
s.states[tenantID] = newState
return newState, nil
}
// DeleteState deletes state from memory
func (s *InMemoryStore) DeleteState(tenantID string) error {
delete(s.states, tenantID)
return nil
}