Merge pull request 'Added workouts database schema' (#25) from dev into main

This commit was merged in pull request #25.
This commit is contained in:
2026-08-28 14:47:13 +02:00
9 changed files with 210 additions and 30 deletions
+1 -1
View File
@@ -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
+72
View File
@@ -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
}
+2 -1
View File
@@ -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)
}
+16 -18
View File
@@ -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
)
+3 -6
View File
@@ -2,9 +2,9 @@ package database
import (
"database/sql"
"embed"
"errors"
"fmt"
"io/fs"
"log/slog"
"github.com/golang-migrate/migrate/v4"
@@ -12,11 +12,8 @@ import (
"github.com/golang-migrate/migrate/v4/source/iofs"
)
//go:embed migrations/*.sql
var migrationFS embed.FS
func RunMigrations(db *sql.DB) error {
sourceDriver, err := iofs.New(migrationFS, "migrations")
func RunMigrations(db *sql.DB, migrationFS fs.FS) error {
sourceDriver, err := iofs.New(migrationFS, ".")
if err != nil {
return fmt.Errorf("failed to load embedded migrations: %w", err)
}
@@ -0,0 +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;
@@ -0,0 +1,101 @@
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',
'parallel_bars',
'low_bar',
'parallettes',
'resistance_band'
]::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 BOOLEAN 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)
);
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);
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}');
+6
View File
@@ -0,0 +1,6 @@
package migrations
import "embed"
//go:embed *.sql
var MigrationFS embed.FS
+4 -4
View File
@@ -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)