Added migration mgmt with goose.
This commit is contained in:
11
Makefile
11
Makefile
@ -12,3 +12,14 @@ docker-push:
|
||||
-t $(IMAGE_REPO):latest \
|
||||
-t $(IMAGE_REPO):$(DATE_TAG) \
|
||||
--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 .
|
||||
|
71
README.md
71
README.md
@ -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).
|
||||
|
||||
## Development notes:
|
||||
## Database Management
|
||||
|
||||
- [ ] Integrate a database migration library. `internal/db/database.go`.
|
||||
- [ ] 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
|
||||
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.
|
||||
|
||||
### Database Migrations
|
||||
|
||||
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`).
|
||||
|
||||
### Creating New Migrations
|
||||
|
||||
```bash
|
||||
# Install goose CLI tool
|
||||
go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||
|
||||
# Create a new migration
|
||||
cd internal/db/migrations
|
||||
goose create your_migration_name sql
|
||||
```
|
||||
|
||||
### sqlc Code Generation
|
||||
|
||||
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
|
||||
|
||||
@ -68,4 +73,26 @@ Example output:
|
||||
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
110
cmd/migrate.go
Normal 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)
|
||||
}
|
@ -35,7 +35,7 @@ var startCmd = &cobra.Command{
|
||||
// Database Setup
|
||||
dbDSN := viper.GetString("db-dsn")
|
||||
dbConfig := db.DefaultDBConfig(dbDSN)
|
||||
database, err := db.NewDB(ctx, logger, dbConfig)
|
||||
database, err := db.ConnectAndMigrate(ctx, logger, dbConfig)
|
||||
if err != nil {
|
||||
logger.Error("failed to initialize database", slog.Any("error", err))
|
||||
os.Exit(1)
|
||||
|
18
go.mod
18
go.mod
@ -12,8 +12,12 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/klauspost/compress v1.17.2 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // 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 (
|
||||
@ -36,12 +40,12 @@ require (
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||
golang.org/x/oauth2 v0.26.0
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
26
go.sum
26
go.sum
@ -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/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.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/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
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.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
|
||||
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
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/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/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/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/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
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.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
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/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/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/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
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/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
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/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
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.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
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/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
|
||||
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/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
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.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/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/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/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/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/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 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
@ -3,7 +3,6 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@ -14,9 +13,6 @@ import (
|
||||
"github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
//go:embed schema.sql
|
||||
var ddl string
|
||||
|
||||
// DBConfig holds database configuration.
|
||||
type DBConfig struct {
|
||||
DSN string // Data Source Name for SQLite
|
||||
@ -126,8 +122,9 @@ func openAndConfigureDB(config *DBConfig) (*sql.DB, error) {
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// NewDB initializes and returns a new database connection pool and runs migrations.
|
||||
func NewDB(ctx context.Context, logger *slog.Logger, config *DBConfig) (*sql.DB, error) {
|
||||
// Connect initializes and returns a new database connection pool.
|
||||
// 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)
|
||||
if err != nil {
|
||||
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_idle_conns", config.MaxIdleConns))
|
||||
|
||||
// Execute schema with retry logic
|
||||
err = retryOperation(ctx, logger, config, "schema execution", func() error {
|
||||
_, err := db.ExecContext(ctx, ddl)
|
||||
return err
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Info("database schema applied")
|
||||
logger.Info("database migrations applied")
|
||||
return db, nil
|
||||
}
|
||||
|
56
internal/db/migrations.go
Normal file
56
internal/db/migrations.go
Normal 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
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
-- internal/db/schema.sql
|
||||
|
||||
-- +goose Up
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
oidc_subject TEXT UNIQUE NOT NULL,
|
||||
@ -31,14 +30,12 @@ CREATE TABLE payments (
|
||||
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_payments_user_id ON payments(user_id);
|
||||
CREATE INDEX idx_users_oidc_subject ON users(oidc_subject);
|
||||
CREATE INDEX idx_sites_domain ON sites(domain);
|
||||
CREATE INDEX idx_payments_payment_processor_id ON payments(payment_processor_id);
|
||||
|
||||
-- Triggers to update 'updated_at' timestamps
|
||||
CREATE TRIGGER trigger_users_updated_at
|
||||
AFTER UPDATE ON users
|
||||
FOR EACH ROW
|
||||
@ -59,3 +56,16 @@ FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE payments SET updated_at = CURRENT_TIMESTAMP WHERE id = OLD.id;
|
||||
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;
|
@ -12,12 +12,6 @@ type Querier interface {
|
||||
CreatePayment(ctx context.Context, arg CreatePaymentParams) (Payment, error)
|
||||
CreateSite(ctx context.Context, arg CreateSiteParams) (Site, 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
|
||||
DeleteUser(ctx context.Context, id int64) error
|
||||
GetPaymentByID(ctx context.Context, id int64) (Payment, error)
|
||||
|
@ -16,13 +16,6 @@ ORDER BY created_at DESC;
|
||||
SELECT * FROM sites
|
||||
WHERE domain = ?;
|
||||
|
||||
-- THIS IS NOT USED YET
|
||||
-- -- name: UpdateSiteDomain :one
|
||||
-- UPDATE sites
|
||||
-- SET domain = ?
|
||||
-- WHERE id = ?
|
||||
-- RETURNING *;
|
||||
|
||||
-- name: DeleteSite :exec
|
||||
DELETE FROM sites
|
||||
WHERE id = ?;
|
||||
|
@ -34,17 +34,10 @@ func (q *Queries) CreateSite(ctx context.Context, arg CreateSiteParams) (Site, e
|
||||
}
|
||||
|
||||
const deleteSite = `-- name: DeleteSite :exec
|
||||
|
||||
DELETE FROM sites
|
||||
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 {
|
||||
_, err := q.db.ExecContext(ctx, deleteSite, id)
|
||||
return err
|
||||
|
@ -2,7 +2,7 @@
|
||||
version: "2"
|
||||
sql:
|
||||
- engine: "sqlite"
|
||||
schema: "schema.sql"
|
||||
schema: "migrations/"
|
||||
queries: "queries/"
|
||||
gen:
|
||||
go:
|
||||
|
Reference in New Issue
Block a user