Created a base to build on

This commit is contained in:
2025-12-22 21:19:31 +01:00
parent a1e33bd403
commit 47a9719eeb
20 changed files with 1590 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
package api
import (
"context"
"completed/internal/db"
)
type MockQuerier struct {
CreateGameFunc func(ctx context.Context, arg db.CreateGameParams) (db.Game, error)
CreatePlatformFunc func(ctx context.Context, name string) (db.Platform, error)
CreateUserFunc func(ctx context.Context, arg db.CreateUserParams) (db.User, error)
DeleteGameFunc func(ctx context.Context, id int64) error
DeletePlatformFunc func(ctx context.Context, id int64) error
DeleteUserFunc func(ctx context.Context, id int64) error
GetGameFunc func(ctx context.Context, id int64) (db.Game, error)
GetUserFunc func(ctx context.Context, id int64) (db.User, error)
ListGamesFunc func(ctx context.Context) ([]db.Game, error)
ListPlatformsFunc func(ctx context.Context) ([]db.Platform, error)
ListUsersFunc func(ctx context.Context) ([]db.User, error)
UpdateGameFunc func(ctx context.Context, arg db.UpdateGameParams) (db.Game, error)
UpdatePlatformFunc func(ctx context.Context, arg db.UpdatePlatformParams) (db.Platform, error)
}
func (m *MockQuerier) CreateGame(ctx context.Context, arg db.CreateGameParams) (db.Game, error) {
return m.CreateGameFunc(ctx, arg)
}
func (m *MockQuerier) CreatePlatform(ctx context.Context, name string) (db.Platform, error) {
return m.CreatePlatformFunc(ctx, name)
}
func (m *MockQuerier) CreateUser(ctx context.Context, arg db.CreateUserParams) (db.User, error) {
return m.CreateUserFunc(ctx, arg)
}
func (m *MockQuerier) DeleteGame(ctx context.Context, id int64) error {
return m.DeleteGameFunc(ctx, id)
}
func (m *MockQuerier) DeletePlatform(ctx context.Context, id int64) error {
return m.DeletePlatformFunc(ctx, id)
}
func (m *MockQuerier) DeleteUser(ctx context.Context, id int64) error {
return m.DeleteUserFunc(ctx, id)
}
func (m *MockQuerier) GetGame(ctx context.Context, id int64) (db.Game, error) {
return m.GetGameFunc(ctx, id)
}
func (m *MockQuerier) GetUser(ctx context.Context, id int64) (db.User, error) {
return m.GetUserFunc(ctx, id)
}
func (m *MockQuerier) ListGames(ctx context.Context) ([]db.Game, error) {
return m.ListGamesFunc(ctx)
}
func (m *MockQuerier) ListPlatforms(ctx context.Context) ([]db.Platform, error) {
return m.ListPlatformsFunc(ctx)
}
func (m *MockQuerier) ListUsers(ctx context.Context) ([]db.User, error) {
return m.ListUsersFunc(ctx)
}
func (m *MockQuerier) UpdateGame(ctx context.Context, arg db.UpdateGameParams) (db.Game, error) {
return m.UpdateGameFunc(ctx, arg)
}
func (m *MockQuerier) UpdatePlatform(ctx context.Context, arg db.UpdatePlatformParams) (db.Platform, error) {
return m.UpdatePlatformFunc(ctx, arg)
}

257
internal/api/routes.go Normal file
View File

