// Command jetgen regenerates the go-jet/v2 query-builder code under // galaxy/gamemaster/internal/adapters/postgres/jet/ against a transient // PostgreSQL instance. // // The program is intended to be invoked as `go run ./cmd/jetgen` (or via // the `make jet` Makefile target) from within `galaxy/gamemaster`. It is // not part of the runtime binary. // // Steps: // // 1. start a postgres:16-alpine container via testcontainers-go // 2. open it through pkg/postgres as the superuser // 3. CREATE ROLE gamemasterservice and CREATE SCHEMA "gamemaster" // AUTHORIZATION gamemasterservice // 4. open a second pool as gamemasterservice with search_path=gamemaster // and apply the embedded goose migrations // 5. run jet's PostgreSQL generator against schema=gamemaster, writing // into ../internal/adapters/postgres/jet package main import ( "context" "errors" "fmt" "log" "net/url" "os" "path/filepath" "runtime" "time" "galaxy/postgres" "galaxy/gamemaster/internal/adapters/postgres/migrations" jetpostgres "github.com/go-jet/jet/v2/generator/postgres" testcontainers "github.com/testcontainers/testcontainers-go" tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" ) const ( postgresImage = "postgres:16-alpine" superuserName = "galaxy" superuserPassword = "galaxy" superuserDatabase = "galaxy_gamemaster" serviceRole = "gamemasterservice" servicePassword = "gamemasterservice" serviceSchema = "gamemaster" containerStartup = 90 * time.Second defaultOpTimeout = 10 * time.Second jetOutputDirSuffix = "internal/adapters/postgres/jet" ) func main() { if err := run(context.Background()); err != nil { log.Fatalf("jetgen: %v", err) } } func run(ctx context.Context) error { outputDir, err := jetOutputDir() if err != nil { return err } container, err := tcpostgres.Run(ctx, postgresImage, tcpostgres.WithDatabase(superuserDatabase), tcpostgres.WithUsername(superuserName), tcpostgres.WithPassword(superuserPassword), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). WithStartupTimeout(containerStartup), ), ) if err != nil { return fmt.Errorf("start postgres container: %w", err) } defer func() { if termErr := testcontainers.TerminateContainer(container); termErr != nil { log.Printf("jetgen: terminate container: %v", termErr) } }() baseDSN, err := container.ConnectionString(ctx, "sslmode=disable") if err != nil { return fmt.Errorf("resolve container dsn: %w", err) } if err := provisionRoleAndSchema(ctx, baseDSN); err != nil { return err } scopedDSN, err := dsnForServiceRole(baseDSN) if err != nil { return err } if err := applyMigrations(ctx, scopedDSN); err != nil { return err } if err := os.RemoveAll(outputDir); err != nil { return fmt.Errorf("remove existing jet output %q: %w", outputDir, err) } if err := os.MkdirAll(filepath.Dir(outputDir), 0o755); err != nil { return fmt.Errorf("ensure jet output parent: %w", err) } jetCfg := postgres.DefaultConfig() jetCfg.PrimaryDSN = scopedDSN jetCfg.OperationTimeout = defaultOpTimeout jetDB, err := postgres.OpenPrimary(ctx, jetCfg) if err != nil { return fmt.Errorf("open scoped pool for jet generation: %w", err) } defer func() { _ = jetDB.Close() }() if err := jetpostgres.GenerateDB(jetDB, serviceSchema, outputDir); err != nil { return fmt.Errorf("jet generate: %w", err) } log.Printf("jetgen: generated jet code into %s (schema=%s)", outputDir, serviceSchema) return nil } func provisionRoleAndSchema(ctx context.Context, baseDSN string) error { cfg := postgres.DefaultConfig() cfg.PrimaryDSN = baseDSN cfg.OperationTimeout = defaultOpTimeout db, err := postgres.OpenPrimary(ctx, cfg) if err != nil { return fmt.Errorf("open admin pool: %w", err) } defer func() { _ = db.Close() }() statements := []string{ fmt.Sprintf(`DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = %s) THEN CREATE ROLE %s LOGIN PASSWORD %s; END IF; END $$;`, sqlLiteral(serviceRole), sqlIdentifier(serviceRole), sqlLiteral(servicePassword)), fmt.Sprintf(`CREATE SCHEMA IF NOT EXISTS %s AUTHORIZATION %s;`, sqlIdentifier(serviceSchema), sqlIdentifier(serviceRole)), fmt.Sprintf(`GRANT USAGE ON SCHEMA %s TO %s;`, sqlIdentifier(serviceSchema), sqlIdentifier(serviceRole)), } for _, statement := range statements { if _, err := db.ExecContext(ctx, statement); err != nil { return fmt.Errorf("provision %q/%q: %w", serviceSchema, serviceRole, err) } } return nil } func dsnForServiceRole(baseDSN string) (string, error) { parsed, err := url.Parse(baseDSN) if err != nil { return "", fmt.Errorf("parse base dsn: %w", err) } values := url.Values{} values.Set("search_path", serviceSchema) values.Set("sslmode", "disable") scoped := url.URL{ Scheme: parsed.Scheme, User: url.UserPassword(serviceRole, servicePassword), Host: parsed.Host, Path: parsed.Path, RawQuery: values.Encode(), } return scoped.String(), nil } func applyMigrations(ctx context.Context, dsn string) error { cfg := postgres.DefaultConfig() cfg.PrimaryDSN = dsn cfg.OperationTimeout = defaultOpTimeout db, err := postgres.OpenPrimary(ctx, cfg) if err != nil { return fmt.Errorf("open scoped pool: %w", err) } defer func() { _ = db.Close() }() if err := postgres.Ping(ctx, db, defaultOpTimeout); err != nil { return err } if err := postgres.RunMigrations(ctx, db, migrations.FS(), "."); err != nil { return fmt.Errorf("run migrations: %w", err) } return nil } // jetOutputDir returns the absolute path that jet should write into. We // rely on the runtime caller info to anchor it to galaxy/gamemaster // regardless of the invoking working directory. func jetOutputDir() (string, error) { _, file, _, ok := runtime.Caller(0) if !ok { return "", errors.New("resolve runtime caller for jet output path") } dir := filepath.Dir(file) // dir = .../galaxy/gamemaster/cmd/jetgen moduleRoot := filepath.Clean(filepath.Join(dir, "..", "..")) return filepath.Join(moduleRoot, jetOutputDirSuffix), nil } func sqlIdentifier(name string) string { return `"` + escapeDoubleQuotes(name) + `"` } func sqlLiteral(value string) string { return "'" + escapeSingleQuotes(value) + "'" } func escapeDoubleQuotes(value string) string { out := make([]byte, 0, len(value)) for index := 0; index < len(value); index++ { if value[index] == '"' { out = append(out, '"', '"') continue } out = append(out, value[index]) } return string(out) } func escapeSingleQuotes(value string) string { out := make([]byte, 0, len(value)) for index := 0; index < len(value); index++ { if value[index] == '\'' { out = append(out, '\'', '\'') continue } out = append(out, value[index]) } return string(out) }