feat: use sqlx
Some checks failed
Build Docker Image / Build-Docker-Image (push) Has been cancelled

This commit is contained in:
2025-05-04 15:52:19 +02:00
parent 54d68175d2
commit d51128ef5b
12 changed files with 154 additions and 155 deletions

View File

@@ -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}
}

View File

@@ -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

126
handler/account.go Normal file
View File

@@ -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
// }
// }
// }

View File

@@ -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
}
}
}

16
main.go
View File

@@ -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)

View File

@@ -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;

View File

@@ -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,

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><path fill="currentColor" d="M165.906 18.688C15.593 59.28-42.187 198.55 92.72 245.375h-1.095c.635.086 1.274.186 1.906.28c8.985 3.077 18.83 5.733 29.532 7.94C173.36 273.35 209.74 321.22 212.69 368c-33.514 23.096-59.47 62.844-59.47 62.844l26.28 38.686L138.28 493h81.97c-40.425-40.435-11.76-85.906 36.125-85.906c48.54 0 73.945 48.112 36.156 85.906h81.126l-40.375-23.47l26.283-38.686s-26.376-40.4-60.282-63.406c3.204-46.602 39.5-94.167 89.595-113.844c10.706-2.207 20.546-4.86 29.53-7.938c.633-.095 1.273-.195 1.908-.28h-1.125c134.927-46.82 77.163-186.094-73.157-226.69c-40.722 39.37 6.54 101.683 43.626 56.877c36.9 69.08 8.603 127.587-72.28 83.406c-11.88 24.492-34.213 41.374-60.688 41.374c-26.703 0-49.168-17.167-60.97-42c-81.774 45.38-110.512-13.372-73.437-82.78c37.09 44.805 84.35-17.508 43.626-56.876zm90.79 35.92c-27.388 0-51.33 27.556-51.33 63.61c0 36.056 23.942 62.995 51.33 62.995s51.327-26.94 51.327-62.994c0-36.058-23.94-63.61-51.328-63.61z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 128 128"><path d="M93.46 39.45c6.71-1.49 15.45-8.15 16.78-11.43.78-1.92-3.11-4.92-4.15-6.13-2.38-2.76-1.42-4.12-.5-7.41 1.05-3.74-1.44-7.87-4.97-9.49s-7.75-1.11-11.3.47-6.58 4.12-9.55 6.62c-2.17-1.37-5.63-7.42-11.23-3.49-3.87 2.71-4.22 8.61-3.72 13.32 1.17 10.87 3.85 16.51 8.9 18.03 6.38 1.92 13.44.91 19.74-.49" style="fill:#ffca28"/><path d="M104.36 8.18c-.85 14.65-15.14 24.37-21.92 28.65l4.4 3.78s2.79.06 6.61-1.16c6.55-2.08 16.12-7.96 16.78-11.43.97-5.05-4.21-3.95-5.38-7.94-.61-2.11 2.97-6.1-.49-11.9M79.78 12.09s-2.55-2.61-4.44-3.8c-.94 1.77-1.61 3.69-1.94 5.67-.59 3.48 0 8.42 1.39 12.1.22.57 1.04.48 1.13-.12 1.2-7.91 3.86-13.85 3.86-13.85" style="fill:#e2a610"/><path d="M61.96 38.16S30.77 41.53 16.7 68.61s-2.11 43.5 10.55 49.48 44.56 8.09 65.31 3.17 25.94-15.12 24.97-24.97c-1.41-14.38-14.77-23.22-14.77-23.22s.53-17.76-13.25-29.29c-12.23-10.24-27.55-5.62-27.55-5.62" style="fill:#ffca28"/><path d="M74.76 83.73c-6.69-8.44-14.59-9.57-17.12-12.6-1.38-1.65-2.19-3.32-1.88-5.39.33-2.2 2.88-3.72 4.86-4.09 2.31-.44 7.82-.21 12.45 4.2 1.1 1.04.7 2.66.67 4.11-.08 3.11 4.37 6.13 7.97 3.53 3.61-2.61.84-8.42-1.49-11.24-1.76-2.13-8.14-6.82-16.07-7.56-2.23-.21-11.2-1.54-16.38 8.31-1.49 2.83-2.04 9.67 5.76 15.45 1.63 1.21 10.09 5.51 12.44 8.3 4.07 4.83 1.28 9.08-1.9 9.64-8.67 1.52-13.58-3.17-14.49-5.74-.65-1.83.03-3.81-.81-5.53-.86-1.77-2.62-2.47-4.48-1.88-6.1 1.94-4.16 8.61-1.46 12.28 2.89 3.93 6.44 6.3 10.43 7.6 14.89 4.85 22.05-2.81 23.3-8.42.92-4.11.82-7.67-1.8-10.97" style="fill:#6b4b46"/><path d="M71.16 48.99c-12.67 27.06-14.85 61.23-14.85 61.23" style="fill:none;stroke:#6b4b46;stroke-width:5;stroke-miterlimit:10"/><path d="M81.67 31.96c8.44 2.75 10.31 10.38 9.7 12.46-.73 2.44-10.08-7.06-23.98-6.49-4.86.2-3.45-2.78-1.2-4.5 2.97-2.27 7.96-3.91 15.48-1.47" style="fill:#6d4c41"/><path d="M81.67 31.96c8.44 2.75 10.31 10.38 9.7 12.46-.73 2.44-10.08-7.06-23.98-6.49-4.86.2-3.45-2.78-1.2-4.5 2.97-2.27 7.96-3.91 15.48-1.47" style="fill:#6b4b46"/><path d="M96.49 58.86c1.06-.73 4.62.53 5.62 7.5.49 3.41.64 6.71.64 6.71s-4.2-3.77-5.59-6.42c-1.75-3.35-2.43-6.59-.67-7.79" style="fill:#e2a610"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1 @@
package account

View File

@@ -0,0 +1,7 @@
package account
import "spend-sparrow/types"
templ AccountListComp(accounts []*types.Account) {
<main class="mx-2"></main>
}

View File

@@ -3,14 +3,7 @@ package template
templ Index() {
<div class="h-full">
<div class="text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Next Level Workout Tracker</h1>
<p class="py-6">
Ever wanted to track your workouts and see your progress over time? spend-sparrow is the perfect
solution for you.
</p>
<a href="/workout" class="">Get Started</a>
</div>
<h1 class="text-5xl font-bold">Spend Sparrow - Your personal finance</h1>
</div>
</div>
}

View File

@@ -24,8 +24,8 @@ templ Layout(slot templ.Component, user templ.Component) {
<div class="h-screen flex flex-col">
<div class="flex justify-end items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2 shadow-sm">
<a href="/" class="flex-1 flex gap-2">
<img src="/static/favicon.svg" alt="spend-sparrow logo"/>
<span>spend-sparrow</span>
<img class="w-6" src="/static/favicon.svg" alt="Spend Sparrow logo"/>
<span class="text-xl font-bold">SpendSparrow</span>
</a>
@user
</div>