Platforms no longer have a user_id: there's one global list, unique by name, that every authenticated user reads from and writes to. Games are unchanged - still private per user, still referencing platform_id. - Migration: dropped platform.user_id and its FK, UNIQUE(user_id, name) -> UNIQUE(name), index on change_seq alone instead of (user_id, change_seq). Amended the one init migration in place rather than stacking a second one, since nothing is deployed yet. - db/query.sql: platform queries no longer take a user_id parameter. DeletePlatform's NOT EXISTS guard already checked games from any user (it never filtered the subquery by user_id), so deleting a shared platform is correctly blocked by any user's games on it, not just the deleter's. - routes.go: platform handlers still require auth (the list isn't public to anonymous requests) but no longer scope by user. - Tests: updated the mocks for the new signatures, added TestListPlatformsIsSharedAcrossUsers. Verified against a real Postgres: user A creates "PC", user B sees it immediately, both can add games against it, each only sees their own games, deleting the platform is blocked while either user has a game on it, and the name stays globally unique. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
514 lines
11 KiB
Go
514 lines
11 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: query.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const createGame = `-- name: CreateGame :one
|
|
WITH inserted AS (
|
|
INSERT INTO game (
|
|
user_id, platform_id, name, score, release_date, finished
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6
|
|
)
|
|
RETURNING id, platform_id, name, score, release_date, finished
|
|
)
|
|
SELECT inserted.id, inserted.name, inserted.platform_id, platform.name AS platform_name,
|
|
inserted.score, inserted.release_date, inserted.finished
|
|
FROM inserted
|
|
JOIN platform ON platform.id = inserted.platform_id
|
|
`
|
|
|
|
type CreateGameParams struct {
|
|
UserID uuid.UUID
|
|
PlatformID uuid.UUID
|
|
Name string
|
|
Score int32
|
|
ReleaseDate pgtype.Date
|
|
Finished pgtype.Date
|
|
}
|
|
|
|
type CreateGameRow struct {
|
|
ID uuid.UUID
|
|
Name string
|
|
PlatformID uuid.UUID
|
|
PlatformName string
|
|
Score int32
|
|
ReleaseDate pgtype.Date
|
|
Finished pgtype.Date
|
|
}
|
|
|
|
func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (CreateGameRow, error) {
|
|
row := q.db.QueryRow(ctx, createGame,
|
|
arg.UserID,
|
|
arg.PlatformID,
|
|
arg.Name,
|
|
arg.Score,
|
|
arg.ReleaseDate,
|
|
arg.Finished,
|
|
)
|
|
var i CreateGameRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PlatformID,
|
|
&i.PlatformName,
|
|
&i.Score,
|
|
&i.ReleaseDate,
|
|
&i.Finished,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createPlatform = `-- name: CreatePlatform :one
|
|
INSERT INTO platform (
|
|
name
|
|
) VALUES (
|
|
$1
|
|
)
|
|
RETURNING id, name
|
|
`
|
|
|
|
type CreatePlatformRow struct {
|
|
ID uuid.UUID
|
|
Name string
|
|
}
|
|
|
|
func (q *Queries) CreatePlatform(ctx context.Context, name string) (CreatePlatformRow, error) {
|
|
row := q.db.QueryRow(ctx, createPlatform, name)
|
|
var i CreatePlatformRow
|
|
err := row.Scan(&i.ID, &i.Name)
|
|
return i, err
|
|
}
|
|
|
|
const createSession = `-- name: CreateSession :exec
|
|
INSERT INTO sessions (
|
|
token_hash, user_id, expires_at
|
|
) VALUES (
|
|
$1, $2, $3
|
|
)
|
|
`
|
|
|
|
type CreateSessionParams struct {
|
|
TokenHash []byte
|
|
UserID uuid.UUID
|
|
ExpiresAt pgtype.Timestamptz
|
|
}
|
|
|
|
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) error {
|
|
_, err := q.db.Exec(ctx, createSession, arg.TokenHash, arg.UserID, arg.ExpiresAt)
|
|
return err
|
|
}
|
|
|
|
const createUser = `-- name: CreateUser :one
|
|
INSERT INTO users (
|
|
email, password_hash, name, bio
|
|
) VALUES (
|
|
$1, $2, $3, $4
|
|
)
|
|
RETURNING id, email, name, bio, created_at
|
|
`
|
|
|
|
type CreateUserParams struct {
|
|
Email string
|
|
PasswordHash string
|
|
Name string
|
|
Bio pgtype.Text
|
|
}
|
|
|
|
type CreateUserRow struct {
|
|
ID uuid.UUID
|
|
Email string
|
|
Name string
|
|
Bio pgtype.Text
|
|
CreatedAt pgtype.Timestamptz
|
|
}
|
|
|
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
|
|
row := q.db.QueryRow(ctx, createUser,
|
|
arg.Email,
|
|
arg.PasswordHash,
|
|
arg.Name,
|
|
arg.Bio,
|
|
)
|
|
var i CreateUserRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Name,
|
|
&i.Bio,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteGame = `-- name: DeleteGame :execrows
|
|
UPDATE game
|
|
SET deleted_at = now()
|
|
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL
|
|
`
|
|
|
|
type DeleteGameParams struct {
|
|
ID uuid.UUID
|
|
UserID uuid.UUID
|
|
}
|
|
|
|
func (q *Queries) DeleteGame(ctx context.Context, arg DeleteGameParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, deleteGame, arg.ID, arg.UserID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const deletePlatform = `-- name: DeletePlatform :execrows
|
|
UPDATE platform
|
|
SET deleted_at = now()
|
|
WHERE platform.id = $1 AND platform.deleted_at IS NULL
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM game
|
|
WHERE game.platform_id = platform.id AND game.deleted_at IS NULL
|
|
)
|
|
`
|
|
|
|
// Soft delete, so the FK on game.platform_id is never touched and never
|
|
// blocks this on its own; the NOT EXISTS guard is what stops a platform
|
|
// that still has active games - from any user, since the list is shared -
|
|
// from being deleted. Call GetPlatform first to tell "not found" apart from
|
|
// "blocked by games" when this affects 0 rows.
|
|
func (q *Queries) DeletePlatform(ctx context.Context, id uuid.UUID) (int64, error) {
|
|
result, err := q.db.Exec(ctx, deletePlatform, id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const deleteSession = `-- name: DeleteSession :exec
|
|
DELETE FROM sessions
|
|
WHERE token_hash = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteSession(ctx context.Context, tokenHash []byte) error {
|
|
_, err := q.db.Exec(ctx, deleteSession, tokenHash)
|
|
return err
|
|
}
|
|
|
|
const getGame = `-- name: GetGame :one
|
|
SELECT game.id, game.name, game.platform_id, platform.name AS platform_name,
|
|
game.score, game.release_date, game.finished
|
|
FROM game
|
|
JOIN platform ON platform.id = game.platform_id
|
|
WHERE game.id = $1 AND game.user_id = $2 AND game.deleted_at IS NULL
|
|
`
|
|
|
|
type GetGameParams struct {
|
|
ID uuid.UUID
|
|
UserID uuid.UUID
|
|
}
|
|
|
|
type GetGameRow struct {
|
|
ID uuid.UUID
|
|
Name string
|
|
PlatformID uuid.UUID
|
|
PlatformName string
|
|
Score int32
|
|
ReleaseDate pgtype.Date
|
|
Finished pgtype.Date
|
|
}
|
|
|
|
func (q *Queries) GetGame(ctx context.Context, arg GetGameParams) (GetGameRow, error) {
|
|
row := q.db.QueryRow(ctx, getGame, arg.ID, arg.UserID)
|
|
var i GetGameRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PlatformID,
|
|
&i.PlatformName,
|
|
&i.Score,
|
|
&i.ReleaseDate,
|
|
&i.Finished,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getPlatform = `-- name: GetPlatform :one
|
|
SELECT id, name
|
|
FROM platform
|
|
WHERE id = $1 AND deleted_at IS NULL
|
|
`
|
|
|
|
type GetPlatformRow struct {
|
|
ID uuid.UUID
|
|
Name string
|
|
}
|
|
|
|
func (q *Queries) GetPlatform(ctx context.Context, id uuid.UUID) (GetPlatformRow, error) {
|
|
row := q.db.QueryRow(ctx, getPlatform, id)
|
|
var i GetPlatformRow
|
|
err := row.Scan(&i.ID, &i.Name)
|
|
return i, err
|
|
}
|
|
|
|
const getSessionAndUser = `-- name: GetSessionAndUser :one
|
|
SELECT users.id, users.email, users.name, users.bio, sessions.expires_at
|
|
FROM sessions
|
|
JOIN users ON users.id = sessions.user_id
|
|
WHERE sessions.token_hash = $1 AND sessions.expires_at > now()
|
|
`
|
|
|
|
type GetSessionAndUserRow struct {
|
|
ID uuid.UUID
|
|
Email string
|
|
Name string
|
|
Bio pgtype.Text
|
|
ExpiresAt pgtype.Timestamptz
|
|
}
|
|
|
|
func (q *Queries) GetSessionAndUser(ctx context.Context, tokenHash []byte) (GetSessionAndUserRow, error) {
|
|
row := q.db.QueryRow(ctx, getSessionAndUser, tokenHash)
|
|
var i GetSessionAndUserRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Name,
|
|
&i.Bio,
|
|
&i.ExpiresAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getUserByEmail = `-- name: GetUserByEmail :one
|
|
SELECT id, email, password_hash, name, bio, created_at
|
|
FROM users
|
|
WHERE email = $1
|
|
`
|
|
|
|
type GetUserByEmailRow struct {
|
|
ID uuid.UUID
|
|
Email string
|
|
PasswordHash string
|
|
Name string
|
|
Bio pgtype.Text
|
|
CreatedAt pgtype.Timestamptz
|
|
}
|
|
|
|
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error) {
|
|
row := q.db.QueryRow(ctx, getUserByEmail, email)
|
|
var i GetUserByEmailRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.PasswordHash,
|
|
&i.Name,
|
|
&i.Bio,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getUserByID = `-- name: GetUserByID :one
|
|
SELECT id, email, name, bio, created_at
|
|
FROM users
|
|
WHERE id = $1
|
|
`
|
|
|
|
type GetUserByIDRow struct {
|
|
ID uuid.UUID
|
|
Email string
|
|
Name string
|
|
Bio pgtype.Text
|
|
CreatedAt pgtype.Timestamptz
|
|
}
|
|
|
|
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, error) {
|
|
row := q.db.QueryRow(ctx, getUserByID, id)
|
|
var i GetUserByIDRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Name,
|
|
&i.Bio,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listGames = `-- name: ListGames :many
|
|
SELECT game.id, game.name, game.platform_id, platform.name AS platform_name,
|
|
game.score, game.release_date, game.finished
|
|
FROM game
|
|
JOIN platform ON platform.id = game.platform_id
|
|
WHERE game.user_id = $1 AND game.deleted_at IS NULL
|
|
ORDER BY game.name
|
|
`
|
|
|
|
type ListGamesRow struct {
|
|
ID uuid.UUID
|
|
Name string
|
|
PlatformID uuid.UUID
|
|
PlatformName string
|
|
Score int32
|
|
ReleaseDate pgtype.Date
|
|
Finished pgtype.Date
|
|
}
|
|
|
|
func (q *Queries) ListGames(ctx context.Context, userID uuid.UUID) ([]ListGamesRow, error) {
|
|
rows, err := q.db.Query(ctx, listGames, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListGamesRow
|
|
for rows.Next() {
|
|
var i ListGamesRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PlatformID,
|
|
&i.PlatformName,
|
|
&i.Score,
|
|
&i.ReleaseDate,
|
|
&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
|
|
WHERE deleted_at IS NULL
|
|
ORDER BY name
|
|
`
|
|
|
|
type ListPlatformsRow struct {
|
|
ID uuid.UUID
|
|
Name string
|
|
}
|
|
|
|
func (q *Queries) ListPlatforms(ctx context.Context) ([]ListPlatformsRow, error) {
|
|
rows, err := q.db.Query(ctx, listPlatforms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListPlatformsRow
|
|
for rows.Next() {
|
|
var i ListPlatformsRow
|
|
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 touchSession = `-- name: TouchSession :exec
|
|
UPDATE sessions
|
|
SET last_seen_at = now()
|
|
WHERE token_hash = $1
|
|
`
|
|
|
|
func (q *Queries) TouchSession(ctx context.Context, tokenHash []byte) error {
|
|
_, err := q.db.Exec(ctx, touchSession, tokenHash)
|
|
return err
|
|
}
|
|
|
|
const updateGame = `-- name: UpdateGame :one
|
|
WITH updated AS (
|
|
UPDATE game
|
|
SET platform_id = $3, name = $4, score = $5, release_date = $6, finished = $7
|
|
WHERE game.id = $1 AND game.user_id = $2 AND game.deleted_at IS NULL
|
|
RETURNING id, platform_id, name, score, release_date, finished
|
|
)
|
|
SELECT updated.id, updated.name, updated.platform_id, platform.name AS platform_name,
|
|
updated.score, updated.release_date, updated.finished
|
|
FROM updated
|
|
JOIN platform ON platform.id = updated.platform_id
|
|
`
|
|
|
|
type UpdateGameParams struct {
|
|
ID uuid.UUID
|
|
UserID uuid.UUID
|
|
PlatformID uuid.UUID
|
|
Name string
|
|
Score int32
|
|
ReleaseDate pgtype.Date
|
|
Finished pgtype.Date
|
|
}
|
|
|
|
type UpdateGameRow struct {
|
|
ID uuid.UUID
|
|
Name string
|
|
PlatformID uuid.UUID
|
|
PlatformName string
|
|
Score int32
|
|
ReleaseDate pgtype.Date
|
|
Finished pgtype.Date
|
|
}
|
|
|
|
func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) (UpdateGameRow, error) {
|
|
row := q.db.QueryRow(ctx, updateGame,
|
|
arg.ID,
|
|
arg.UserID,
|
|
arg.PlatformID,
|
|
arg.Name,
|
|
arg.Score,
|
|
arg.ReleaseDate,
|
|
arg.Finished,
|
|
)
|
|
var i UpdateGameRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PlatformID,
|
|
&i.PlatformName,
|
|
&i.Score,
|
|
&i.ReleaseDate,
|
|
&i.Finished,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updatePlatform = `-- name: UpdatePlatform :one
|
|
UPDATE platform
|
|
SET name = $2
|
|
WHERE id = $1 AND deleted_at IS NULL
|
|
RETURNING id, name
|
|
`
|
|
|
|
type UpdatePlatformParams struct {
|
|
ID uuid.UUID
|
|
Name string
|
|
}
|
|
|
|
type UpdatePlatformRow struct {
|
|
ID uuid.UUID
|
|
Name string
|
|
}
|
|
|
|
func (q *Queries) UpdatePlatform(ctx context.Context, arg UpdatePlatformParams) (UpdatePlatformRow, error) {
|
|
row := q.db.QueryRow(ctx, updatePlatform, arg.ID, arg.Name)
|
|
var i UpdatePlatformRow
|
|
err := row.Scan(&i.ID, &i.Name)
|
|
return i, err
|
|
}
|