mirror of
https://github.com/DeBrosOfficial/network.git
synced 2025-10-06 12:29:07 +00:00
153 lines
5.0 KiB
Go
153 lines
5.0 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.debros.io/DeBros/network/pkg/logging"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var errNoMigrationsFound = errors.New("no migrations found")
|
|
|
|
func (g *Gateway) applyAutoMigrations(ctx context.Context) error {
|
|
if g.client == nil {
|
|
return nil
|
|
}
|
|
db := g.client.Database()
|
|
|
|
stmts := []string{
|
|
// namespaces
|
|
"CREATE TABLE IF NOT EXISTS namespaces (\n\t id INTEGER PRIMARY KEY AUTOINCREMENT,\n\t name TEXT NOT NULL UNIQUE,\n\t created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n)",
|
|
// api_keys
|
|
"CREATE TABLE IF NOT EXISTS api_keys (\n\t id INTEGER PRIMARY KEY AUTOINCREMENT,\n\t key TEXT NOT NULL UNIQUE,\n\t name TEXT,\n\t namespace_id INTEGER NOT NULL,\n\t scopes TEXT,\n\t created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\t last_used_at TIMESTAMP,\n\t FOREIGN KEY(namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE\n)",
|
|
"CREATE INDEX IF NOT EXISTS idx_api_keys_namespace ON api_keys(namespace_id)",
|
|
// request_logs
|
|
"CREATE TABLE IF NOT EXISTS request_logs (\n\t id INTEGER PRIMARY KEY AUTOINCREMENT,\n\t method TEXT NOT NULL,\n\t path TEXT NOT NULL,\n\t status_code INTEGER NOT NULL,\n\t bytes_out INTEGER NOT NULL DEFAULT 0,\n\t duration_ms INTEGER NOT NULL DEFAULT 0,\n\t ip TEXT,\n\t api_key_id INTEGER,\n\t created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\t FOREIGN KEY(api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL\n)",
|
|
"CREATE INDEX IF NOT EXISTS idx_request_logs_api_key ON request_logs(api_key_id)",
|
|
"CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON request_logs(created_at)",
|
|
// seed default namespace
|
|
"INSERT OR IGNORE INTO namespaces(name) VALUES ('default')",
|
|
}
|
|
|
|
for _, s := range stmts {
|
|
if _, err := db.Query(ctx, s); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *Gateway) applyMigrations(ctx context.Context) error {
|
|
if g.client == nil {
|
|
return nil
|
|
}
|
|
db := g.client.Database()
|
|
|
|
// Ensure schema_migrations exists first
|
|
if _, err := db.Query(ctx, "CREATE TABLE IF NOT EXISTS schema_migrations (\n\tversion INTEGER PRIMARY KEY,\n\tapplied_at TIMESTAMP NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))\n)"); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Locate migrations directory relative to CWD
|
|
migDir := "migrations"
|
|
if fi, err := os.Stat(migDir); err != nil || !fi.IsDir() {
|
|
return errNoMigrationsFound
|
|
}
|
|
|
|
entries, err := os.ReadDir(migDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
type mig struct{ ver int; path string }
|
|
migrations := make([]mig, 0)
|
|
for _, e := range entries {
|
|
if e.IsDir() { continue }
|
|
name := e.Name()
|
|
if !strings.HasSuffix(strings.ToLower(name), ".sql") { continue }
|
|
if ver, ok := parseMigrationVersion(name); ok {
|
|
migrations = append(migrations, mig{ver: ver, path: filepath.Join(migDir, name)})
|
|
}
|
|
}
|
|
if len(migrations) == 0 {
|
|
return errNoMigrationsFound
|
|
}
|
|
sort.Slice(migrations, func(i, j int) bool { return migrations[i].ver < migrations[j].ver })
|
|
|
|
// Helper to check if version applied
|
|
isApplied := func(ctx context.Context, v int) (bool, error) {
|
|
res, err := db.Query(ctx, "SELECT 1 FROM schema_migrations WHERE version = ? LIMIT 1", v)
|
|
if err != nil { return false, err }
|
|
return res != nil && res.Count > 0, nil
|
|
}
|
|
|
|
for _, m := range migrations {
|
|
applied, err := isApplied(ctx, m.ver)
|
|
if err != nil { return err }
|
|
if applied {
|
|
continue
|
|
}
|
|
// Read and split SQL file into statements
|
|
content, err := os.ReadFile(m.path)
|
|
if err != nil { return err }
|
|
stmts := splitSQLStatements(string(content))
|
|
for _, s := range stmts {
|
|
if s == "" { continue }
|
|
if _, err := db.Query(ctx, s); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Mark as applied
|
|
if _, err := db.Query(ctx, "INSERT OR IGNORE INTO schema_migrations(version) VALUES (?)", m.ver); err != nil {
|
|
return err
|
|
}
|
|
g.logger.ComponentInfo(logging.ComponentDatabase, "applied migration", zap.Int("version", m.ver), zap.String("file", m.path))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseMigrationVersion(name string) (int, bool) {
|
|
i := 0
|
|
for i < len(name) && name[i] >= '0' && name[i] <= '9' {
|
|
i++
|
|
}
|
|
if i == 0 { return 0, false }
|
|
v, err := strconv.Atoi(name[:i])
|
|
if err != nil { return 0, false }
|
|
return v, true
|
|
}
|
|
|
|
func splitSQLStatements(sqlText string) []string {
|
|
lines := strings.Split(sqlText, "\n")
|
|
cleaned := make([]string, 0, len(lines))
|
|
for _, ln := range lines {
|
|
s := strings.TrimSpace(ln)
|
|
if s == "" { continue }
|
|
if strings.HasPrefix(s, "--") { continue }
|
|
upper := strings.ToUpper(s)
|
|
if upper == "BEGIN;" || upper == "COMMIT;" || upper == "BEGIN" || upper == "COMMIT" {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(upper, "INSERT") && strings.Contains(upper, "SCHEMA_MIGRATIONS") {
|
|
// ignore in-file migration markers
|
|
continue
|
|
}
|
|
cleaned = append(cleaned, s)
|
|
}
|
|
// Join and split by ';'
|
|
joined := strings.Join(cleaned, "\n")
|
|
parts := strings.Split(joined, ";")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
sp := strings.TrimSpace(p)
|
|
if sp == "" { continue }
|
|
out = append(out, sp)
|
|
}
|
|
return out
|
|
}
|