initial commit
This commit is contained in:
commit
ebd5bb09fd
30 changed files with 1920 additions and 0 deletions
89
internal/config/config.go
Normal file
89
internal/config/config.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
JWT JWTConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading config file: %w", err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parsing config file: %w", err)
|
||||
}
|
||||
|
||||
cfg.applyDefaults()
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func LoadFromEnv() (*Config, error) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if host := os.Getenv("GO_MAD_SERVER_HOST"); host != "" {
|
||||
cfg.Server.Host = host
|
||||
}
|
||||
if port := os.Getenv("GO_MAD_SERVER_PORT"); port != "" {
|
||||
cfg.Server.Port = port
|
||||
}
|
||||
if dbURL := os.Getenv("GO_MAD_DATABASE_URL"); dbURL != "" {
|
||||
cfg.Database.URL = dbURL
|
||||
}
|
||||
if jwtSecret := os.Getenv("GO_MAD_JWT_SECRET"); jwtSecret != "" {
|
||||
cfg.JWT.SecretKey = jwtSecret
|
||||
}
|
||||
|
||||
cfg.applyDefaults()
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Host: "0.0.0.0",
|
||||
Port: "8080",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
URL: "postgres://mad:mad@localhost:5432/mad?sslmode=disable",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
SecretKey: "change-me-to-a-32-plus-char-secret",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) applyDefaults() {
|
||||
if c.Server.Port == "" {
|
||||
c.Server.Port = "8080"
|
||||
}
|
||||
if c.Database.URL == "" {
|
||||
c.Database.URL = "postgres://mad:mad@localhost:5432/mad?sslmode=disable"
|
||||
}
|
||||
if c.JWT.SecretKey == "" {
|
||||
c.JWT.SecretKey = "change-me-to-a-32-plus-char-secret"
|
||||
}
|
||||
}
|
||||
171
internal/database/database.go
Normal file
171
internal/database/database.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
func Open(databaseURL string) (*sql.DB, error) {
|
||||
db, err := sql.Open("pgx", databaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening database: %w", err)
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(20)
|
||||
db.SetMaxIdleConns(5)
|
||||
db.SetConnMaxLifetime(30 * 60)
|
||||
|
||||
if err := db.PingContext(context.Background()); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("pinging database: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Database connection opened successfully")
|
||||
return db, nil
|
||||
}
|
||||
|
||||
type Migration struct {
|
||||
Version string
|
||||
Description string
|
||||
SQL string
|
||||
}
|
||||
|
||||
func FindMigrations(migrationPath string) ([]Migration, error) {
|
||||
var migrations []Migration
|
||||
|
||||
entries, err := os.ReadDir(migrationPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading migration directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(migrationPath, entry.Name())
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading migration file %s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
migration := parseMigration(contentStr)
|
||||
migrations = append(migrations, migration)
|
||||
}
|
||||
|
||||
return migrations, nil
|
||||
}
|
||||
|
||||
func parseMigration(content string) Migration {
|
||||
// Parse goose-style migration: -- +goose Up / -- +goose Down
|
||||
lines := strings.Split(content, "\n")
|
||||
var upSQL strings.Builder
|
||||
inUp := false
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.Contains(trimmed, "-- +goose Up") {
|
||||
inUp = true
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, "-- +goose Down") {
|
||||
inUp = false
|
||||
continue
|
||||
}
|
||||
|
||||
if inUp {
|
||||
upSQL.WriteString(line)
|
||||
upSQL.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Extract version from filename (first part before _)
|
||||
version := ""
|
||||
desc := ""
|
||||
if idx := strings.Index(content, "_"); idx >= 0 {
|
||||
version = content[:idx]
|
||||
desc = content[idx+1 : strings.Index(content, "_")] // simplistic
|
||||
}
|
||||
|
||||
return Migration{
|
||||
Version: version,
|
||||
Description: desc,
|
||||
SQL: upSQL.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func Migrate(db *sql.DB, migrationPath string) error {
|
||||
migrations, err := FindMigrations(migrationPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding migrations: %w", err)
|
||||
}
|
||||
|
||||
if len(migrations) == 0 {
|
||||
log.Println("No migration files found")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Found %d migration(s), running up...", len(migrations))
|
||||
|
||||
// Ensure schema_migrations table exists
|
||||
_, err = db.ExecContext(context.Background(), `
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version VARCHAR(255) PRIMARY KEY,
|
||||
description VARCHAR(255),
|
||||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating schema_migrations table: %w", err)
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
// Check if already applied
|
||||
var count int
|
||||
err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM schema_migrations WHERE version = $1", m.Version).Scan(&count)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking migration %s: %w", m.Version, err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
log.Printf("Migration %s already applied, skipping", m.Version)
|
||||
continue
|
||||
}
|
||||
|
||||
// Execute the up SQL
|
||||
tx, err := db.BeginTx(context.Background(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("beginning transaction for migration %s: %w", m.Version, err)
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(context.Background(), m.SQL)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("executing migration %s: %w", m.Version, err)
|
||||
}
|
||||
|
||||
// Record the migration as applied
|
||||
_, err = tx.ExecContext(context.Background(), "INSERT INTO schema_migrations (version, description) VALUES ($1, $2)", m.Version, m.Description)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("recording migration %s: %w", m.Version, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("committing migration %s: %w", m.Version, err)
|
||||
}
|
||||
|
||||
log.Printf("Applied migration %s: %s", m.Version, m.Description)
|
||||
}
|
||||
|
||||
log.Println("Migrations completed successfully")
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue