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

+12 -12
View File
@@ -41,6 +41,11 @@ func RegisterRoutes(api huma.API, queries db.Querier) {
}
// --- Platform Routes ---
//
// Platforms are a shared list: every authenticated user reads and writes
// the same rows, unlike games, which are private per user. Routes still
// require auth (so the list isn't public), but none of the queries below
// filter by user_id, because platform has no such column.
func registerPlatformRoutes(api huma.API, queries db.Querier) {
listOp := huma.Operation{
@@ -51,7 +56,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
}
addAuth(&listOp, api, queries)
huma.Register(api, listOp, func(ctx context.Context, input *struct{}) (*struct{ Body []model.Platform }, error) {
platforms, err := queries.ListPlatforms(ctx, userFromContext(ctx).ID)
platforms, err := queries.ListPlatforms(ctx)
if err != nil {
return nil, dbError(err)
}
@@ -72,7 +77,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
huma.Register(api, getOp, func(ctx context.Context, input *struct {
ID uuid.UUID `path:"id"`
}) (*struct{ Body model.Platform }, error) {
p, err := queries.GetPlatform(ctx, db.GetPlatformParams{ID: input.ID, UserID: userFromContext(ctx).ID})
p, err := queries.GetPlatform(ctx, input.ID)
if err != nil {
return nil, dbError(err)
}
@@ -87,10 +92,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
}
addAuth(&createOp, api, queries)
huma.Register(api, createOp, func(ctx context.Context, input *struct{ Body model.PlatformInput }) (*struct{ Body model.Platform }, error) {
platform, err := queries.CreatePlatform(ctx, db.CreatePlatformParams{
UserID: userFromContext(ctx).ID,
Name: input.Body.Name,
})
platform, err := queries.CreatePlatform(ctx, input.Body.Name)
if err != nil {
return nil, dbError(err)
}
@@ -109,9 +111,8 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
Body model.PlatformInput
}) (*struct{ Body model.Platform }, error) {
platform, err := queries.UpdatePlatform(ctx, db.UpdatePlatformParams{
ID: input.ID,
UserID: userFromContext(ctx).ID,
Name: input.Body.Name,
ID: input.ID,
Name: input.Body.Name,
})
if err != nil {
return nil, dbError(err)
@@ -129,7 +130,6 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
huma.Register(api, deleteOp, func(ctx context.Context, input *struct {
ID uuid.UUID `path:"id"`
}) (*struct{}, error) {
userID := userFromContext(ctx).ID
// Checked separately so a 0-rows result from DeletePlatform below can
// only mean "blocked by games referencing it", not "doesn't exist" -
// soft delete means the FK alone can't tell the two apart (see
@@ -137,11 +137,11 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
// the platform is deleted between these two queries; harmless, since
// the second query would then just affect 0 rows and report 409
// instead of the 404 a plain retry would hit anyway.
if _, err := queries.GetPlatform(ctx, db.GetPlatformParams{ID: input.ID, UserID: userID}); err != nil {
if _, err := queries.GetPlatform(ctx, input.ID); err != nil {
return nil, dbError(err)
}
rows, err := queries.DeletePlatform(ctx, db.DeletePlatformParams{ID: input.ID, UserID: userID})
rows, err := queries.DeletePlatform(ctx, input.ID)
if err != nil {
return nil, dbError(err)
}