@@ -0,0 +1,257 @@
package api
import (
"context"
"net/http"
"time"
"github.com/danielgtaylor/huma/v2"
"github.com/jackc/pgx/v5/pgtype"
"completed/internal/db"
)
func RegisterRoutes(api huma.API, queries db.Querier) {
// Root/Hello Route
huma.Register(api, huma.Operation{
OperationID: "get-hello",
Method: http.MethodGet,
Path: "/hello",
Summary: "Say Hello",
Description: "Returns a friendly hello message.",
}, func(ctx context.Context, input *struct{}) (*struct {
Body struct {
Message string `json:"message"`
}
}, error) {
resp := &struct {
Body struct {
Message string `json:"message"`
}
}{}
resp.Body.Message = "Hello from Huma + Echo!"
return resp, nil
})
registerPlatformRoutes(api, queries)
registerGameRoutes(api, queries)
}
// --- Platform Routes ---
// PlatformDTO ensures consistent JSON output
type PlatformDTO struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
type PlatformInput struct {
Name string `json:"name" doc:"Name of the platform"`
}
func registerPlatformRoutes(api huma.API, queries db.Querier) {
huma.Register(api, huma.Operation{
OperationID: "list-platforms",
Method: http.MethodGet,
Path: "/platforms",
Summary: "List Platforms",
}, func(ctx context.Context, input *struct{}) (*struct{ Body []PlatformDTO }, error) {
platforms, err := queries.ListPlatforms(ctx)
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
dtos := make([]PlatformDTO, len(platforms))
for i, p := range platforms {
dtos[i] = PlatformDTO{ID: p.ID, Name: p.Name}
}
return &struct{ Body []PlatformDTO }{Body: dtos}, nil
})
huma.Register(api, huma.Operation{
OperationID: "create-platform",
Method: http.MethodPost,
Path: "/platforms",
Summary: "Create Platform",
}, func(ctx context.Context, input *struct{ Body PlatformInput }) (*struct{ Body PlatformDTO }, error) {
platform, err := queries.CreatePlatform(ctx, input.Body.Name)
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return &struct{ Body PlatformDTO }{Body: PlatformDTO{ID: platform.ID, Name: platform.Name}}, nil
})
huma.Register(api, huma.Operation{
OperationID: "update-platform",
Method: http.MethodPut,
Path: "/platforms/{id}",
Summary: "Update Platform",
}, func(ctx context.Context, input *struct {
ID int64 `path:"id"`
Body PlatformInput
}) (*struct{ Body PlatformDTO }, error) {
platform, err := queries.UpdatePlatform(ctx, db.UpdatePlatformParams{
ID: input.ID,
Name: input.Body.Name,
})
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return &struct{ Body PlatformDTO }{Body: PlatformDTO{ID: platform.ID, Name: platform.Name}}, nil
})
huma.Register(api, huma.Operation{
OperationID: "delete-platform",
Method: http.MethodDelete,
Path: "/platforms/{id}",
Summary: "Delete Platform",
}, func(ctx context.Context, input *struct {
ID int64 `path:"id"`
}) (*struct{}, error) {
err := queries.DeletePlatform(ctx, input.ID)
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return nil, nil
})
}
// --- Game Routes ---
// GameDTO for API communication (Clean JSON)
type GameDTO struct {
ID int64 `json:"id"`
Name string `json:"name"`
PlatformID int64 `json:"platform_id"`
Score int32 `json:"score"`
ReleaseYear string `json:"release_year" format:"date" doc:"YYYY-MM-DD"`
Finished string `json:"finished,omitempty" format:"date" doc:"YYYY-MM-DD (optional)"`
}
type GameInput struct {
Name string `json:"name"`
PlatformID int64 `json:"platform_id"`
Score int32 `json:"score"`
ReleaseYear string `json:"release_year" format:"date" doc:"YYYY-MM-DD"`
Finished string `json:"finished,omitempty" format:"date" doc:"YYYY-MM-DD (optional)"`
}
// Helper to convert DB Game to DTO
func toGameDTO(g db.Game) GameDTO {
dto := GameDTO{
ID: g.ID,
Name: g.Name,
PlatformID: g.PlatformID,
Score: g.Score,
ReleaseYear: g.ReleaseYear.Time.Format("2006-01-02"),
}
if g.Finished.Valid {
dto.Finished = g.Finished.Time.Format("2006-01-02")
}
return dto
}
// Helper to parse date string to pgtype.Date
func parseDate(s string) (pgtype.Date, error) {
if s == "" {
return pgtype.Date{}, nil
}
t, err := time.Parse("2006-01-02", s)
if err != nil {
return pgtype.Date{}, err
}
return pgtype.Date{Time: t, Valid: true}, nil
}
func registerGameRoutes(api huma.API, queries db.Querier) {
huma.Register(api, huma.Operation{
OperationID: "list-games",
Method: http.MethodGet,
Path: "/games",
Summary: "List Games",
}, func(ctx context.Context, input *struct{}) (*struct{ Body []GameDTO }, error) {
games, err := queries.ListGames(ctx)
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
dtos := make([]GameDTO, len(games))
for i, g := range games {
dtos[i] = toGameDTO(g)
}
return &struct{ Body []GameDTO }{Body: dtos}, nil
})
huma.Register(api, huma.Operation{
OperationID: "create-game",
Method: http.MethodPost,
Path: "/games",
Summary: "Create Game",
}, func(ctx context.Context, input *struct{ Body GameInput }) (*struct{ Body GameDTO }, error) {
ry, err := parseDate(input.Body.ReleaseYear)
if err != nil {
return nil, huma.Error400BadRequest("Invalid release_year format (expected YYYY-MM-DD)")
}
fin, err := parseDate(input.Body.Finished)
if err != nil {
return nil, huma.Error400BadRequest("Invalid finished format (expected YYYY-MM-DD)")
}
game, err := queries.CreateGame(ctx, db.CreateGameParams{
Name: input.Body.Name,
PlatformID: input.Body.PlatformID,
Score: input.Body.Score,
ReleaseYear: ry,
Finished: fin,
})
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return &struct{ Body GameDTO }{Body: toGameDTO(game)}, nil
})
huma.Register(api, huma.Operation{
OperationID: "update-game",
Method: http.MethodPut,
Path: "/games/{id}",
Summary: "Update Game",
}, func(ctx context.Context, input *struct {
ID int64 `path:"id"`
Body GameInput
}) (*struct{ Body GameDTO }, error) {
ry, err := parseDate(input.Body.ReleaseYear)
if err != nil {
return nil, huma.Error400BadRequest("Invalid release_year format (expected YYYY-MM-DD)")
}
fin, err := parseDate(input.Body.Finished)
if err != nil {
return nil, huma.Error400BadRequest("Invalid finished format (expected YYYY-MM-DD)")
}
game, err := queries.UpdateGame(ctx, db.UpdateGameParams{
ID: input.ID,
Name: input.Body.Name,
PlatformID: input.Body.PlatformID,
Score: input.Body.Score,
ReleaseYear: ry,
Finished: fin,
})
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return &struct{ Body GameDTO }{Body: toGameDTO(game)}, nil
})
huma.Register(api, huma.Operation{
OperationID: "delete-game",
Method: http.MethodDelete,
Path: "/games/{id}",
Summary: "Delete Game",
}, func(ctx context.Context, input *struct {
ID int64 `path:"id"`
}) (*struct{}, error) {
err := queries.DeleteGame(ctx, input.ID)
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return nil, nil
})
}

