Created a base to build on
This commit is contained in:
45
Dockerfile
Normal file
45
Dockerfile
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Build Stage
|
||||||
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install git and build dependencies
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
# Copy go mod and sum files
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
# CGO_ENABLED=0 ensures a static binary
|
||||||
|
ARG TARGETOS TARGETARCH
|
||||||
|
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o api-server ./cmd/api/main.go
|
||||||
|
|
||||||
|
# Runtime Stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install ca-certificates for database connections (TLS)
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
# Copy the binary from the builder stage
|
||||||
|
COPY --from=builder /app/api-server .
|
||||||
|
|
||||||
|
# Copy migrations
|
||||||
|
COPY --from=builder /app/db/migrations ./db/migrations
|
||||||
|
|
||||||
|
# Expose the application port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Environment variables with defaults (can be overridden)
|
||||||
|
ENV PORT=8080
|
||||||
|
ENV DATABASE_URL=""
|
||||||
|
|
||||||
|
# Run the binary
|
||||||
|
CMD ["./api-server"]
|
||||||
65
Justfile
Normal file
65
Justfile
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Justfile for Go Project Management
|
||||||
|
|
||||||
|
# List all available recipes
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
# --- Backend ---
|
||||||
|
|
||||||
|
# Build the API server
|
||||||
|
build-api:
|
||||||
|
go build -o bin/api ./cmd/api
|
||||||
|
|
||||||
|
# Run the API server locally
|
||||||
|
run-api:
|
||||||
|
go run ./cmd/api/main.go
|
||||||
|
|
||||||
|
# Start the API and Database using Docker Compose
|
||||||
|
docker-up:
|
||||||
|
docker-compose up --build -d
|
||||||
|
|
||||||
|
# Stop Docker Compose services
|
||||||
|
docker-down:
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run tests with verbose output
|
||||||
|
test-v:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
rm -rf bin/
|
||||||
|
|
||||||
|
# --- TUI ---
|
||||||
|
|
||||||
|
# Build the Terminal UI
|
||||||
|
build-tui:
|
||||||
|
go build -o bin/tui ./cmd/tui
|
||||||
|
|
||||||
|
# Run the Terminal UI
|
||||||
|
run-tui:
|
||||||
|
go run ./cmd/tui/main.go
|
||||||
|
|
||||||
|
# --- GUI ---
|
||||||
|
|
||||||
|
# Build the GUI (Raylib)
|
||||||
|
build-gui:
|
||||||
|
go build -o bin/gui ./cmd/gui
|
||||||
|
|
||||||
|
# Run the GUI
|
||||||
|
run-gui:
|
||||||
|
go run ./cmd/gui/main.go
|
||||||
|
|
||||||
|
# --- Database ---
|
||||||
|
|
||||||
|
# Generate Go code from SQL (requires sqlc)
|
||||||
|
sqlc:
|
||||||
|
sqlc generate
|
||||||
|
|
||||||
|
# Connect to the running database via psql
|
||||||
|
db-shell:
|
||||||
|
docker-compose exec db psql -U postgres -d gamedb
|
||||||
138
cmd/api/main.go
Normal file
138
cmd/api/main.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/danielgtaylor/huma/v2"
|
||||||
|
"github.com/danielgtaylor/huma/v2/adapters/humaecho"
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
|
||||||
|
"completed/internal/api"
|
||||||
|
"completed/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureDatabase(dbURL string) error {
|
||||||
|
u, err := url.Parse(dbURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbName := strings.TrimPrefix(u.Path, "/")
|
||||||
|
if dbName == "" {
|
||||||
|
return fmt.Errorf("database name is empty in URL: %s", dbURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to 'postgres' database to create the new DB
|
||||||
|
u.Path = "/postgres"
|
||||||
|
db, err := sql.Open("postgres", u.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Check if DB exists
|
||||||
|
var exists bool
|
||||||
|
err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)", dbName).Scan(&exists)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
fmt.Printf("Database '%s' does not exist. Creating...\n", dbName)
|
||||||
|
_, err = db.Exec(fmt.Sprintf("CREATE DATABASE \"%s\"", dbName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Database '%s' created successfully.\n", dbName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Database '%s' already exists.\n", dbName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrations(dbURL string) {
|
||||||
|
m, err := migrate.New(
|
||||||
|
"file:///app/db/migrations", // Path inside Docker container
|
||||||
|
dbURL,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback for local development if not in /app
|
||||||
|
if os.Getenv("MIGRATION_PATH") != "" {
|
||||||
|
m, err = migrate.New(
|
||||||
|
"file://"+os.Getenv("MIGRATION_PATH"),
|
||||||
|
dbURL,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Try relative path default
|
||||||
|
m, err = migrate.New(
|
||||||
|
"file://db/migrations",
|
||||||
|
dbURL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Migration initialization failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
|
||||||
|
fmt.Fprintf(os.Stderr, "Migration failed: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Migrations ran successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 1. Database connection
|
||||||
|
dbURL := os.Getenv("DATABASE_URL")
|
||||||
|
if dbURL == "" {
|
||||||
|
dbURL = "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure Database Exists
|
||||||
|
if err := ensureDatabase(dbURL); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to ensure database: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Migrations
|
||||||
|
runMigrations(dbURL)
|
||||||
|
|
||||||
|
pool, err := pgxpool.New(context.Background(), dbURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
|
||||||
|
queries := db.New(pool)
|
||||||
|
|
||||||
|
// 2. Setup Echo
|
||||||
|
e := echo.New()
|
||||||
|
e.Use(middleware.Logger())
|
||||||
|
e.Use(middleware.Recover())
|
||||||
|
|
||||||
|
// 3. Setup Huma
|
||||||
|
humaApi := humaecho.New(e, huma.DefaultConfig("My API", "1.0.0"))
|
||||||
|
|
||||||
|
// 4. Register Routes
|
||||||
|
api.RegisterRoutes(humaApi, queries)
|
||||||
|
|
||||||
|
// 5. Start Server
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
e.Logger.Fatal(e.Start(":" + port))
|
||||||
|
}
|
||||||
145
cmd/gui/main.go
Normal file
145
cmd/gui/main.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PlatformID int64 `json:"platform_id"`
|
||||||
|
Score int32 `json:"score"`
|
||||||
|
ReleaseYear string `json:"release_year"`
|
||||||
|
Finished string `json:"finished,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GamesResponse struct {
|
||||||
|
Body []Game `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rl.InitWindow(800, 600, "My Game Collection - Raylib")
|
||||||
|
defer rl.CloseWindow()
|
||||||
|
|
||||||
|
rl.SetTargetFPS(60)
|
||||||
|
|
||||||
|
loading := false
|
||||||
|
var err error
|
||||||
|
var games []Game
|
||||||
|
|
||||||
|
// Channel to receive data from background goroutine
|
||||||
|
dataChan := make(chan []Game)
|
||||||
|
errChan := make(chan error)
|
||||||
|
|
||||||
|
func() {
|
||||||
|
loading = true
|
||||||
|
go fetchGames(dataChan, errChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for !rl.WindowShouldClose() {
|
||||||
|
rl.BeginDrawing()
|
||||||
|
rl.ClearBackground(rl.RayWhite)
|
||||||
|
|
||||||
|
// Header
|
||||||
|
rl.DrawRectangle(0, 0, 800, 60, rl.NewColor(100, 100, 255, 255))
|
||||||
|
rl.DrawText("My Game Collection", 20, 15, 30, rl.White)
|
||||||
|
|
||||||
|
startY := 80
|
||||||
|
|
||||||
|
if loading {
|
||||||
|
rl.DrawText("Loading games...", 20, int32(startY), 20, rl.Gray)
|
||||||
|
// Simple spinner animation
|
||||||
|
if int(rl.GetTime()*4)%2 == 0 {
|
||||||
|
rl.DrawText(".", 180, int32(startY), 20, rl.Gray)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
rl.DrawText(fmt.Sprintf("Error: %v", err), 20, int32(startY), 20, rl.Red)
|
||||||
|
rl.DrawText("Press SPACE to retry", 20, int32(startY)+30, 20, rl.DarkGray)
|
||||||
|
} else {
|
||||||
|
if len(games) == 0 {
|
||||||
|
rl.DrawText("No games found in database.", 20, int32(startY), 20, rl.DarkGray)
|
||||||
|
} else {
|
||||||
|
for i, g := range games {
|
||||||
|
yPos := int32(startY + (i * 40))
|
||||||
|
|
||||||
|
// Row background (alternating)
|
||||||
|
if i%2 == 0 {
|
||||||
|
rl.DrawRectangle(20, yPos-5, 760, 35, rl.NewColor(240, 240, 240, 255))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game Name
|
||||||
|
rl.DrawText(g.Name, 30, yPos, 20, rl.Black)
|
||||||
|
|
||||||
|
// Year
|
||||||
|
rl.DrawText(g.ReleaseYear, 350, yPos, 20, rl.DarkGray)
|
||||||
|
|
||||||
|
// Score
|
||||||
|
scoreColor := rl.Orange
|
||||||
|
if g.Score >= 90 {
|
||||||
|
scoreColor = rl.Green
|
||||||
|
} else if g.Score < 70 {
|
||||||
|
scoreColor = rl.Red
|
||||||
|
}
|
||||||
|
rl.DrawText(fmt.Sprintf("%d", g.Score), 500, yPos, 20, scoreColor)
|
||||||
|
|
||||||
|
// Finished Status
|
||||||
|
if g.Finished != "" {
|
||||||
|
rl.DrawText("Finished", 600, yPos, 20, rl.NewColor(0, 200, 0, 255))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer / Help
|
||||||
|
rl.DrawText("Press SPACE to refresh", 20, 570, 10, rl.Gray)
|
||||||
|
|
||||||
|
rl.EndDrawing()
|
||||||
|
|
||||||
|
// Inputs
|
||||||
|
if rl.IsKeyPressed(rl.KeySpace) && !loading {
|
||||||
|
loading = true
|
||||||
|
err = nil
|
||||||
|
games = nil
|
||||||
|
go fetchGames(dataChan, errChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check channels (non-blocking)
|
||||||
|
select {
|
||||||
|
case data := <-dataChan:
|
||||||
|
loading = false
|
||||||
|
games = data
|
||||||
|
case e := <-errChan:
|
||||||
|
loading = false
|
||||||
|
err = e
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchGames(dataChan chan<- []Game, errChan chan<- error) {
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Get("http://localhost:8080/games")
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
errChan <- fmt.Errorf("status code: %d", resp.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data GamesResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||||
|
errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataChan <- data.Body
|
||||||
|
}
|
||||||
150
cmd/tui/main.go
Normal file
150
cmd/tui/main.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
var (
|
||||||
|
titleStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#FAFAFA")).
|
||||||
|
Background(lipgloss.Color("#7D56F4")).
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
itemStyle = lipgloss.NewStyle().
|
||||||
|
PaddingLeft(2)
|
||||||
|
|
||||||
|
scoreStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#FFD700")) // Gold color
|
||||||
|
|
||||||
|
errorStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#FF5555")).
|
||||||
|
Bold(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PlatformID int64 `json:"platform_id"`
|
||||||
|
Score int32 `json:"score"`
|
||||||
|
ReleaseYear string `json:"release_year"`
|
||||||
|
Finished string `json:"finished,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GamesResponse struct {
|
||||||
|
Body []Game `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
spinner spinner.Model
|
||||||
|
loading bool
|
||||||
|
err error
|
||||||
|
games []Game
|
||||||
|
quitting bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitialModel() Model {
|
||||||
|
s := spinner.New()
|
||||||
|
s.Spinner = spinner.Dot
|
||||||
|
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
|
||||||
|
return Model{spinner: s, loading: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
type loadedMsg []Game
|
||||||
|
type errMsg error
|
||||||
|
|
||||||
|
func fetchGames() tea.Msg {
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Get("http://localhost:8080/games")
|
||||||
|
if err != nil {
|
||||||
|
return errMsg(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return errMsg(fmt.Errorf("status code: %d", resp.StatusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
var data GamesResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||||
|
return errMsg(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadedMsg(data.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Init() tea.Cmd {
|
||||||
|
return tea.Batch(m.spinner.Tick, fetchGames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
if msg.String() == "q" || msg.String() == "ctrl+c" {
|
||||||
|
m.quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
case spinner.TickMsg:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.spinner, cmd = m.spinner.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
case loadedMsg:
|
||||||
|
m.loading = false
|
||||||
|
m.games = msg
|
||||||
|
return m, nil
|
||||||
|
case errMsg:
|
||||||
|
m.loading = false
|
||||||
|
m.err = msg
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) View() string {
|
||||||
|
if m.quitting {
|
||||||
|
return "Bye!\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
s := titleStyle.Render("My Game Collection") + "\n\n"
|
||||||
|
|
||||||
|
if m.loading {
|
||||||
|
s += fmt.Sprintf("%s Loading games...", m.spinner.View())
|
||||||
|
} else if m.err != nil {
|
||||||
|
s += errorStyle.Render(fmt.Sprintf("Error: %v", m.err))
|
||||||
|
s += "\nMake sure the API is running on localhost:8080"
|
||||||
|
} else {
|
||||||
|
if len(m.games) == 0 {
|
||||||
|
s += "No games found."
|
||||||
|
} else {
|
||||||
|
for _, g := range m.games {
|
||||||
|
score := scoreStyle.Render(fmt.Sprintf("★ %d", g.Score))
|
||||||
|
line := fmt.Sprintf("• %s (%s) - %s", g.Name, g.ReleaseYear, score)
|
||||||
|
if g.Finished != "" {
|
||||||
|
line += " [✓ Finished]"
|
||||||
|
}
|
||||||
|
s += itemStyle.Render(line) + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s += strings.Repeat("\n", 2) + lipgloss.NewStyle().Foreground(lipgloss.Color("#555")).Render("Press 'q' to quit")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
p := tea.NewProgram(InitialModel())
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
fmt.Printf("Alas, there's been an error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
db/migrations/20251222211500_add_auth_to_users.down.sql
Normal file
3
db/migrations/20251222211500_add_auth_to_users.down.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
DROP COLUMN email,
|
||||||
|
DROP COLUMN password_hash;
|
||||||
3
db/migrations/20251222211500_add_auth_to_users.up.sql
Normal file
3
db/migrations/20251222211500_add_auth_to_users.up.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN email TEXT NOT NULL UNIQUE,
|
||||||
|
ADD COLUMN password_hash TEXT NOT NULL;
|
||||||
67
db/query.sql
Normal file
67
db/query.sql
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
-- name: GetUser :one
|
||||||
|
SELECT * FROM users
|
||||||
|
WHERE id = $1 LIMIT 1;
|
||||||
|
|
||||||
|
-- name: ListUsers :many
|
||||||
|
SELECT * FROM users
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
-- name: CreateUser :one
|
||||||
|
INSERT INTO users (
|
||||||
|
name, bio
|
||||||
|
) VALUES (
|
||||||
|
$1, $2
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteUser :exec
|
||||||
|
DELETE FROM users
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: CreatePlatform :one
|
||||||
|
INSERT INTO platform (
|
||||||
|
name
|
||||||
|
) VALUES (
|
||||||
|
$1
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: ListPlatforms :many
|
||||||
|
SELECT * FROM platform
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
-- name: CreateGame :one
|
||||||
|
INSERT INTO game (
|
||||||
|
name, platform_id, score, release_year, finished
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetGame :one
|
||||||
|
SELECT * FROM game
|
||||||
|
WHERE id = $1 LIMIT 1;
|
||||||
|
|
||||||
|
-- name: ListGames :many
|
||||||
|
SELECT * FROM game
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
-- name: UpdateGame :one
|
||||||
|
UPDATE game
|
||||||
|
SET name = $2, platform_id = $3, score = $4, release_year = $5, finished = $6
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteGame :exec
|
||||||
|
DELETE FROM game
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: UpdatePlatform :one
|
||||||
|
UPDATE platform
|
||||||
|
SET name = $2
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeletePlatform :exec
|
||||||
|
DELETE FROM platform
|
||||||
|
WHERE id = $1;
|
||||||
21
db/schema.sql
Normal file
21
db/schema.sql
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE users (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name text NOT NULL,
|
||||||
|
email text NOT NULL UNIQUE,
|
||||||
|
password_hash text NOT NULL,
|
||||||
|
bio text
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE platform (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE game (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name text NOT NULL,
|
||||||
|
platform_id BIGINT NOT NULL REFERENCES platform(id),
|
||||||
|
score INT NOT NULL,
|
||||||
|
release_year DATE NOT NULL,
|
||||||
|
finished DATE
|
||||||
|
);
|
||||||
28
docker-compose.yaml
Normal file
28
docker-compose.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- PORT=8080
|
||||||
|
- DATABASE_URL=postgres://postgres:password@db:5432/gamedb?sslmode=disable
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
restart: on-failure
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
- POSTGRES_DB=gamedb
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-data:
|
||||||
52
go.mod
Normal file
52
go.mod
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
module completed
|
||||||
|
|
||||||
|
go 1.25.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
github.com/danielgtaylor/huma/v2 v2.34.1
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6
|
||||||
|
github.com/labstack/echo/v4 v4.14.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/ebitengine/purego v0.7.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/gen2brain/raylib-go/raylib v0.55.1 // indirect
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.19.1 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/crypto v0.46.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||||
|
golang.org/x/net v0.48.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
|
golang.org/x/text v0.32.0 // indirect
|
||||||
|
golang.org/x/time v0.14.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
103
go.sum
Normal file
103
go.sum
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/danielgtaylor/huma/v2 v2.34.1 h1:EmOJAbzEGfy0wAq/QMQ1YKfEMBEfE94xdBRLPBP0gwQ=
|
||||||
|
github.com/danielgtaylor/huma/v2 v2.34.1/go.mod h1:ynwJgLk8iGVgoaipi5tgwIQ5yoFNmiu+QdhU7CEEmhk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
|
||||||
|
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/gen2brain/raylib-go/raylib v0.55.1 h1:1rdc10WvvYjtj7qijHnV9T38/WuvlT6IIL+PaZ6cNA8=
|
||||||
|
github.com/gen2brain/raylib-go/raylib v0.55.1/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/labstack/echo/v4 v4.14.0 h1:+tiMrDLxwv6u0oKtD03mv+V1vXXB3wCqPHJqPuIe+7M=
|
||||||
|
github.com/labstack/echo/v4 v4.14.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||||
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||||
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
63
internal/api/mock_db_test.go
Normal file
63
internal/api/mock_db_test.go
Normal 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
257
internal/api/routes.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
66
internal/api/routes_test.go
Normal file
66
internal/api/routes_test.go
Normal 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
32
internal/db/db.go
Normal 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
29
internal/db/models.go
Normal 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
27
internal/db/querier.go
Normal 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
285
internal/db/query.sql.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user