Make the platform list shared across users; keep games private

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>
This commit is contained in:
SansanandClaude Sonnet 5 committed 2026-10-03 11:34:47 +02:00
1 parent 5072919b61
commit 7fbd403d30
8 files changed
+93 -86

No files matched your search

+21 -36
View File
@@ -70,25 +70,20 @@ func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (CreateG
const createPlatform = `-- name: CreatePlatform :one
INSERT INTO platform (
user_id, name
name
) VALUES (
$1, $2
$1
)
RETURNING id, name
`
type CreatePlatformParams struct {
UserID uuid.UUID
Name string
}
type CreatePlatformRow struct {
ID uuid.UUID
Name string
}
func (q *Queries) CreatePlatform(ctx context.Context, arg CreatePlatformParams) (CreatePlatformRow, error) {
row := q.db.QueryRow(ctx, createPlatform, arg.UserID, arg.Name)
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
@@ -177,24 +172,20 @@ func (q *Queries) DeleteGame(ctx context.Context, arg DeleteGameParams) (int64,
const deletePlatform = `-- name: DeletePlatform :execrows
UPDATE platform
SET deleted_at = now()
WHERE platform.id = $1 AND platform.user_id = $2 AND platform.deleted_at IS NULL
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
)
`
type DeletePlatformParams struct {
ID uuid.UUID
UserID uuid.UUID
}
// 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 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, arg DeletePlatformParams) (int64, error) {
result, err := q.db.Exec(ctx, deletePlatform, arg.ID, arg.UserID)
// 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
}
@@ -252,21 +243,16 @@ func (q *Queries) GetGame(ctx context.Context, arg GetGameParams) (GetGameRow, e
const getPlatform = `-- name: GetPlatform :one
SELECT id, name
FROM platform
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL
WHERE id = $1 AND deleted_at IS NULL
`
type GetPlatformParams struct {
ID uuid.UUID
UserID uuid.UUID
}
type GetPlatformRow struct {
ID uuid.UUID
Name string
}
func (q *Queries) GetPlatform(ctx context.Context, arg GetPlatformParams) (GetPlatformRow, error) {
row := q.db.QueryRow(ctx, getPlatform, arg.ID, arg.UserID)
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
@@ -406,7 +392,7 @@ func (q *Queries) ListGames(ctx context.Context, userID uuid.UUID) ([]ListGamesR
const listPlatforms = `-- name: ListPlatforms :many
SELECT id, name
FROM platform
WHERE user_id = $1 AND deleted_at IS NULL
WHERE deleted_at IS NULL
ORDER BY name
`
@@ -415,8 +401,8 @@ type ListPlatformsRow struct {
Name string
}
func (q *Queries) ListPlatforms(ctx context.Context, userID uuid.UUID) ([]ListPlatformsRow, error) {
rows, err := q.db.Query(ctx, listPlatforms, userID)
func (q *Queries) ListPlatforms(ctx context.Context) ([]ListPlatformsRow, error) {
rows, err := q.db.Query(ctx, listPlatforms)
if err != nil {
return nil, err
}
@@ -504,15 +490,14 @@ func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) (UpdateG
const updatePlatform = `-- name: UpdatePlatform :one
UPDATE platform
SET name = $3
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL
SET name = $2
WHERE id = $1 AND deleted_at IS NULL
RETURNING id, name
`
type UpdatePlatformParams struct {
ID uuid.UUID
UserID uuid.UUID
Name string
ID uuid.UUID
Name string
}
type UpdatePlatformRow struct {
@@ -521,7 +506,7 @@ type UpdatePlatformRow struct {
}
func (q *Queries) UpdatePlatform(ctx context.Context, arg UpdatePlatformParams) (UpdatePlatformRow, error) {
row := q.db.QueryRow(ctx, updatePlatform, arg.ID, arg.UserID, arg.Name)
row := q.db.QueryRow(ctx, updatePlatform, arg.ID, arg.Name)
var i UpdatePlatformRow
err := row.Scan(&i.ID, &i.Name)
return i, err