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:
1 parent
5072919b61
commit
7fbd403d30
8 files changed
+93
-86
No files matched your search
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user