Added migration mgmt with goose.

This commit is contained in:
2025-06-04 02:36:09 -05:00
parent 9e59d05efe
commit fa5be206cb
13 changed files with 297 additions and 66 deletions

View File

@ -12,3 +12,14 @@ docker-push:
-t $(IMAGE_REPO):latest \ -t $(IMAGE_REPO):latest \
-t $(IMAGE_REPO):$(DATE_TAG) \ -t $(IMAGE_REPO):$(DATE_TAG) \
--push . --push .
# Database migration targets
.PHONY: sqlc-generate
sqlc-generate:
cd internal/db && sqlc generate
# Build the application
.PHONY: build
build:
go build -o member-console .

View File

@ -2,28 +2,33 @@
Member console application for users to create, acccess, and manage their accounts associated with the Wiki Cafe MSC (multi-stakeholder co-operative). Member console application for users to create, acccess, and manage their accounts associated with the Wiki Cafe MSC (multi-stakeholder co-operative).
## Development notes: ## Database Management
- [ ] Integrate a database migration library. `internal/db/database.go`. This project uses [pressly/goose](https://github.com/pressly/goose) for database migrations and [sqlc](https://github.com/sqlc-dev/sqlc) for type-safe SQL code generation.
- [ ] Make sure viper's 'env' key will work correctly in production
- [ ] Should session-secret and csrf-secret be generated on startup instead of in the config file? They should be persisted nonetheless. Do they need to be rotated? ### Database Migrations
- [ ] Add remove trailing slash middleware if we start using more custom handlers that don't end with a slash
- [ ] Add tests Migrations are embedded in the binary and run automatically on application startup. The CLI also provides migration management commands (`migrate up`, `migrate down`, `migrate status`).
- [ ] CSRF
- [ ] Logging ### Creating New Migrations
- [ ] compression
- [ ] recovery ```bash
- [ ] request ID # Install goose CLI tool
- [ ] timeout go install github.com/pressly/goose/v3/cmd/goose@latest
- [ ] secure headers and CORS
- [ ] Auth setup sanity check. Review code. # Create a new migration
- [ ] Remove keycloak specific code cd internal/db/migrations
- [ ] Implement backchannel logout: When a user logs out of the application, the application should notify the identity provider to log the user out of the identity provider as well. goose create your_migration_name sql
- [ ] Auth session timeout should match security policy ```
- [ ] Rate limiting on login attempts
- [ ] Subresource Integrity (SRI) for CDN assets ### sqlc Code Generation
- [ ] Serve HTMX assets not from CDN
- [ ] Find out if timeout middleware is actually needed or if net/http handles it sqlc generates type-safe Go code from SQL queries and migration files. Database models and query methods are automatically generated from the migration schema and SQL files in `internal/db/queries/`.
```bash
# Regenerate sqlc code after schema or query changes
cd internal/db && sqlc generate
```
## Building and publishing container image ## Building and publishing container image
@ -68,4 +73,26 @@ Example output:
e157b42a5b608882179cb4ac69c12f84 e157b42a5b608882179cb4ac69c12f84
``` ```
Ensure these secrets are securely stored and persisted for application use. Ensure these secrets are securely stored and persisted for application use.
## Development notes:
- [ ] Make sure viper's 'env' key will work correctly in production
- [ ] Should session-secret and csrf-secret be generated on startup instead of in the config file? They should be persisted nonetheless. Do they need to be rotated?
- [ ] Add remove trailing slash middleware if we start using more custom handlers that don't end with a slash
- [ ] Add tests
- [ ] CSRF
- [ ] Logging
- [ ] compression
- [ ] recovery
- [ ] request ID
- [ ] timeout
- [ ] secure headers and CORS
- [ ] Auth setup sanity check. Review code.
- [ ] Remove keycloak specific code
- [ ] Implement backchannel logout: When a user logs out of the application, the application should notify the identity provider to log the user out of the identity provider as well.
- [ ] Auth session timeout should match security policy
- [ ] Rate limiting on login attempts
- [ ] Subresource Integrity (SRI) for CDN assets
- [ ] Serve HTMX assets not from CDN
- [ ] Find out if timeout middleware is actually needed or if net/http handles it

110
cmd/migrate.go Normal file
View File

@ -0,0 +1,110 @@
package cmd
import (
"context"
"log/slog"
"git.coopcloud.tech/wiki-cafe/member-console/internal/db"
"git.coopcloud.tech/wiki-cafe/member-console/internal/logging"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Database migration commands",
Long: "Run database migrations up, down, or check status",
}
var migrateUpCmd = &cobra.Command{
Use: "up",
Short: "Run all pending migrations",
Long: "Apply all pending database migrations",
RunE: func(cmd *cobra.Command, args []string) error {
logger := slog.Default()
if logging.AppLogger != nil {
logger = logging.AppLogger
}
dbDSN := viper.GetString("db-dsn")
config := db.DefaultDBConfig(dbDSN)
database, err := db.Connect(context.Background(), logger, config)
if err != nil {
logger.Error("failed to connect to database", slog.Any("error", err))
return err
}
defer database.Close()
if err := db.RunMigrations(database); err != nil {
logger.Error("failed to run migrations", slog.Any("error", err))
return err
}
logger.Info("migrations completed successfully")
return nil
},
}
var migrateDownCmd = &cobra.Command{
Use: "down",
Short: "Rollback the last migration",
Long: "Rollback the most recently applied migration",
RunE: func(cmd *cobra.Command, args []string) error {
logger := slog.Default()
if logging.AppLogger != nil {
logger = logging.AppLogger
}
dbDSN := viper.GetString("db-dsn")
config := db.DefaultDBConfig(dbDSN)
database, err := db.Connect(context.Background(), logger, config)
if err != nil {
logger.Error("failed to connect to database", slog.Any("error", err))
return err
}
defer database.Close()
if err := db.RollbackMigration(database); err != nil {
logger.Error("failed to rollback migration", slog.Any("error", err))
return err
}
logger.Info("migration rollback completed successfully")
return nil
},
}
var migrateStatusCmd = &cobra.Command{
Use: "status",
Short: "Show migration status",
Long: "Display the status of all migrations",
RunE: func(cmd *cobra.Command, args []string) error {
logger := slog.Default()
if logging.AppLogger != nil {
logger = logging.AppLogger
}
dbDSN := viper.GetString("db-dsn")
config := db.DefaultDBConfig(dbDSN)
database, err := db.Connect(context.Background(), logger, config)
if err != nil {
logger.Error("failed to connect to database", slog.Any("error", err))
return err
}
defer database.Close()
if err := db.MigrationStatus(database); err != nil {
logger.Error("failed to get migration status", slog.Any("error", err))
return err
}
return nil
},
}
func init() {
migrateCmd.AddCommand(migrateUpCmd)
migrateCmd.AddCommand(migrateDownCmd)
migrateCmd.AddCommand(migrateStatusCmd)
rootCmd.AddCommand(migrateCmd)
}

View File

@ -35,7 +35,7 @@ var startCmd = &cobra.Command{
// Database Setup // Database Setup
dbDSN := viper.GetString("db-dsn") dbDSN := viper.GetString("db-dsn")
dbConfig := db.DefaultDBConfig(dbDSN) dbConfig := db.DefaultDBConfig(dbDSN)
database, err := db.NewDB(ctx, logger, dbConfig) database, err := db.ConnectAndMigrate(ctx, logger, dbConfig)
if err != nil { if err != nil {
logger.Error("failed to initialize database", slog.Any("error", err)) logger.Error("failed to initialize database", slog.Any("error", err))
os.Exit(1) os.Exit(1)

18
go.mod
View File

@ -12,8 +12,12 @@ require (
) )
require ( require (
github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/klauspost/compress v1.17.2 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/pressly/goose/v3 v3.24.3 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
golang.org/x/sync v0.14.0 // indirect
) )
require ( require (
@ -36,12 +40,12 @@ require (
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.25.0 // indirect golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/oauth2 v0.26.0 golang.org/x/oauth2 v0.26.0
golang.org/x/sys v0.22.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.25.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

26
go.sum
View File

@ -2,6 +2,8 @@ github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWG
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM= github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@ -36,6 +38,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -45,17 +49,23 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM=
github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -63,6 +73,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
@ -85,25 +97,39 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g= github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -3,7 +3,6 @@ package db
import ( import (
"context" "context"
"database/sql" "database/sql"
_ "embed"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@ -14,9 +13,6 @@ import (
"github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
) )
//go:embed schema.sql
var ddl string
// DBConfig holds database configuration. // DBConfig holds database configuration.
type DBConfig struct { type DBConfig struct {
DSN string // Data Source Name for SQLite DSN string // Data Source Name for SQLite
@ -126,8 +122,9 @@ func openAndConfigureDB(config *DBConfig) (*sql.DB, error) {
return db, nil return db, nil
} }
// NewDB initializes and returns a new database connection pool and runs migrations. // Connect initializes and returns a new database connection pool.
func NewDB(ctx context.Context, logger *slog.Logger, config *DBConfig) (*sql.DB, error) { // This is the basic connection function without any automatic operations.
func Connect(ctx context.Context, logger *slog.Logger, config *DBConfig) (*sql.DB, error) {
db, err := openAndConfigureDB(config) db, err := openAndConfigureDB(config)
if err != nil { if err != nil {
return nil, err return nil, err
@ -147,16 +144,26 @@ func NewDB(ctx context.Context, logger *slog.Logger, config *DBConfig) (*sql.DB,
slog.Int("max_open_conns", config.MaxOpenConns), slog.Int("max_open_conns", config.MaxOpenConns),
slog.Int("max_idle_conns", config.MaxIdleConns)) slog.Int("max_idle_conns", config.MaxIdleConns))
// Execute schema with retry logic return db, nil
err = retryOperation(ctx, logger, config, "schema execution", func() error { }
_, err := db.ExecContext(ctx, ddl)
return err // ConnectAndMigrate initializes a database connection and automatically runs migrations.
// This is the main function for application startup.
func ConnectAndMigrate(ctx context.Context, logger *slog.Logger, config *DBConfig) (*sql.DB, error) {
db, err := Connect(ctx, logger, config)
if err != nil {
return nil, err
}
// Run migrations with retry logic
err = retryOperation(ctx, logger, config, "migrations", func() error {
return RunMigrations(db)
}) })
if err != nil { if err != nil {
db.Close() db.Close()
return nil, err return nil, err
} }
logger.Info("database schema applied") logger.Info("database migrations applied")
return db, nil return db, nil
} }

56
internal/db/migrations.go Normal file
View File

@ -0,0 +1,56 @@
package db
import (
"database/sql"
"embed"
"github.com/pressly/goose/v3"
)
//go:embed migrations/*.sql
var embedMigrations embed.FS
// RunMigrations runs all pending migrations
func RunMigrations(db *sql.DB) error {
goose.SetBaseFS(embedMigrations)
if err := goose.SetDialect("sqlite3"); err != nil {
return err
}
if err := goose.Up(db, "migrations"); err != nil {
return err
}
return nil
}
// RollbackMigration rolls back the last migration
func RollbackMigration(db *sql.DB) error {
goose.SetBaseFS(embedMigrations)
if err := goose.SetDialect("sqlite3"); err != nil {
return err
}
if err := goose.Down(db, "migrations"); err != nil {
return err
}
return nil
}
// MigrationStatus shows the status of all migrations
func MigrationStatus(db *sql.DB) error {
goose.SetBaseFS(embedMigrations)
if err := goose.SetDialect("sqlite3"); err != nil {
return err
}
if err := goose.Status(db, "migrations"); err != nil {
return err
}
return nil
}

View File

@ -1,5 +1,4 @@
-- internal/db/schema.sql -- +goose Up
CREATE TABLE users ( CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
oidc_subject TEXT UNIQUE NOT NULL, oidc_subject TEXT UNIQUE NOT NULL,
@ -31,14 +30,12 @@ CREATE TABLE payments (
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
); );
-- Create indexes for foreign keys and frequently queried columns
CREATE INDEX idx_sites_user_id ON sites(user_id); CREATE INDEX idx_sites_user_id ON sites(user_id);
CREATE INDEX idx_payments_user_id ON payments(user_id); CREATE INDEX idx_payments_user_id ON payments(user_id);
CREATE INDEX idx_users_oidc_subject ON users(oidc_subject); CREATE INDEX idx_users_oidc_subject ON users(oidc_subject);
CREATE INDEX idx_sites_domain ON sites(domain); CREATE INDEX idx_sites_domain ON sites(domain);
CREATE INDEX idx_payments_payment_processor_id ON payments(payment_processor_id); CREATE INDEX idx_payments_payment_processor_id ON payments(payment_processor_id);
-- Triggers to update 'updated_at' timestamps
CREATE TRIGGER trigger_users_updated_at CREATE TRIGGER trigger_users_updated_at
AFTER UPDATE ON users AFTER UPDATE ON users
FOR EACH ROW FOR EACH ROW
@ -59,3 +56,16 @@ FOR EACH ROW
BEGIN BEGIN
UPDATE payments SET updated_at = CURRENT_TIMESTAMP WHERE id = OLD.id; UPDATE payments SET updated_at = CURRENT_TIMESTAMP WHERE id = OLD.id;
END; END;
-- +goose Down
DROP TRIGGER IF EXISTS trigger_payments_updated_at;
DROP TRIGGER IF EXISTS trigger_sites_updated_at;
DROP TRIGGER IF EXISTS trigger_users_updated_at;
DROP INDEX IF EXISTS idx_payments_payment_processor_id;
DROP INDEX IF EXISTS idx_sites_domain;
DROP INDEX IF EXISTS idx_users_oidc_subject;
DROP INDEX IF EXISTS idx_payments_user_id;
DROP INDEX IF EXISTS idx_sites_user_id;
DROP TABLE IF EXISTS payments;
DROP TABLE IF EXISTS sites;
DROP TABLE IF EXISTS users;

View File

@ -12,12 +12,6 @@ type Querier interface {
CreatePayment(ctx context.Context, arg CreatePaymentParams) (Payment, error) CreatePayment(ctx context.Context, arg CreatePaymentParams) (Payment, error)
CreateSite(ctx context.Context, arg CreateSiteParams) (Site, error) CreateSite(ctx context.Context, arg CreateSiteParams) (Site, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error) CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
// THIS IS NOT USED YET
// -- name: UpdateSiteDomain :one
// UPDATE sites
// SET domain = ?
// WHERE id = ?
// RETURNING *;
DeleteSite(ctx context.Context, id int64) error DeleteSite(ctx context.Context, id int64) error
DeleteUser(ctx context.Context, id int64) error DeleteUser(ctx context.Context, id int64) error
GetPaymentByID(ctx context.Context, id int64) (Payment, error) GetPaymentByID(ctx context.Context, id int64) (Payment, error)

View File

@ -16,13 +16,6 @@ ORDER BY created_at DESC;
SELECT * FROM sites SELECT * FROM sites
WHERE domain = ?; WHERE domain = ?;
-- THIS IS NOT USED YET
-- -- name: UpdateSiteDomain :one
-- UPDATE sites
-- SET domain = ?
-- WHERE id = ?
-- RETURNING *;
-- name: DeleteSite :exec -- name: DeleteSite :exec
DELETE FROM sites DELETE FROM sites
WHERE id = ?; WHERE id = ?;

View File

@ -34,17 +34,10 @@ func (q *Queries) CreateSite(ctx context.Context, arg CreateSiteParams) (Site, e
} }
const deleteSite = `-- name: DeleteSite :exec const deleteSite = `-- name: DeleteSite :exec
DELETE FROM sites DELETE FROM sites
WHERE id = ? WHERE id = ?
` `
// THIS IS NOT USED YET
// -- name: UpdateSiteDomain :one
// UPDATE sites
// SET domain = ?
// WHERE id = ?
// RETURNING *;
func (q *Queries) DeleteSite(ctx context.Context, id int64) error { func (q *Queries) DeleteSite(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteSite, id) _, err := q.db.ExecContext(ctx, deleteSite, id)
return err return err

View File

@ -2,7 +2,7 @@
version: "2" version: "2"
sql: sql:
- engine: "sqlite" - engine: "sqlite"
schema: "schema.sql" schema: "migrations/"
queries: "queries/" queries: "queries/"
gen: gen:
go: go: