From b20a48be2547416f709e1b10b19436d5e2405e88 Mon Sep 17 00:00:00 2001 From: Tim Wundenberg Date: Sun, 4 May 2025 15:17:48 +0200 Subject: [PATCH] feat: use sqlx --- db/account.go | 43 ++--------- db/auth.go | 5 +- db/default.go | 6 +- go.mod | 1 + go.sum | 2 + handler/account.go | 126 ++++++++++++++++++++++++++++++++ handler/workout.go | 129 --------------------------------- main.go | 16 ++-- migration/002_account.up.sql | 2 +- service/account.go | 2 +- static/favicon.svg | 2 +- template/account/default.go | 1 + template/account/workout.templ | 7 ++ template/index.templ | 9 +-- template/layout.templ | 4 +- 15 files changed, 165 insertions(+), 190 deletions(-) create mode 100644 handler/account.go delete mode 100644 handler/workout.go create mode 100644 template/account/default.go create mode 100644 template/account/workout.templ diff --git a/db/account.go b/db/account.go index 4a007d7..fa14b53 100644 --- a/db/account.go +++ b/db/account.go @@ -4,7 +4,7 @@ import ( "spend-sparrow/log" "spend-sparrow/types" - "database/sql" + "github.com/jmoiron/sqlx" "github.com/google/uuid" ) @@ -18,10 +18,10 @@ type Account interface { } type AccountSqlite struct { - db *sql.DB + db *sqlx.DB } -func NewAccountSqlite(db *sql.DB) *AccountSqlite { +func NewAccountSqlite(db *sqlx.DB) *AccountSqlite { return &AccountSqlite{db: db} } @@ -60,7 +60,8 @@ func (db AccountSqlite) Update(account *types.Account) error { func (db AccountSqlite) GetAll(groupId uuid.UUID) ([]*types.Account, error) { - rows, err := db.db.Query(` + accounts := make([]*types.Account, 0) + err := db.db.Select(&accounts, ` SELECT id, name, current_balance, last_transaction, oink_balance, @@ -73,23 +74,13 @@ func (db AccountSqlite) GetAll(groupId uuid.UUID) ([]*types.Account, error) { return nil, types.ErrInternal } - var accounts = make([]*types.Account, 0) - for rows.Next() { - - account, err := scanAccount(rows) - if err != nil { - return nil, types.ErrInternal - } - - accounts = append(accounts, account) - } - return accounts, nil } func (db AccountSqlite) Get(groupId uuid.UUID, id uuid.UUID) (*types.Account, error) { - rows, err := db.db.Query(` + account := &types.Account{} + err := db.db.Get(account, ` SELECT id, name, current_balance, last_transaction, oink_balance, @@ -102,25 +93,7 @@ func (db AccountSqlite) Get(groupId uuid.UUID, id uuid.UUID) (*types.Account, er return nil, types.ErrInternal } - if !rows.Next() { - return nil, ErrNotFound - } - - return scanAccount(rows) -} - -func scanAccount(rows *sql.Rows) (*types.Account, error) { - var ( - account types.Account - ) - - err := rows.Scan(&account.Id, &account.Name, &account.CurrentBalance, &account.LastTransaction, &account.OinkBalance, &account.CreatedAt, &account.CreatedBy, &account.UpdatedAt, &account.UpdatedBy) - if err != nil { - log.Error("Could not scan account: %v", err) - return nil, types.ErrInternal - } - - return &account, nil + return account, nil } func (db AccountSqlite) Delete(id uuid.UUID) error { diff --git a/db/auth.go b/db/auth.go index 8525b8b..3ee56e2 100644 --- a/db/auth.go +++ b/db/auth.go @@ -9,6 +9,7 @@ import ( "time" "github.com/google/uuid" + "github.com/jmoiron/sqlx" ) type Auth interface { @@ -32,10 +33,10 @@ type Auth interface { } type AuthSqlite struct { - db *sql.DB + db *sqlx.DB } -func NewAuthSqlite(db *sql.DB) *AuthSqlite { +func NewAuthSqlite(db *sqlx.DB) *AuthSqlite { return &AuthSqlite{db: db} } diff --git a/db/default.go b/db/default.go index 895eb83..544efd1 100644 --- a/db/default.go +++ b/db/default.go @@ -4,12 +4,12 @@ import ( "spend-sparrow/log" "spend-sparrow/types" - "database/sql" "errors" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/sqlite3" _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/jmoiron/sqlx" ) var ( @@ -17,8 +17,8 @@ var ( ErrAlreadyExists = errors.New("row already exists") ) -func RunMigrations(db *sql.DB, pathPrefix string) error { - driver, err := sqlite3.WithInstance(db, &sqlite3.Config{}) +func RunMigrations(db *sqlx.DB, pathPrefix string) error { + driver, err := sqlite3.WithInstance(db.DB, &sqlite3.Config{}) if err != nil { log.Error("Could not create Migration instance: %v", err) return types.ErrInternal diff --git a/go.mod b/go.mod index bcc5316..fad95cd 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/a-h/templ v0.3.865 github.com/golang-migrate/migrate/v4 v4.18.3 github.com/google/uuid v1.6.0 + github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.28 github.com/prometheus/client_golang v1.22.0 diff --git a/go.sum b/go.sum index aa3f4e7..7e34222 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= diff --git a/handler/account.go b/handler/account.go new file mode 100644 index 0000000..7df1efb --- /dev/null +++ b/handler/account.go @@ -0,0 +1,126 @@ +package handler + +import ( + "spend-sparrow/handler/middleware" + "spend-sparrow/service" + "spend-sparrow/template/account" + "spend-sparrow/utils" + + "net/http" +) + +type Account interface { + Handle(router *http.ServeMux) +} + +type AccountImpl struct { + service service.Account + auth service.Auth + render *Render +} + +func NewAccount(service service.Account, auth service.Auth, render *Render) Account { + return AccountImpl{ + service: service, + auth: auth, + render: render, + } +} + +func (handler AccountImpl) Handle(router *http.ServeMux) { + router.Handle("/account", handler.handleAccountPage()) + // router.Handle("POST /account", handler.handleAddAccount()) + // router.Handle("GET /account", handler.handleGetAccount()) + // router.Handle("DELETE /account/{id}", handler.handleDeleteAccount()) +} + +func (handler AccountImpl) handleAccountPage() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user := middleware.GetUser(r) + if user == nil { + utils.DoRedirect(w, r, "/auth/signin") + return + } + + comp := account.AccountListComp(nil) + handler.render.RenderLayout(r, w, comp, user) + } +} + +// func (handler AccountImpl) handleAddAccount() http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) { +// user := middleware.GetUser(r) +// if user == nil { +// utils.DoRedirect(w, r, "/auth/signin") +// return +// } +// +// var dateStr = r.FormValue("date") +// var typeStr = r.FormValue("type") +// var setsStr = r.FormValue("sets") +// var repsStr = r.FormValue("reps") +// +// wo := service.NewAccountDto("", dateStr, typeStr, setsStr, repsStr) +// wo, err := handler.service.AddAccount(user, wo) +// if err != nil { +// utils.TriggerToast(w, r, "error", "Invalid input values", http.StatusBadRequest) +// http.Error(w, "Invalid input values", http.StatusBadRequest) +// return +// } +// wor := account.Account{Id: wo.RowId, Date: wo.Date, Type: wo.Type, Sets: wo.Sets, Reps: wo.Reps} +// +// comp := account.AccountItemComp(wor, true) +// handler.render.Render(r, w, comp) +// } +// } +// +// func (handler AccountImpl) handleGetAccount() http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) { +// user := middleware.GetUser(r) +// if user == nil { +// utils.DoRedirect(w, r, "/auth/signin") +// return +// } +// +// workouts, err := handler.service.GetAccounts(user) +// if err != nil { +// return +// } +// +// wos := make([]*types.Account, 0) +// for _, wo := range workouts { +// wos = append(wos, *types.Account{Id: wo.RowId, Date: wo.Date, Type: wo.Type, Sets: wo.Sets, Reps: wo.Reps}) +// } +// +// comp := account.AccountListComp(wos) +// handler.render.Render(r, w, comp) +// } +// } +// +// func (handler AccountImpl) handleDeleteAccount() http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) { +// user := middleware.GetUser(r) +// if user == nil { +// utils.DoRedirect(w, r, "/auth/signin") +// return +// } +// +// rowId := r.PathValue("id") +// if rowId == "" { +// utils.TriggerToast(w, r, "error", "Missing ID field", http.StatusBadRequest) +// return +// } +// +// rowIdInt, err := strconv.Atoi(rowId) +// if err != nil { +// utils.TriggerToast(w, r, "error", "Invalid ID", http.StatusBadRequest) +// return +// } +// +// err = handler.service.DeleteAccount(user, rowIdInt) +// if err != nil { +// utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError) +// return +// } +// } +// } diff --git a/handler/workout.go b/handler/workout.go deleted file mode 100644 index 1fb8c2f..0000000 --- a/handler/workout.go +++ /dev/null @@ -1,129 +0,0 @@ -package handler - -import ( - "spend-sparrow/handler/middleware" - "spend-sparrow/service" - "spend-sparrow/template/workout" - "spend-sparrow/utils" - - "net/http" - "strconv" - "time" -) - -type Workout interface { - Handle(router *http.ServeMux) -} - -type WorkoutImpl struct { - service service.Workout - auth service.Auth - render *Render -} - -func NewWorkout(service service.Workout, auth service.Auth, render *Render) Workout { - return WorkoutImpl{ - service: service, - auth: auth, - render: render, - } -} - -func (handler WorkoutImpl) Handle(router *http.ServeMux) { - router.Handle("/workout", handler.handleWorkoutPage()) - router.Handle("POST /api/workout", handler.handleAddWorkout()) - router.Handle("GET /api/workout", handler.handleGetWorkout()) - router.Handle("DELETE /api/workout/{id}", handler.handleDeleteWorkout()) -} - -func (handler WorkoutImpl) handleWorkoutPage() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - user := middleware.GetUser(r) - if user == nil { - utils.DoRedirect(w, r, "/auth/signin") - return - } - - currentDate := time.Now().Format("2006-01-02") - comp := workout.WorkoutComp(currentDate) - handler.render.RenderLayout(r, w, comp, user) - } -} - -func (handler WorkoutImpl) handleAddWorkout() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - user := middleware.GetUser(r) - if user == nil { - utils.DoRedirect(w, r, "/auth/signin") - return - } - - var dateStr = r.FormValue("date") - var typeStr = r.FormValue("type") - var setsStr = r.FormValue("sets") - var repsStr = r.FormValue("reps") - - wo := service.NewWorkoutDto("", dateStr, typeStr, setsStr, repsStr) - wo, err := handler.service.AddWorkout(user, wo) - if err != nil { - utils.TriggerToast(w, r, "error", "Invalid input values", http.StatusBadRequest) - http.Error(w, "Invalid input values", http.StatusBadRequest) - return - } - wor := workout.Workout{Id: wo.RowId, Date: wo.Date, Type: wo.Type, Sets: wo.Sets, Reps: wo.Reps} - - comp := workout.WorkoutItemComp(wor, true) - handler.render.Render(r, w, comp) - } -} - -func (handler WorkoutImpl) handleGetWorkout() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - user := middleware.GetUser(r) - if user == nil { - utils.DoRedirect(w, r, "/auth/signin") - return - } - - workouts, err := handler.service.GetWorkouts(user) - if err != nil { - return - } - - wos := make([]workout.Workout, 0) - for _, wo := range workouts { - wos = append(wos, workout.Workout{Id: wo.RowId, Date: wo.Date, Type: wo.Type, Sets: wo.Sets, Reps: wo.Reps}) - } - - comp := workout.WorkoutListComp(wos) - handler.render.Render(r, w, comp) - } -} - -func (handler WorkoutImpl) handleDeleteWorkout() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - user := middleware.GetUser(r) - if user == nil { - utils.DoRedirect(w, r, "/auth/signin") - return - } - - rowId := r.PathValue("id") - if rowId == "" { - utils.TriggerToast(w, r, "error", "Missing ID field", http.StatusBadRequest) - return - } - - rowIdInt, err := strconv.Atoi(rowId) - if err != nil { - utils.TriggerToast(w, r, "error", "Invalid ID", http.StatusBadRequest) - return - } - - err = handler.service.DeleteWorkout(user, rowIdInt) - if err != nil { - utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError) - return - } - } -} diff --git a/main.go b/main.go index ede9ff5..e760968 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,6 @@ import ( "spend-sparrow/types" "context" - "database/sql" "net/http" "os" "os/signal" @@ -17,6 +16,7 @@ import ( "syscall" "time" + "github.com/jmoiron/sqlx" "github.com/joho/godotenv" _ "github.com/mattn/go-sqlite3" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -28,7 +28,7 @@ func main() { log.Fatal("Error loading .env file") } - db, err := sql.Open("sqlite3", "./data.db") + db, err := sqlx.Open("sqlite3", "./data.db") if err != nil { log.Fatal("Could not open Database data.db: %v", err) } @@ -37,7 +37,7 @@ func main() { run(context.Background(), db, os.Getenv) } -func run(ctx context.Context, database *sql.DB, env func(string) string) { +func run(ctx context.Context, database *sqlx.DB, env func(string) string) { ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer cancel() @@ -100,26 +100,26 @@ func shutdownServer(s *http.Server, ctx context.Context, wg *sync.WaitGroup) { } } -func createHandler(d *sql.DB, serverSettings *types.Settings) http.Handler { +func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler { var router = http.NewServeMux() authDb := db.NewAuthSqlite(d) - workoutDb := db.NewWorkoutDbSqlite(d) + accountDb := db.NewAccountSqlite(d) randomService := service.NewRandomImpl() clockService := service.NewClockImpl() mailService := service.NewMailImpl(serverSettings) authService := service.NewAuthImpl(authDb, randomService, clockService, mailService, serverSettings) - workoutService := service.NewWorkoutImpl(workoutDb, randomService, clockService, mailService, serverSettings) + accountService := service.NewAccountImpl(accountDb, randomService, clockService, serverSettings) render := handler.NewRender() indexHandler := handler.NewIndex(authService, render) authHandler := handler.NewAuth(authService, render) - workoutHandler := handler.NewWorkout(workoutService, authService, render) + accountHandler := handler.NewAccount(accountService, authService, render) indexHandler.Handle(router) - workoutHandler.Handle(router) + accountHandler.Handle(router) authHandler.Handle(router) // Serve static files (CSS, JS and images) diff --git a/migration/002_account.up.sql b/migration/002_account.up.sql index 0d1ccb0..eb031f9 100644 --- a/migration/002_account.up.sql +++ b/migration/002_account.up.sql @@ -12,6 +12,6 @@ CREATE TABLE account ( created_at DATETIME NOT NULL, created_by TEXT NOT NULL, updated_at DATETIME, - updated_by TEXT, + updated_by TEXT ) WITHOUT ROWID; diff --git a/service/account.go b/service/account.go index 189b411..523f99a 100644 --- a/service/account.go +++ b/service/account.go @@ -29,7 +29,7 @@ type AccountImpl struct { settings *types.Settings } -func NewAccountImpl(db db.Account, clock Clock, random Random, settings *types.Settings) Account { +func NewAccountImpl(db db.Account, random Random, clock Clock, settings *types.Settings) Account { return AccountImpl{ db: db, clock: clock, diff --git a/static/favicon.svg b/static/favicon.svg index 2a070d7..f3a9ca5 100644 --- a/static/favicon.svg +++ b/static/favicon.svg @@ -1 +1 @@ - + diff --git a/template/account/default.go b/template/account/default.go new file mode 100644 index 0000000..0bc7635 --- /dev/null +++ b/template/account/default.go @@ -0,0 +1 @@ +package account diff --git a/template/account/workout.templ b/template/account/workout.templ new file mode 100644 index 0000000..4924a27 --- /dev/null +++ b/template/account/workout.templ @@ -0,0 +1,7 @@ +package account + +import "spend-sparrow/types" + +templ AccountListComp(accounts []*types.Account) { +
+} diff --git a/template/index.templ b/template/index.templ index e737a50..2735dd0 100644 --- a/template/index.templ +++ b/template/index.templ @@ -3,14 +3,7 @@ package template templ Index() {
-
-

Next Level Workout Tracker

-

- Ever wanted to track your workouts and see your progress over time? spend-sparrow is the perfect - solution for you. -

- Get Started -
+

Spend Sparrow - Your personal finance

} diff --git a/template/layout.templ b/template/layout.templ index e43cfa4..7d92788 100644 --- a/template/layout.templ +++ b/template/layout.templ @@ -24,8 +24,8 @@ templ Layout(slot templ.Component, user templ.Component) {