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

+4 -3
View File
@@ -28,17 +28,18 @@ CREATE TABLE sessions (
CREATE INDEX idx_sessions_user_id ON sessions(user_id); CREATE INDEX idx_sessions_user_id ON sessions(user_id);
-- Platforms are shared: every user picks from and adds to the same list,
-- unlike games, which are private per user_id.
CREATE TABLE platform ( CREATE TABLE platform (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name text NOT NULL, name text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz, deleted_at timestamptz,
change_seq bigint NOT NULL DEFAULT nextval('change_seq_seq'), change_seq bigint NOT NULL DEFAULT nextval('change_seq_seq'),
UNIQUE (user_id, name) UNIQUE (name)
); );
CREATE INDEX idx_platform_user_change_seq ON platform(user_id, change_seq); CREATE INDEX idx_platform_change_seq ON platform(change_seq);
CREATE TABLE game ( CREATE TABLE game (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+10 -9
View File
@@ -40,37 +40,38 @@ WHERE token_hash = $1;
-- name: CreatePlatform :one -- name: CreatePlatform :one
INSERT INTO platform ( INSERT INTO platform (
user_id, name name
) VALUES ( ) VALUES (
$1, $2 $1
) )
RETURNING id, name; RETURNING id, name;
-- name: GetPlatform :one -- name: GetPlatform :one
SELECT id, name SELECT id, name
FROM platform FROM platform
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL; WHERE id = $1 AND deleted_at IS NULL;
-- name: ListPlatforms :many -- name: ListPlatforms :many
SELECT id, name SELECT id, name
FROM platform FROM platform
WHERE user_id = $1 AND deleted_at IS NULL WHERE deleted_at IS NULL
ORDER BY name; ORDER BY name;
-- name: UpdatePlatform :one -- name: UpdatePlatform :one
UPDATE platform UPDATE platform
SET name = $3 SET name = $2
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL WHERE id = $1 AND deleted_at IS NULL
RETURNING id, name; RETURNING id, name;
-- name: DeletePlatform :execrows -- name: DeletePlatform :execrows
-- Soft delete, so the FK on game.platform_id is never touched and never -- 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 -- 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 -- that still has active games - from any user, since the list is shared -
-- tell "not found" apart from "blocked by games" when this affects 0 rows. -- from being deleted. Call GetPlatform first to tell "not found" apart from
-- "blocked by games" when this affects 0 rows.
UPDATE platform UPDATE platform
SET deleted_at = now() 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 ( AND NOT EXISTS (
SELECT 1 FROM game SELECT 1 FROM game
WHERE game.platform_id = platform.id AND game.deleted_at IS NULL WHERE game.platform_id = platform.id AND game.deleted_at IS NULL
+12 -12
View File
@@ -15,19 +15,19 @@ import (
// pointer deep in sqlc-generated code. // pointer deep in sqlc-generated code.
type MockQuerier struct { type MockQuerier struct {
CreateGameFunc func(ctx context.Context, arg db.CreateGameParams) (db.CreateGameRow, error) CreateGameFunc func(ctx context.Context, arg db.CreateGameParams) (db.CreateGameRow, error)
CreatePlatformFunc func(ctx context.Context, arg db.CreatePlatformParams) (db.CreatePlatformRow, error) CreatePlatformFunc func(ctx context.Context, name string) (db.CreatePlatformRow, error)
CreateSessionFunc func(ctx context.Context, arg db.CreateSessionParams) error CreateSessionFunc func(ctx context.Context, arg db.CreateSessionParams) error
CreateUserFunc func(ctx context.Context, arg db.CreateUserParams) (db.CreateUserRow, error) CreateUserFunc func(ctx context.Context, arg db.CreateUserParams) (db.CreateUserRow, error)
DeleteGameFunc func(ctx context.Context, arg db.DeleteGameParams) (int64, error) DeleteGameFunc func(ctx context.Context, arg db.DeleteGameParams) (int64, error)
DeletePlatformFunc func(ctx context.Context, arg db.DeletePlatformParams) (int64, error) DeletePlatformFunc func(ctx context.Context, id uuid.UUID) (int64, error)
DeleteSessionFunc func(ctx context.Context, tokenHash []byte) error DeleteSessionFunc func(ctx context.Context, tokenHash []byte) error
GetGameFunc func(ctx context.Context, arg db.GetGameParams) (db.GetGameRow, error) GetGameFunc func(ctx context.Context, arg db.GetGameParams) (db.GetGameRow, error)
GetPlatformFunc func(ctx context.Context, arg db.GetPlatformParams) (db.GetPlatformRow, error) GetPlatformFunc func(ctx context.Context, id uuid.UUID) (db.GetPlatformRow, error)
GetSessionAndUserFunc func(ctx context.Context, tokenHash []byte) (db.GetSessionAndUserRow, error) GetSessionAndUserFunc func(ctx context.Context, tokenHash []byte) (db.GetSessionAndUserRow, error)
GetUserByEmailFunc func(ctx context.Context, email string) (db.GetUserByEmailRow, error) GetUserByEmailFunc func(ctx context.Context, email string) (db.GetUserByEmailRow, error)
GetUserByIDFunc func(ctx context.Context, id uuid.UUID) (db.GetUserByIDRow, error) GetUserByIDFunc func(ctx context.Context, id uuid.UUID) (db.GetUserByIDRow, error)
ListGamesFunc func(ctx context.Context, userID uuid.UUID) ([]db.ListGamesRow, error) ListGamesFunc func(ctx context.Context, userID uuid.UUID) ([]db.ListGamesRow, error)
ListPlatformsFunc func(ctx context.Context, userID uuid.UUID) ([]db.ListPlatformsRow, error) ListPlatformsFunc func(ctx context.Context) ([]db.ListPlatformsRow, error)
TouchSessionFunc func(ctx context.Context, tokenHash []byte) error TouchSessionFunc func(ctx context.Context, tokenHash []byte) error
UpdateGameFunc func(ctx context.Context, arg db.UpdateGameParams) (db.UpdateGameRow, error) UpdateGameFunc func(ctx context.Context, arg db.UpdateGameParams) (db.UpdateGameRow, error)
UpdatePlatformFunc func(ctx context.Context, arg db.UpdatePlatformParams) (db.UpdatePlatformRow, error) UpdatePlatformFunc func(ctx context.Context, arg db.UpdatePlatformParams) (db.UpdatePlatformRow, error)
@@ -36,8 +36,8 @@ type MockQuerier struct {
func (m *MockQuerier) CreateGame(ctx context.Context, arg db.CreateGameParams) (db.CreateGameRow, error) { func (m *MockQuerier) CreateGame(ctx context.Context, arg db.CreateGameParams) (db.CreateGameRow, error) {
return m.CreateGameFunc(ctx, arg) return m.CreateGameFunc(ctx, arg)
} }
func (m *MockQuerier) CreatePlatform(ctx context.Context, arg db.CreatePlatformParams) (db.CreatePlatformRow, error) { func (m *MockQuerier) CreatePlatform(ctx context.Context, name string) (db.CreatePlatformRow, error) {
return m.CreatePlatformFunc(ctx, arg) return m.CreatePlatformFunc(ctx, name)
} }
func (m *MockQuerier) CreateSession(ctx context.Context, arg db.CreateSessionParams) error { func (m *MockQuerier) CreateSession(ctx context.Context, arg db.CreateSessionParams) error {
return m.CreateSessionFunc(ctx, arg) return m.CreateSessionFunc(ctx, arg)
@@ -48,8 +48,8 @@ func (m *MockQuerier) CreateUser(ctx context.Context, arg db.CreateUserParams) (
func (m *MockQuerier) DeleteGame(ctx context.Context, arg db.DeleteGameParams) (int64, error) { func (m *MockQuerier) DeleteGame(ctx context.Context, arg db.DeleteGameParams) (int64, error) {
return m.DeleteGameFunc(ctx, arg) return m.DeleteGameFunc(ctx, arg)
} }
func (m *MockQuerier) DeletePlatform(ctx context.Context, arg db.DeletePlatformParams) (int64, error) { func (m *MockQuerier) DeletePlatform(ctx context.Context, id uuid.UUID) (int64, error) {
return m.DeletePlatformFunc(ctx, arg) return m.DeletePlatformFunc(ctx, id)
} }
func (m *MockQuerier) DeleteSession(ctx context.Context, tokenHash []byte) error { func (m *MockQuerier) DeleteSession(ctx context.Context, tokenHash []byte) error {
return m.DeleteSessionFunc(ctx, tokenHash) return m.DeleteSessionFunc(ctx, tokenHash)
@@ -57,8 +57,8 @@ func (m *MockQuerier) DeleteSession(ctx context.Context, tokenHash []byte) error
func (m *MockQuerier) GetGame(ctx context.Context, arg db.GetGameParams) (db.GetGameRow, error) { func (m *MockQuerier) GetGame(ctx context.Context, arg db.GetGameParams) (db.GetGameRow, error) {
return m.GetGameFunc(ctx, arg) return m.GetGameFunc(ctx, arg)
} }
func (m *MockQuerier) GetPlatform(ctx context.Context, arg db.GetPlatformParams) (db.GetPlatformRow, error) { func (m *MockQuerier) GetPlatform(ctx context.Context, id uuid.UUID) (db.GetPlatformRow, error) {
return m.GetPlatformFunc(ctx, arg) return m.GetPlatformFunc(ctx, id)
} }
func (m *MockQuerier) GetSessionAndUser(ctx context.Context, tokenHash []byte) (db.GetSessionAndUserRow, error) { func (m *MockQuerier) GetSessionAndUser(ctx context.Context, tokenHash []byte) (db.GetSessionAndUserRow, error) {
return m.GetSessionAndUserFunc(ctx, tokenHash) return m.GetSessionAndUserFunc(ctx, tokenHash)
@@ -72,8 +72,8 @@ func (m *MockQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (db.GetUser
func (m *MockQuerier) ListGames(ctx context.Context, userID uuid.UUID) ([]db.ListGamesRow, error) { func (m *MockQuerier) ListGames(ctx context.Context, userID uuid.UUID) ([]db.ListGamesRow, error) {
return m.ListGamesFunc(ctx, userID) return m.ListGamesFunc(ctx, userID)
} }
func (m *MockQuerier) ListPlatforms(ctx context.Context, userID uuid.UUID) ([]db.ListPlatformsRow, error) { func (m *MockQuerier) ListPlatforms(ctx context.Context) ([]db.ListPlatformsRow, error) {
return m.ListPlatformsFunc(ctx, userID) return m.ListPlatformsFunc(ctx)
} }
func (m *MockQuerier) TouchSession(ctx context.Context, tokenHash []byte) error { func (m *MockQuerier) TouchSession(ctx context.Context, tokenHash []byte) error {
return m.TouchSessionFunc(ctx, tokenHash) return m.TouchSessionFunc(ctx, tokenHash)
+12 -12
View File
@@ -41,6 +41,11 @@ func RegisterRoutes(api huma.API, queries db.Querier) {
} }
// --- Platform Routes --- // --- 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) { func registerPlatformRoutes(api huma.API, queries db.Querier) {
listOp := huma.Operation{ listOp := huma.Operation{
@@ -51,7 +56,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
} }
addAuth(&listOp, api, queries) addAuth(&listOp, api, queries)
huma.Register(api, listOp, func(ctx context.Context, input *struct{}) (*struct{ Body []model.Platform }, error) { 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 { if err != nil {
return nil, dbError(err) 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 { huma.Register(api, getOp, func(ctx context.Context, input *struct {
ID uuid.UUID `path:"id"` ID uuid.UUID `path:"id"`
}) (*struct{ Body model.Platform }, error) { }) (*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 { if err != nil {
return nil, dbError(err) return nil, dbError(err)
} }
@@ -87,10 +92,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
} }
addAuth(&createOp, api, queries) addAuth(&createOp, api, queries)
huma.Register(api, createOp, func(ctx context.Context, input *struct{ Body model.PlatformInput }) (*struct{ Body model.Platform }, error) { 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{ platform, err := queries.CreatePlatform(ctx, input.Body.Name)
UserID: userFromContext(ctx).ID,
Name: input.Body.Name,
})
if err != nil { if err != nil {
return nil, dbError(err) return nil, dbError(err)
} }
@@ -109,9 +111,8 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
Body model.PlatformInput Body model.PlatformInput
}) (*struct{ Body model.Platform }, error) { }) (*struct{ Body model.Platform }, error) {
platform, err := queries.UpdatePlatform(ctx, db.UpdatePlatformParams{ platform, err := queries.UpdatePlatform(ctx, db.UpdatePlatformParams{
ID: input.ID, ID: input.ID,
UserID: userFromContext(ctx).ID, Name: input.Body.Name,
Name: input.Body.Name,
}) })
if err != nil { if err != nil {
return nil, dbError(err) 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 { huma.Register(api, deleteOp, func(ctx context.Context, input *struct {
ID uuid.UUID `path:"id"` ID uuid.UUID `path:"id"`
}) (*struct{}, error) { }) (*struct{}, error) {
userID := userFromContext(ctx).ID
// Checked separately so a 0-rows result from DeletePlatform below can // Checked separately so a 0-rows result from DeletePlatform below can
// only mean "blocked by games referencing it", not "doesn't exist" - // only mean "blocked by games referencing it", not "doesn't exist" -
// soft delete means the FK alone can't tell the two apart (see // 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 platform is deleted between these two queries; harmless, since
// the second query would then just affect 0 rows and report 409 // the second query would then just affect 0 rows and report 409
// instead of the 404 a plain retry would hit anyway. // 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) 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 { if err != nil {
return nil, dbError(err) return nil, dbError(err)
} }
+27 -7
View File
@@ -119,10 +119,10 @@ func TestDeletePlatformConflictWhenReferencedByGames(t *testing.T) {
// the 0-rows result itself and turn it into 409, not just pass through // the 0-rows result itself and turn it into 409, not just pass through
// whatever error (if any) the query returns. // whatever error (if any) the query returns.
mockDB := authedMock() mockDB := authedMock()
mockDB.GetPlatformFunc = func(ctx context.Context, arg db.GetPlatformParams) (db.GetPlatformRow, error) { mockDB.GetPlatformFunc = func(ctx context.Context, id uuid.UUID) (db.GetPlatformRow, error) {
return db.GetPlatformRow{ID: arg.ID, Name: "PC"}, nil return db.GetPlatformRow{ID: id, Name: "PC"}, nil
} }
mockDB.DeletePlatformFunc = func(ctx context.Context, arg db.DeletePlatformParams) (int64, error) { mockDB.DeletePlatformFunc = func(ctx context.Context, id uuid.UUID) (int64, error) {
return 0, nil return 0, nil
} }
@@ -135,7 +135,7 @@ func TestDeletePlatformConflictWhenReferencedByGames(t *testing.T) {
func TestDeletePlatformNotFound(t *testing.T) { func TestDeletePlatformNotFound(t *testing.T) {
mockDB := authedMock() mockDB := authedMock()
mockDB.GetPlatformFunc = func(ctx context.Context, arg db.GetPlatformParams) (db.GetPlatformRow, error) { mockDB.GetPlatformFunc = func(ctx context.Context, id uuid.UUID) (db.GetPlatformRow, error) {
return db.GetPlatformRow{}, pgx.ErrNoRows return db.GetPlatformRow{}, pgx.ErrNoRows
} }
@@ -161,9 +161,8 @@ func TestCreateGameRejectsScoreOutOfRange(t *testing.T) {
func TestCreatePlatform(t *testing.T) { func TestCreatePlatform(t *testing.T) {
mockDB := authedMock() mockDB := authedMock()
mockDB.CreatePlatformFunc = func(ctx context.Context, arg db.CreatePlatformParams) (db.CreatePlatformRow, error) { mockDB.CreatePlatformFunc = func(ctx context.Context, name string) (db.CreatePlatformRow, error) {
assert.Equal(t, testUserID, arg.UserID) return db.CreatePlatformRow{ID: uuid.New(), Name: name}, nil
return db.CreatePlatformRow{ID: uuid.New(), Name: arg.Name}, nil
} }
e, _ := newTestAPI(mockDB) e, _ := newTestAPI(mockDB)
@@ -174,6 +173,27 @@ func TestCreatePlatform(t *testing.T) {
assert.Contains(t, rec.Body.String(), "Sega Genesis") assert.Contains(t, rec.Body.String(), "Sega Genesis")
} }
func TestListPlatformsIsSharedAcrossUsers(t *testing.T) {
// Unlike games, the platform list has no user_id to scope by: every
// authenticated user sees the same rows. ListPlatformsFunc taking no
// user id (beyond ctx) is itself part of what's being checked here -
// this test would fail to compile if a user_id parameter crept back in.
calls := 0
mockDB := authedMock()
mockDB.ListPlatformsFunc = func(ctx context.Context) ([]db.ListPlatformsRow, error) {
calls++
return []db.ListPlatformsRow{{ID: uuid.New(), Name: "PC"}}, nil
}
e, _ := newTestAPI(mockDB)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, authedRequest(http.MethodGet, "/api/platforms", ""))
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "PC")
assert.Equal(t, 1, calls)
}
func TestRegisterDuplicateEmailConflicts(t *testing.T) { func TestRegisterDuplicateEmailConflicts(t *testing.T) {
mockDB := &MockQuerier{ mockDB := &MockQuerier{
CreateUserFunc: func(ctx context.Context, arg db.CreateUserParams) (db.CreateUserRow, error) { CreateUserFunc: func(ctx context.Context, arg db.CreateUserParams) (db.CreateUserRow, error) {
-1
View File
@@ -24,7 +24,6 @@ type Game struct {
type Platform struct { type Platform struct {
ID uuid.UUID ID uuid.UUID
UserID uuid.UUID
Name string Name string
UpdatedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz
DeletedAt pgtype.Timestamptz DeletedAt pgtype.Timestamptz
+7 -6
View File
@@ -12,23 +12,24 @@ import (
type Querier interface { type Querier interface {
CreateGame(ctx context.Context, arg CreateGameParams) (CreateGameRow, error) CreateGame(ctx context.Context, arg CreateGameParams) (CreateGameRow, error)
CreatePlatform(ctx context.Context, arg CreatePlatformParams) (CreatePlatformRow, error) CreatePlatform(ctx context.Context, name string) (CreatePlatformRow, error)
CreateSession(ctx context.Context, arg CreateSessionParams) error CreateSession(ctx context.Context, arg CreateSessionParams) error
CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error)
DeleteGame(ctx context.Context, arg DeleteGameParams) (int64, error) DeleteGame(ctx context.Context, arg DeleteGameParams) (int64, error)
// Soft delete, so the FK on game.platform_id is never touched and never // 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 // 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 // that still has active games - from any user, since the list is shared -
// tell "not found" apart from "blocked by games" when this affects 0 rows. // from being deleted. Call GetPlatform first to tell "not found" apart from
DeletePlatform(ctx context.Context, arg DeletePlatformParams) (int64, error) // "blocked by games" when this affects 0 rows.
DeletePlatform(ctx context.Context, id uuid.UUID) (int64, error)
DeleteSession(ctx context.Context, tokenHash []byte) error DeleteSession(ctx context.Context, tokenHash []byte) error
GetGame(ctx context.Context, arg GetGameParams) (GetGameRow, error) GetGame(ctx context.Context, arg GetGameParams) (GetGameRow, error)
GetPlatform(ctx context.Context, arg GetPlatformParams) (GetPlatformRow, error) GetPlatform(ctx context.Context, id uuid.UUID) (GetPlatformRow, error)
GetSessionAndUser(ctx context.Context, tokenHash []byte) (GetSessionAndUserRow, error) GetSessionAndUser(ctx context.Context, tokenHash []byte) (GetSessionAndUserRow, error)
GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error) GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error)
GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, error) GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, error)
ListGames(ctx context.Context, userID uuid.UUID) ([]ListGamesRow, error) ListGames(ctx context.Context, userID uuid.UUID) ([]ListGamesRow, error)
ListPlatforms(ctx context.Context, userID uuid.UUID) ([]ListPlatformsRow, error) ListPlatforms(ctx context.Context) ([]ListPlatformsRow, error)
TouchSession(ctx context.Context, tokenHash []byte) error TouchSession(ctx context.Context, tokenHash []byte) error
UpdateGame(ctx context.Context, arg UpdateGameParams) (UpdateGameRow, error) UpdateGame(ctx context.Context, arg UpdateGameParams) (UpdateGameRow, error)
UpdatePlatform(ctx context.Context, arg UpdatePlatformParams) (UpdatePlatformRow, error) UpdatePlatform(ctx context.Context, arg UpdatePlatformParams) (UpdatePlatformRow, error)
+21 -36
View File
@@ -70,25 +70,20 @@ func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (CreateG
const createPlatform = `-- name: CreatePlatform :one const createPlatform = `-- name: CreatePlatform :one
INSERT INTO platform ( INSERT INTO platform (
user_id, name name
) VALUES ( ) VALUES (
$1, $2 $1
) )
RETURNING id, name RETURNING id, name
` `
type CreatePlatformParams struct {
UserID uuid.UUID
Name string
}
type CreatePlatformRow struct { type CreatePlatformRow struct {
ID uuid.UUID ID uuid.UUID
Name string Name string
} }
func (q *Queries) CreatePlatform(ctx context.Context, arg CreatePlatformParams) (CreatePlatformRow, error) { func (q *Queries) CreatePlatform(ctx context.Context, name string) (CreatePlatformRow, error) {
row := q.db.QueryRow(ctx, createPlatform, arg.UserID, arg.Name) row := q.db.QueryRow(ctx, createPlatform, name)
var i CreatePlatformRow var i CreatePlatformRow
err := row.Scan(&i.ID, &i.Name) err := row.Scan(&i.ID, &i.Name)
return i, err return i, err
@@ -177,24 +172,20 @@ func (q *Queries) DeleteGame(ctx context.Context, arg DeleteGameParams) (int64,
const deletePlatform = `-- name: DeletePlatform :execrows const deletePlatform = `-- name: DeletePlatform :execrows
UPDATE platform UPDATE platform
SET deleted_at = now() 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 ( AND NOT EXISTS (
SELECT 1 FROM game SELECT 1 FROM game
WHERE game.platform_id = platform.id AND game.deleted_at IS NULL 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 // 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 // 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 // that still has active games - from any user, since the list is shared -
// tell "not found" apart from "blocked by games" when this affects 0 rows. // from being deleted. Call GetPlatform first to tell "not found" apart from
func (q *Queries) DeletePlatform(ctx context.Context, arg DeletePlatformParams) (int64, error) { // "blocked by games" when this affects 0 rows.
result, err := q.db.Exec(ctx, deletePlatform, arg.ID, arg.UserID) func (q *Queries) DeletePlatform(ctx context.Context, id uuid.UUID) (int64, error) {
result, err := q.db.Exec(ctx, deletePlatform, id)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -252,21 +243,16 @@ func (q *Queries) GetGame(ctx context.Context, arg GetGameParams) (GetGameRow, e
const getPlatform = `-- name: GetPlatform :one const getPlatform = `-- name: GetPlatform :one
SELECT id, name SELECT id, name
FROM platform 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 { type GetPlatformRow struct {
ID uuid.UUID ID uuid.UUID
Name string Name string
} }
func (q *Queries) GetPlatform(ctx context.Context, arg GetPlatformParams) (GetPlatformRow, error) { func (q *Queries) GetPlatform(ctx context.Context, id uuid.UUID) (GetPlatformRow, error) {
row := q.db.QueryRow(ctx, getPlatform, arg.ID, arg.UserID) row := q.db.QueryRow(ctx, getPlatform, id)
var i GetPlatformRow var i GetPlatformRow
err := row.Scan(&i.ID, &i.Name) err := row.Scan(&i.ID, &i.Name)
return i, err return i, err
@@ -406,7 +392,7 @@ func (q *Queries) ListGames(ctx context.Context, userID uuid.UUID) ([]ListGamesR
const listPlatforms = `-- name: ListPlatforms :many const listPlatforms = `-- name: ListPlatforms :many
SELECT id, name SELECT id, name
FROM platform FROM platform
WHERE user_id = $1 AND deleted_at IS NULL WHERE deleted_at IS NULL
ORDER BY name ORDER BY name
` `
@@ -415,8 +401,8 @@ type ListPlatformsRow struct {
Name string Name string
} }
func (q *Queries) ListPlatforms(ctx context.Context, userID uuid.UUID) ([]ListPlatformsRow, error) { func (q *Queries) ListPlatforms(ctx context.Context) ([]ListPlatformsRow, error) {
rows, err := q.db.Query(ctx, listPlatforms, userID) rows, err := q.db.Query(ctx, listPlatforms)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -504,15 +490,14 @@ func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) (UpdateG
const updatePlatform = `-- name: UpdatePlatform :one const updatePlatform = `-- name: UpdatePlatform :one
UPDATE platform UPDATE platform
SET name = $3 SET name = $2
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL WHERE id = $1 AND deleted_at IS NULL
RETURNING id, name RETURNING id, name
` `
type UpdatePlatformParams struct { type UpdatePlatformParams struct {
ID uuid.UUID ID uuid.UUID
UserID uuid.UUID Name string
Name string
} }
type UpdatePlatformRow struct { type UpdatePlatformRow struct {
@@ -521,7 +506,7 @@ type UpdatePlatformRow struct {
} }
func (q *Queries) UpdatePlatform(ctx context.Context, arg UpdatePlatformParams) (UpdatePlatformRow, error) { 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 var i UpdatePlatformRow
err := row.Scan(&i.ID, &i.Name) err := row.Scan(&i.ID, &i.Name)
return i, err return i, err