Added workouts database schema #25
+1
-1
@@ -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
|
RUN apk add --no-cache make git
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/robindittmar/dttmr-api/internal/config"
|
"github.com/robindittmar/dttmr-api/internal/config"
|
||||||
"github.com/robindittmar/dttmr-api/internal/database"
|
"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/domain"
|
||||||
"github.com/robindittmar/dttmr-api/internal/repository"
|
"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))
|
slog.Error("failed to run migrations", slog.Any("error", err))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module github.com/robindittmar/dttmr-api
|
module github.com/robindittmar/dttmr-api
|
||||||
|
|
||||||
go 1.26
|
go 1.27
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
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/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/jackc/pgx/v5 v5.10.0
|
github.com/jackc/pgx/v5 v5.10.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.12.1
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0
|
||||||
go.opentelemetry.io/otel v1.45.0
|
go.opentelemetry.io/otel v1.46.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.45.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0
|
||||||
go.opentelemetry.io/otel/sdk v1.45.0
|
go.opentelemetry.io/otel/sdk v1.46.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.45.0
|
go.opentelemetry.io/otel/sdk/metric v1.46.0
|
||||||
go.opentelemetry.io/otel/trace v1.45.0
|
go.opentelemetry.io/otel/trace v1.46.0
|
||||||
golang.org/x/crypto v0.55.0
|
golang.org/x/crypto v0.55.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // 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/felixge/httpsnoop v1.1.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.4 // indirect
|
github.com/go-logr/logr v1.4.4 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // 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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/lib/pq v1.12.3 // 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.3 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // 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/exporters/otlp/otlptrace v1.46.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
go.opentelemetry.io/otel/metric v1.46.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.11.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/net v0.58.0 // indirect
|
||||||
golang.org/x/sync v0.22.0 // indirect
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
golang.org/x/sys v0.47.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
golang.org/x/text v0.41.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/api v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||||
google.golang.org/grpc v1.83.1 // indirect
|
google.golang.org/grpc v1.83.2 // indirect
|
||||||
google.golang.org/protobuf v1.36.12 // indirect
|
google.golang.org/protobuf v1.36.12 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"embed"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
"github.com/golang-migrate/migrate/v4"
|
||||||
@@ -12,11 +12,8 @@ import (
|
|||||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
func RunMigrations(db *sql.DB, migrationFS fs.FS) error {
|
||||||
var migrationFS embed.FS
|
sourceDriver, err := iofs.New(migrationFS, ".")
|
||||||
|
|
||||||
func RunMigrations(db *sql.DB) error {
|
|
||||||
sourceDriver, err := iofs.New(migrationFS, "migrations")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load embedded migrations: %w", err)
|
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}');
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed *.sql
|
||||||
|
var MigrationFS embed.FS
|
||||||
@@ -149,11 +149,11 @@ func (s *AuthService) issueTokens(ctx context.Context, authUser *AuthUser) (Toke
|
|||||||
|
|
||||||
func (s *AuthService) GenerateAccessToken(authUser *AuthUser) (string, error) {
|
func (s *AuthService) GenerateAccessToken(authUser *AuthUser) (string, error) {
|
||||||
claims := JWTClaims{
|
claims := JWTClaims{
|
||||||
UserID: authUser.ID,
|
UserID: authUser.ID,
|
||||||
Email: authUser.Email,
|
Email: authUser.Email,
|
||||||
Name: authUser.Name,
|
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)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user