From 2732038a1f9add649a681e9ab61c4cccc2eb5b8b Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Tue, 25 Aug 2026 11:05:23 +0200 Subject: [PATCH 1/9] fix: moved embed.FS to own package, so the api binary has no sql files embedded --- cmd/bootstrap/main.go | 3 ++- internal/database/migrate.go | 7 ++----- internal/database/migrations/embed.go | 6 ++++++ 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 internal/database/migrations/embed.go diff --git a/cmd/bootstrap/main.go b/cmd/bootstrap/main.go index 6aa38ca..1d087c7 100644 --- a/cmd/bootstrap/main.go +++ b/cmd/bootstrap/main.go @@ -11,6 +11,7 @@ import ( "github.com/joho/godotenv" "github.com/robindittmar/dttmr-api/internal/config" "github.com/robindittmar/dttmr-api/internal/database" + "github.com/robindittmar/dttmr-api/internal/database/migrations" "github.com/robindittmar/dttmr-api/internal/domain" "github.com/robindittmar/dttmr-api/internal/repository" ) @@ -51,7 +52,7 @@ func main() { } }() - if err := database.RunMigrations(db); err != nil { + if err := database.RunMigrations(db, migrations.MigrationFS); err != nil { slog.Error("failed to run migrations", slog.Any("error", err)) os.Exit(1) } diff --git a/internal/database/migrate.go b/internal/database/migrate.go index 70cfa27..31bf4b3 100644 --- a/internal/database/migrate.go +++ b/internal/database/migrate.go @@ -2,9 +2,9 @@ package database import ( "database/sql" - "embed" "errors" "fmt" + "io/fs" "log/slog" "github.com/golang-migrate/migrate/v4" @@ -12,10 +12,7 @@ import ( "github.com/golang-migrate/migrate/v4/source/iofs" ) -//go:embed migrations/*.sql -var migrationFS embed.FS - -func RunMigrations(db *sql.DB) error { +func RunMigrations(db *sql.DB, migrationFS fs.FS) error { sourceDriver, err := iofs.New(migrationFS, "migrations") if err != nil { return fmt.Errorf("failed to load embedded migrations: %w", err) diff --git a/internal/database/migrations/embed.go b/internal/database/migrations/embed.go new file mode 100644 index 0000000..2d1fab3 --- /dev/null +++ b/internal/database/migrations/embed.go @@ -0,0 +1,6 @@ +package migrations + +import "embed" + +//go:embed *.sql +var MigrationFS embed.FS From 7fdf1ed925476019833e187f6e4c1611a4f5d4c7 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Tue, 25 Aug 2026 11:06:45 +0200 Subject: [PATCH 2/9] fix: migrations path in iofs --- internal/database/migrate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/database/migrate.go b/internal/database/migrate.go index 31bf4b3..93bf54f 100644 --- a/internal/database/migrate.go +++ b/internal/database/migrate.go @@ -13,7 +13,7 @@ import ( ) func RunMigrations(db *sql.DB, migrationFS fs.FS) error { - sourceDriver, err := iofs.New(migrationFS, "migrations") + sourceDriver, err := iofs.New(migrationFS, ".") if err != nil { return fmt.Errorf("failed to load embedded migrations: %w", err) } From a7d3d6c37853ba6ad359945b78cc2d4e188ec6ba Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Thu, 27 Aug 2026 12:01:48 +0200 Subject: [PATCH 3/9] feat: added some example http rqeuests (api.http) --- api.http | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 api.http diff --git a/api.http b/api.http new file mode 100644 index 0000000..b841c5c --- /dev/null +++ b/api.http @@ -0,0 +1,72 @@ +### POST login +POST http://localhost:8080/api/v1/login +Content-Type: application/json + +{ + "email": "admin@example.com", + "password": "12345" +} + +> {% client.global.set("access_token", response.body.access_token); %} + +### + +POST http://localhost:8080/api/v1/lists +Content-Type: application/json +Authorization: Bearer {{access_token}} + +{ + "name": "test_list" +} + +### + +GET http://localhost:8080/api/v1/lists +Content-Type: application/json +Authorization: Bearer {{access_token}} + +{ +} + +### + +GET http://localhost:8080/api/v1/lists/e817465a-d75d-420b-9d4a-610496ed3bc7 +Content-Type: application/json +Authorization: Bearer {{access_token}} + +{ +} + + +### + +POST http://localhost:8080/api/v1/lists/item +Content-Type: application/json +Authorization: Bearer {{access_token}} + +{ + "list_id": "cd2c280a-05fa-4ea0-b350-0d3f972b9777", + "title": "hello" +} + +### + +PUT http://localhost:8080/api/v1/lists/item +Content-Type: application/json +Authorization: Bearer {{access_token}} + +{ + "list_item_id": "3b0666b0-bfc5-41e2-8c22-e3437da40298", + "title": "hello 2", + "is_completed": true +} + +### + +POST http://localhost:8080/api/v1/lists/items/3b0666b0-bfc5-41e2-8c22-e3437da40298 +Content-Type: application/json +Authorization: Bearer {{access_token}} + +{ + "is_completed": false +} From 8eb70a9a63527b362199507213c499040ce66f65 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Thu, 27 Aug 2026 19:41:00 +0200 Subject: [PATCH 4/9] fix: migrate to go 1.27 --- go.mod | 34 ++++++++++++++++------------------ internal/domain/auth.go | 8 ++++---- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 0f9f902..ed5c78d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/robindittmar/dttmr-api -go 1.26 +go 1.27 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 @@ -8,21 +8,20 @@ require ( github.com/golang-migrate/migrate/v4 v4.19.1 github.com/jackc/pgx/v5 v5.10.0 github.com/joho/godotenv v1.5.1 - github.com/stretchr/testify v1.11.1 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 - go.opentelemetry.io/otel v1.45.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.45.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 - go.opentelemetry.io/otel/sdk v1.45.0 - go.opentelemetry.io/otel/sdk/metric v1.45.0 - go.opentelemetry.io/otel/trace v1.45.0 + github.com/stretchr/testify v1.12.1 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 + go.opentelemetry.io/otel v1.46.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 + go.opentelemetry.io/otel/sdk v1.46.0 + go.opentelemetry.io/otel/sdk/metric v1.46.0 + go.opentelemetry.io/otel/trace v1.46.0 golang.org/x/crypto v0.55.0 ) require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/felixge/httpsnoop v1.1.0 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -32,19 +31,18 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/lib/pq v1.12.3 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/objx v0.5.3 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect - go.opentelemetry.io/otel/metric v1.45.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect + go.opentelemetry.io/otel/metric v1.46.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.12 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/domain/auth.go b/internal/domain/auth.go index 68754a0..eb86bc5 100644 --- a/internal/domain/auth.go +++ b/internal/domain/auth.go @@ -149,11 +149,11 @@ func (s *AuthService) issueTokens(ctx context.Context, authUser *AuthUser) (Toke func (s *AuthService) GenerateAccessToken(authUser *AuthUser) (string, error) { claims := JWTClaims{ - UserID: authUser.ID, - Email: authUser.Email, - Name: authUser.Name, + UserID: authUser.ID, + Email: authUser.Email, + Name: authUser.Name, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 15)), } - claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 15)) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) From e8405e31a8be0dccefac65ee88363bbb9568f179 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Thu, 27 Aug 2026 20:17:06 +0200 Subject: [PATCH 5/9] feat: db migration for workouts. still in progress --- .../000003_create_workouts.down.sql | 3 ++ .../migrations/000003_create_workouts.up.sql | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 internal/database/migrations/000003_create_workouts.down.sql create mode 100644 internal/database/migrations/000003_create_workouts.up.sql diff --git a/internal/database/migrations/000003_create_workouts.down.sql b/internal/database/migrations/000003_create_workouts.down.sql new file mode 100644 index 0000000..3624dc4 --- /dev/null +++ b/internal/database/migrations/000003_create_workouts.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS template_exercises; +DROP TABLE IF EXISTS templates; +DROP TABLE IF EXISTS exercises; \ No newline at end of file diff --git a/internal/database/migrations/000003_create_workouts.up.sql b/internal/database/migrations/000003_create_workouts.up.sql new file mode 100644 index 0000000..632c838 --- /dev/null +++ b/internal/database/migrations/000003_create_workouts.up.sql @@ -0,0 +1,45 @@ +CREATE TABLE exercises ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + equipment TEXT[] NOT NULL DEFAULT '{}' + CHECK ( + equipment <@ ARRAY [ + 'floor', + 'rings', + 'pull_up_bar', + 'dip_bars', + 'parallettes', + ]::TEXT[]), + metric TEXT NOT NULL DEFAULT 'reps' + CHECK metric IN ('reps', 'seconds'), + load TEXT NOT NULL DEFAULT 'bodyweight' + CHECK load IN ('bodyweight', 'absolute'), + tags TEXT[] NOT NULL DEFAULT '{}', -- push/pull/legs/core/skill/shoulders/... + notes TEXT, + hidden NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX exercises_name_key ON exercises (lower(name)); +CREATE INDEX exercises_equipment_idx ON exercises USING gin (equipment); +CREATE INDEX exercises_tags_idx ON exercises USING gin (tags); + + +CREATE TABLE templates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE template_exercises ( + template_id UUID NOT NULL REFERENCES templates(id) ON DELETE CASCADE, + exercise_id UUID NOT NULL REFERENCES exercises(id) ON DELETE CASCADE, + position SMALLINT NOT NULL, + target TEXT, + + PRIMARY KEY (template_id, exercise_id) +); + +-- todo: workouts, sets From a4a84bed68e49806b94f0b83e2a4d7ea02e2745f Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 28 Aug 2026 11:00:30 +0200 Subject: [PATCH 6/9] feat: added workouts and workout_sets db tables --- .../000003_create_workouts.down.sql | 4 ++- .../migrations/000003_create_workouts.up.sql | 32 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/internal/database/migrations/000003_create_workouts.down.sql b/internal/database/migrations/000003_create_workouts.down.sql index 3624dc4..ff32d5a 100644 --- a/internal/database/migrations/000003_create_workouts.down.sql +++ b/internal/database/migrations/000003_create_workouts.down.sql @@ -1,3 +1,5 @@ +DROP TABLE IF EXISTS workout_sets; +DROP TABLE IF EXISTS workouts; DROP TABLE IF EXISTS template_exercises; DROP TABLE IF EXISTS templates; -DROP TABLE IF EXISTS exercises; \ No newline at end of file +DROP TABLE IF EXISTS exercises; diff --git a/internal/database/migrations/000003_create_workouts.up.sql b/internal/database/migrations/000003_create_workouts.up.sql index 632c838..dad6084 100644 --- a/internal/database/migrations/000003_create_workouts.up.sql +++ b/internal/database/migrations/000003_create_workouts.up.sql @@ -9,6 +9,7 @@ CREATE TABLE exercises ( 'pull_up_bar', 'dip_bars', 'parallettes', + 'resistance_band' ]::TEXT[]), metric TEXT NOT NULL DEFAULT 'reps' CHECK metric IN ('reps', 'seconds'), @@ -42,4 +43,33 @@ CREATE TABLE template_exercises ( PRIMARY KEY (template_id, exercise_id) ); --- todo: workouts, sets + +CREATE TABLE workouts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + template_id UUID REFERENCES templates(id) ON DELETE SET NULL, + started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + ended_at TIMESTAMPTZ, + rpe smallint CHECK (rpe BETWEEN 1 AND 10), + bodyweight_kg NUMERIC(5,2), + notes TEXT +); + +CREATE INDEX workouts_time_idx ON workouts (started_at DESC); + + +CREATE TABLE workout_sets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workout_id UUID NOT NULL REFERENCES workouts(id) ON DELETE CASCADE, + exercise_id UUID NOT NULL REFERENCES exercises(id) ON DELETE RESTRICT, + logged_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + reps SMALLINT, + seconds SMALLINT, + weight_kg NUMERIC(5,2), + note TEXT, + + CONSTRAINT set_has_number CHECK (reps IS NOT NULL OR seconds IS NOT NULL) +); + +CREATE INDEX sets_workout_idx ON workout_sets (workout_id, logged_at); +CREATE INDEX sets_exercise_ifx ON workout_sets (exercise_id, logged_at DESC); From 817c845e3b5b62a76cc7b87ddee7bf500f995edf Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 28 Aug 2026 14:18:04 +0200 Subject: [PATCH 7/9] fix: adding some exercises in migration --- .../migrations/000003_create_workouts.up.sql | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/internal/database/migrations/000003_create_workouts.up.sql b/internal/database/migrations/000003_create_workouts.up.sql index dad6084..4d836fb 100644 --- a/internal/database/migrations/000003_create_workouts.up.sql +++ b/internal/database/migrations/000003_create_workouts.up.sql @@ -7,7 +7,8 @@ CREATE TABLE exercises ( 'floor', 'rings', 'pull_up_bar', - 'dip_bars', + 'parallel_bars', + 'low_bar', 'parallettes', 'resistance_band' ]::TEXT[]), @@ -73,3 +74,28 @@ CREATE TABLE workout_sets ( CREATE INDEX sets_workout_idx ON workout_sets (workout_id, logged_at); CREATE INDEX sets_exercise_ifx ON workout_sets (exercise_id, logged_at DESC); + +INSERT INTO exercises (name, metric, load, tags, equipment) VALUES + ('Push-up', 'reps', 'bodyweight', '{push,chest,triceps}', '{floor,rings,parallettes}'), + ('Diamond push-up', 'reps', 'bodyweight', '{push,triceps}', '{floor,rings,parallettes}'), + ('Pseudo planche push-up','reps', 'bodyweight', '{push,shoulders,skill}', '{floor,rings,parallettes}'), + ('Pike push-up', 'reps', 'bodyweight', '{push,shoulders}', '{floor,rings,parallettes}'), + ('Dip', 'reps', 'bodyweight', '{push,chest,triceps}', '{rings,parallel_bars}'), + ('Handstand hold', 'seconds', 'bodyweight', '{push,shoulders,skill}', '{floor,rings,parallettes}'), + ('Handstand Push-up', 'reps', 'bodyweight', '{push,shoulders,skill}', '{floor,rings,parallettes}'), + ('Pull-up', 'reps', 'bodyweight', '{pull,back,biceps}', '{rings,pull_up_bar}'), + ('Chin-up', 'reps', 'bodyweight', '{pull,back,biceps}', '{rings,pull_up_bar}'), + ('Inverted row', 'reps', 'bodyweight', '{pull,back}', '{rings,parallel_bars,low_bar}'), + ('Muscle-up', 'reps', 'bodyweight', '{pull,push,skill}', '{rings,pull_up_bar}'), + ('Dead hang', 'seconds', 'bodyweight', '{pull,grip}', '{rings,pull_up_bar}'), + ('Front lever hold', 'seconds', 'bodyweight', '{pull,core,skill}', '{rings,pull_up_bar,low_bar}'), + ('Pistol squat', 'reps', 'bodyweight', '{legs}', '{floor}'), + ('Bulgarian split squat','reps', 'bodyweight', '{legs}', '{floor}'), + ('Nordic curl', 'reps', 'bodyweight', '{legs,hamstrings}', '{floor}'), + ('Calf raise', 'reps', 'bodyweight', '{legs}', '{floor}'), + ('Plank', 'seconds', 'bodyweight', '{core}', '{floor}'), + ('Hollow body hold', 'seconds', 'bodyweight', '{core}', '{floor}'), + ('L-sit', 'seconds', 'bodyweight', '{core,skill}', '{floor,parallettes,parallel_bars}'), + ('Hanging leg raise', 'reps', 'bodyweight', '{core}', '{rings,pull_up_bar}'), + ('Face pull', 'reps', 'bodyweight', '{pull,shoulders}', '{rings,resistance_band}'), + ('Lateral raise', 'reps', 'absolute', '{shoulders,isolation}', '{floor}'), From d68231c4c1a09be2bd55910294495ca885ff3349 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 28 Aug 2026 14:31:42 +0200 Subject: [PATCH 8/9] fix: update go version to 1.27 in Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f806404..7f49701 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.26-alpine AS build +FROM golang:1.27-alpine AS build RUN apk add --no-cache make git From dac38bd0a10c061aba870150dc4e8b2ffd2411ad Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 28 Aug 2026 14:44:41 +0200 Subject: [PATCH 9/9] fix: syntax errors in migration sql --- .../database/migrations/000003_create_workouts.up.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/database/migrations/000003_create_workouts.up.sql b/internal/database/migrations/000003_create_workouts.up.sql index 4d836fb..29b708c 100644 --- a/internal/database/migrations/000003_create_workouts.up.sql +++ b/internal/database/migrations/000003_create_workouts.up.sql @@ -13,12 +13,12 @@ CREATE TABLE exercises ( 'resistance_band' ]::TEXT[]), metric TEXT NOT NULL DEFAULT 'reps' - CHECK metric IN ('reps', 'seconds'), + CHECK (metric IN ('reps', 'seconds')), load TEXT NOT NULL DEFAULT 'bodyweight' - CHECK load IN ('bodyweight', 'absolute'), + CHECK (load IN ('bodyweight', 'absolute')), tags TEXT[] NOT NULL DEFAULT '{}', -- push/pull/legs/core/skill/shoulders/... notes TEXT, - hidden NOT NULL DEFAULT FALSE, + hidden BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); @@ -98,4 +98,4 @@ INSERT INTO exercises (name, metric, load, tags, equipment) VALUES ('L-sit', 'seconds', 'bodyweight', '{core,skill}', '{floor,parallettes,parallel_bars}'), ('Hanging leg raise', 'reps', 'bodyweight', '{core}', '{rings,pull_up_bar}'), ('Face pull', 'reps', 'bodyweight', '{pull,shoulders}', '{rings,resistance_band}'), - ('Lateral raise', 'reps', 'absolute', '{shoulders,isolation}', '{floor}'), + ('Lateral raise', 'reps', 'absolute', '{shoulders,isolation}', '{floor}');