feat(core): add configuration, logging and sqlite persistence

DB_PATH, HTTP_ADDR, HTTP_PORT and LOG_LEVEL parsing with validation; slog text logger with a TRACE level and token redaction; embedded idempotent schema on CGO-free SQLite (WAL, busy timeout); token store (create, list, deactivate, authenticate) and append-only audit store with ordered, limited and windowed queries; shared unit test helpers.
This commit is contained in:
bryanpedini committed 2026-10-06 10:52:25 +02:00
1 parent fe0cd4bf6d
commit 88d7d6d2b9
11 files changed
+2426

No files matched your search

+118
View File
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 Bryan Joshua Pedini
package main
import (
"context"
"database/sql"
_ "embed"
"errors"
"fmt"
"log/slog"
"path/filepath"
"time"
// Pure-Go SQLite driver, registered as "sqlite"
"modernc.org/sqlite"
)
// schemaSQL holds the authoritative, idempotent table definitions
//
//go:embed schema.sql
var schemaSQL string
// dsnParams are applied by the driver to every pooled connection
// foreign_keys: enforce the operations -> tokens reference
// busy_timeout: wait instead of failing when the server and the CLI write concurrently
// journal_mode WAL: readers never block the writer
// _txlock=immediate: transactions take the write lock at BEGIN, so read-then-write never fails with SQLITE_BUSY
const dsnParams = "_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_txlock=immediate"
// maxOpenConns bounds the SQLite connection pool
const maxOpenConns = 4
// sqliteDSN builds the driver DSN for a filesystem path
// The path is made absolute and percent-encoded so that spaces, '?', '#', '%' and other bytes are taken literally
func sqliteDSN(path string) (string, error) {
abs, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("resolve database path: %w", err)
}
return "file://" + escapeURIPath(abs) + "?" + dsnParams, nil
}
// escapeURIPath percent-encodes every byte except RFC 3986 unreserved characters and '/'
func escapeURIPath(p string) string {
const hexDigits = "0123456789ABCDEF"
out := make([]byte, 0, len(p))
for i := 0; i < len(p); i++ {
c := p[i]
switch {
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9',
c == '-', c == '.', c == '_', c == '~', c == '/':
out = append(out, c)
default:
out = append(out, '%', hexDigits[c>>4], hexDigits[c&0x0f])
}
}
return string(out)
}
// OpenDatabase opens (creating if needed) the SQLite database at path and initializes the schema
func OpenDatabase(ctx context.Context, path string, log *slog.Logger) (*sql.DB, error) {
defer traceFunc(ctx, log, "OpenDatabase")()
// Build the DSN
dsn, err := sqliteDSN(path)
if err != nil {
return nil, err
}
// Open the connection pool (lazy: the file is opened on first use)
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
db.SetMaxOpenConns(maxOpenConns)
db.SetMaxIdleConns(maxOpenConns)
// Create the tables if needed
if err := InitializeSchema(ctx, db, log); err != nil {
db.Close()
return nil, err
}
log.DebugContext(ctx, "database opened", "path", path)
return db, nil
}
// InitializeSchema executes the embedded schema; every statement is idempotent
func InitializeSchema(ctx context.Context, db *sql.DB, log *slog.Logger) error {
defer traceFunc(ctx, log, "InitializeSchema")()
if _, err := db.ExecContext(ctx, schemaSQL); err != nil {
return fmt.Errorf("initialize database schema: %w", err)
}
log.DebugContext(ctx, "database schema initialized")
return nil
}
// isSQLiteCode reports whether err is a SQLite error with the given (extended) result code
func isSQLiteCode(err error, code int) bool {
var se *sqlite.Error
return errors.As(err, &se) && se.Code() == code
}
// formatTime renders a timestamp as stored in the database: UTC RFC3339Nano
func formatTime(t time.Time) string {
return t.UTC().Format(time.RFC3339Nano)
}
// parseTime parses a timestamp stored by formatTime
func parseTime(s string) (time.Time, error) {
t, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
return time.Time{}, fmt.Errorf("parse stored timestamp: %w", err)
}
return t.UTC(), nil
}