View File

@@ -0,0 +1,66 @@
package api
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/danielgtaylor/huma/v2"
"github.com/danielgtaylor/huma/v2/adapters/humaecho"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"completed/internal/db"
)
func TestListGames(t *testing.T) {
// 1. Setup Mock
mockDB := &MockQuerier{
ListGamesFunc: func(ctx context.Context) ([]db.Game, error) {
return []db.Game{
{ID: 1, Name: "Test Game", Score: 90},
}, nil
},
}
// 2. Setup API
e := echo.New()
api := humaecho.New(e, huma.DefaultConfig("Test API", "1.0.0"))
RegisterRoutes(api, mockDB)
// 3. Execute Request
req := httptest.NewRequest(http.MethodGet, "/games", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// 4. Assert
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Test Game")
}
func TestCreatePlatform(t *testing.T) {
// 1. Setup Mock
mockDB := &MockQuerier{
CreatePlatformFunc: func(ctx context.Context, name string) (db.Platform, error) {
return db.Platform{ID: 1, Name: name}, nil
},
}
// 2. Setup API
e := echo.New()
api := humaecho.New(e, huma.DefaultConfig("Test API", "1.0.0"))
RegisterRoutes(api, mockDB)
// 3. Execute Request
req := httptest.NewRequest(http.MethodPost, "/platforms", strings.NewReader(`{"name": "Sega Genesis"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// 4. Assert
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Sega Genesis")
assert.Contains(t, rec.Body.String(), `"id":1`)
}

32
internal/db/db.go Normal file
View File

@@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

29
internal/db/models.go Normal file
View File

@@ -0,0 +1,29 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"github.com/jackc/pgx/v5/pgtype"
)
type Game struct {
ID int64
Name string
PlatformID int64
Score int32
ReleaseYear pgtype.Date
Finished pgtype.Date
}
type Platform struct {
ID int64
Name string
}
type User struct {
ID int64
Name string
Bio pgtype.Text
}

27
internal/db/querier.go Normal file
View File

@@ -0,0 +1,27 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"context"
)
type Querier interface {
CreateGame(ctx context.Context, arg CreateGameParams) (Game, error)
CreatePlatform(ctx context.Context, name string) (Platform, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
DeleteGame(ctx context.Context, id int64) error
DeletePlatform(ctx context.Context, id int64) error
DeleteUser(ctx context.Context, id int64) error
GetGame(ctx context.Context, id int64) (Game, error)
GetUser(ctx context.Context, id int64) (User, error)
ListGames(ctx context.Context) ([]Game, error)
ListPlatforms(ctx context.Context) ([]Platform, error)
ListUsers(ctx context.Context) ([]User, error)
UpdateGame(ctx context.Context, arg UpdateGameParams) (Game, error)
UpdatePlatform(ctx context.Context, arg UpdatePlatformParams) (Platform, error)
}
var _ Querier = (*Queries)(nil)

285
internal/db/query.sql.go Normal file
View File

@@ -0,0 +1,285 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: query.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createGame = `-- name: CreateGame :one
INSERT INTO game (
name, platform_id, score, release_year, finished
) VALUES (
$1, $2, $3, $4, $5
)
RETURNING id, name, platform_id, score, release_year, finished
`
type CreateGameParams struct {
Name string
PlatformID int64
Score int32
ReleaseYear pgtype.Date
Finished pgtype.Date
}
func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (Game, error) {
row := q.db.QueryRow(ctx, createGame,
arg.Name,
arg.PlatformID,
arg.Score,
arg.ReleaseYear,
arg.Finished,
)
var i Game
err := row.Scan(
&i.ID,
&i.Name,
&i.PlatformID,
&i.Score,
&i.ReleaseYear,
&i.Finished,
)
return i, err
}
const createPlatform = `-- name: CreatePlatform :one
INSERT INTO platform (
name
) VALUES (
$1
)
RETURNING id, name
`
func (q *Queries) CreatePlatform(ctx context.Context, name string) (Platform, error) {
row := q.db.QueryRow(ctx, createPlatform, name)
var i Platform
err := row.Scan(&i.ID, &i.Name)
return i, err
}
const createUser = `-- name: CreateUser :one
INSERT INTO users (
name, bio
) VALUES (
$1, $2
)
RETURNING id, name, bio
`
type CreateUserParams struct {
Name string
Bio pgtype.Text
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRow(ctx, createUser, arg.Name, arg.Bio)
var i User
err := row.Scan(&i.ID, &i.Name, &i.Bio)
return i, err
}
const deleteGame = `-- name: DeleteGame :exec
DELETE FROM game
WHERE id = $1
`
func (q *Queries) DeleteGame(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, deleteGame, id)
return err
}
const deletePlatform = `-- name: DeletePlatform :exec
DELETE FROM platform
WHERE id = $1
`
func (q *Queries) DeletePlatform(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, deletePlatform, id)
return err
}
const deleteUser = `-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1
`
func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, deleteUser, id)
return err
}
const getGame = `-- name: GetGame :one
SELECT id, name, platform_id, score, release_year, finished FROM game
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetGame(ctx context.Context, id int64) (Game, error) {
row := q.db.QueryRow(ctx, getGame, id)
var i Game
err := row.Scan(
&i.ID,
&i.Name,
&i.PlatformID,
&i.Score,
&i.ReleaseYear,
&i.Finished,
)
return i, err
}
const getUser = `-- name: GetUser :one
SELECT id, name, bio FROM users
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) {
row := q.db.QueryRow(ctx, getUser, id)
var i User
err := row.Scan(&i.ID, &i.Name, &i.Bio)
return i, err
}
const listGames = `-- name: ListGames :many
SELECT id, name, platform_id, score, release_year, finished FROM game
ORDER BY name
`
func (q *Queries) ListGames(ctx context.Context) ([]Game, error) {
rows, err := q.db.Query(ctx, listGames)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Game
for rows.Next() {
var i Game
if err := rows.Scan(
&i.ID,
&i.Name,
&i.PlatformID,
&i.Score,
&i.ReleaseYear,
&i.Finished,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPlatforms = `-- name: ListPlatforms :many
SELECT id, name FROM platform
ORDER BY name
`
func (q *Queries) ListPlatforms(ctx context.Context) ([]Platform, error) {
rows, err := q.db.Query(ctx, listPlatforms)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Platform
for rows.Next() {
var i Platform
if err := rows.Scan(&i.ID, &i.Name); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listUsers = `-- name: ListUsers :many
SELECT id, name, bio FROM users
ORDER BY name
`
func (q *Queries) ListUsers(ctx context.Context) ([]User, error) {
rows, err := q.db.Query(ctx, listUsers)
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
for rows.Next() {
var i User
if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateGame = `-- name: UpdateGame :one
UPDATE game
SET name = $2, platform_id = $3, score = $4, release_year = $5, finished = $6
WHERE id = $1
RETURNING id, name, platform_id, score, release_year, finished
`
type UpdateGameParams struct {
ID int64
Name string
PlatformID int64
Score int32
ReleaseYear pgtype.Date
Finished pgtype.Date
}
func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) (Game, error) {
row := q.db.QueryRow(ctx, updateGame,
arg.ID,
arg.Name,
arg.PlatformID,
arg.Score,
arg.ReleaseYear,
arg.Finished,
)
var i Game
err := row.Scan(
&i.ID,
&i.Name,
&i.PlatformID,
&i.Score,
&i.ReleaseYear,
&i.Finished,
)
return i, err
}
const updatePlatform = `-- name: UpdatePlatform :one
UPDATE platform
SET name = $2
WHERE id = $1
RETURNING id, name
`
type UpdatePlatformParams struct {
ID int64
Name string
}
func (q *Queries) UpdatePlatform(ctx context.Context, arg UpdatePlatformParams) (Platform, error) {
row := q.db.QueryRow(ctx, updatePlatform, arg.ID, arg.Name)
var i Platform
err := row.Scan(&i.ID, &i.Name)
return i, err
}