Skip to content

Commit

Permalink
feat(game): Add get games endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Likqez committed Dec 10, 2024
1 parent 989d477 commit 0ab2ce5
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 17 deletions.
122 changes: 111 additions & 11 deletions DB/game.sql
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,18 @@ CREATE OR REPLACE FUNCTION init_game(
playlist_id_input TEXT,
player_ids UUID[],
song_data game_song_input[]
) RETURNS TABLE(game_id INTEGER, created_at TIMESTAMP WITH TIME ZONE) AS
)
RETURNS TABLE
(
game_id INTEGER,
created_at TIMESTAMP WITH TIME ZONE
)
AS
$$
DECLARE
new_game_id INTEGER;
new_game_id INTEGER;
new_created_at TIMESTAMP WITH TIME ZONE;
player_id UUID;
player_id UUID;
BEGIN
-- Create game
INSERT INTO games (playlist_id, status)
Expand All @@ -118,7 +124,8 @@ BEGIN

UPDATE game_players
SET is_creator = true
WHERE game_players.game_id = new_game_id AND game_players.user_id = player_ids[1];
WHERE game_players.game_id = new_game_id
AND game_players.user_id = player_ids[1];

-- Insert songs (assuming the songs already exist in the songs table)
INSERT INTO game_rounds (game_id,
Expand All @@ -127,17 +134,110 @@ BEGIN
wrong_option_1,
wrong_option_2,
wrong_option_3)
SELECT
new_game_id,
row_number() OVER (ORDER BY song.spotify_song_id), -- Added ORDER BY
song.spotify_song_id,
song.wrong_option_1,
song.wrong_option_2,
song.wrong_option_3
SELECT new_game_id,
row_number() OVER (ORDER BY song.spotify_song_id), -- Added ORDER BY
song.spotify_song_id,
song.wrong_option_1,
song.wrong_option_2,
song.wrong_option_3
FROM unnest(song_data) song;

RETURN QUERY
SELECT new_game_id AS game_id, new_created_at AS created_at;

END;
$$ LANGUAGE plpgsql;

-- Get all games for a user
CREATE OR REPLACE FUNCTION get_player_games(p_user_id UUID)
RETURNS TABLE
(
-- Game basic info
game_id INTEGER,
status game_status,
playlist_id TEXT,
playlist_cover TEXT,
playlist_name TEXT,
created_at TIMESTAMPTZ,
creator_id UUID,
-- Opponent info (renamed from player_ to opponent_)
opponent_user_id UUID,
opponent_avatar_url TEXT,
opponent_username TEXT,
opponent_spotify_id TEXT,
opponent_spotify_visibility BOOLEAN,
opponent_daily_streak INTEGER,
opponent_daily_streak_updated_at TIMESTAMPTZ,
-- Round info
round_number INTEGER,
correct_song_id TEXT,
correct_song_name TEXT,
correct_song_artist TEXT,
correct_song_preview_url TEXT,
wrong_song_1_id TEXT,
wrong_song_1_name TEXT,
wrong_song_1_artist TEXT,
wrong_song_1_preview_url TEXT,
wrong_song_2_id TEXT,
wrong_song_2_name TEXT,
wrong_song_2_artist TEXT,
wrong_song_2_preview_url TEXT,
wrong_song_3_id TEXT,
wrong_song_3_name TEXT,
wrong_song_3_artist TEXT,
wrong_song_3_preview_url TEXT
)
AS
$$
BEGIN
RETURN QUERY
SELECT g.game_id,
g.status,
g.playlist_id,
p.cover,
p.name,
g.created_at,
gp_creator.user_id,

-- Opponent information
u.id,
u.avatar_url,
u.username,
u.spotify_id,
u.spotify_visibility,
u.daily_streak,
u.daily_streak_updated_at,
-- Round information
gr.song_order,
cs.spotify_song_id,
cs.song_name,
cs.artist_name,
cs.preview_url,
w1.spotify_song_id,
w1.song_name,
w1.artist_name,
w1.preview_url,
w2.spotify_song_id,
w2.song_name,
w2.artist_name,
w2.preview_url,
w3.spotify_song_id,
w3.song_name,
w3.artist_name,
w3.preview_url
FROM games g
-- Join to get current user's games
JOIN game_players gp_current ON g.game_id = gp_current.game_id AND gp_current.user_id = p_user_id
-- Join to get opponent's info
JOIN game_players gp_opponent ON g.game_id = gp_opponent.game_id AND gp_opponent.user_id != p_user_id
LEFT JOIN game_players gp_creator ON g.game_id = gp_creator.game_id AND gp_creator.is_creator = true
JOIN users u ON gp_opponent.user_id = u.id
JOIN game_rounds gr ON g.game_id = gr.game_id
JOIN songs cs ON gr.song_id = cs.spotify_song_id
JOIN songs w1 ON gr.wrong_option_1 = w1.spotify_song_id
JOIN songs w2 ON gr.wrong_option_2 = w2.spotify_song_id
JOIN songs w3 ON gr.wrong_option_3 = w3.spotify_song_id
JOIN playlists p ON g.playlist_id = p.id
ORDER BY g.created_at DESC, gr.song_order ASC;
END;
$$ LANGUAGE plpgsql;
30 changes: 30 additions & 0 deletions server/api/v1/game/index.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {serverSupabaseServiceRole, serverSupabaseUser} from "#supabase/server";
import {mapDatabaseRowsToGames} from "~/server/utils/mapper/game-mapper";
import {GameStatus} from "~/types/api/game";

export default defineEventHandler(async (event) => {
// Require user to be authenticated
const user = await serverSupabaseUser(event);
if (!user?.id) {
setResponseStatus(event, 401);
return {error: 'unauthenticated'};
}

// Get user's games
const client = serverSupabaseServiceRole(event);
const {data, error} = await client.rpc('get_player_games', {p_user_id: user.id} as never).select();

if (error) {
setResponseStatus(event, 500);
return {error: error.message};
}

const games = mapDatabaseRowsToGames(data);

return {
active: games.filter(game => game.status === GameStatus.PLAYING && game.creator_id !== user.id),
waiting: games.filter(game => game.status === GameStatus.PLAYING && game.creator_id === user.id),
past: games.filter(game => game.status === GameStatus.FINISHED)
}

});
145 changes: 145 additions & 0 deletions server/utils/mapper/game-mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Types from your original interfaces

import type {Game, GameRound, Song} from "~/types/api/game";
import type {GetUserResponse} from "~/types/api/users";
import {GameStatus} from "~/types/api/game";

Check failure on line 5 in server/utils/mapper/game-mapper.ts

View workflow job for this annotation

GitHub Actions / eslint

All imports in the declaration are only used as types. Use `import type`

interface DatabaseGameRow {
// Game basic info
game_id: number;
status: string;
playlist_id: string;
playlist_cover: string;
playlist_name: string;
created_at: string;
creator_id: string; // who created the game and thus already started playing

// Opponent info (the other player's data)
opponent_user_id: string;
opponent_avatar_url: string | null;
opponent_username: string;
opponent_spotify_id: string | null;
opponent_spotify_visibility: boolean;
opponent_daily_streak: number | null;
opponent_daily_streak_updated_at: string | null;

// Round info (the song being played)
round_number: number; // From game_rounds table - the order of the song
correct_song_id: string;
correct_song_name: string;
correct_song_artist: string;
correct_song_preview_url: string | null;

// Wrong options (the 3 incorrect choices)
wrong_song_1_id: string;
wrong_song_1_name: string;
wrong_song_1_artist: string;
wrong_song_1_preview_url: string | null;

wrong_song_2_id: string; // Similar to wrong_song_1
wrong_song_2_name: string;
wrong_song_2_artist: string;
wrong_song_2_preview_url: string | null;

wrong_song_3_id: string; // Similar to wrong_song_1
wrong_song_3_name: string;
wrong_song_3_artist: string;
wrong_song_3_preview_url: string | null;
}

const createSongFromRow = (
id: string,
name: string,
artist: string,
previewUrl: string | null
): Song => ({
id,
name,
artists: [{
id: 'placeholder', // You might want to adjust this based on your needs
name: artist
}],
preview_url: previewUrl,
is_playable: previewUrl !== null
});


export const mapDatabaseRowsToGame = (rows: DatabaseGameRow[]): Game => {
if (rows.length === 0) {
throw new Error('No rows provided to map to Game');
}

const firstRow = rows[0];

// Map opponent info
const opponent: GetUserResponse = {
id: firstRow.opponent_user_id,
avatar_url: firstRow.opponent_avatar_url ?? undefined,
username: firstRow.opponent_username,
spotify_id: firstRow.opponent_spotify_id ?? undefined,
spotify_visibility: firstRow.opponent_spotify_visibility,
daily_streak: firstRow.opponent_daily_streak ?? undefined,
daily_streak_updated_at: firstRow.opponent_daily_streak_updated_at ?? undefined
};

// Map rounds
const rounds: GameRound[] = rows.map(row => ({
round: row.round_number,
correct_song: createSongFromRow(
row.correct_song_id,
row.correct_song_name,
row.correct_song_artist,
row.correct_song_preview_url
),
wrong_songs: [
createSongFromRow(
row.wrong_song_1_id,
row.wrong_song_1_name,
row.wrong_song_1_artist,
row.wrong_song_1_preview_url
),
createSongFromRow(
row.wrong_song_2_id,
row.wrong_song_2_name,
row.wrong_song_2_artist,
row.wrong_song_2_preview_url
),
createSongFromRow(
row.wrong_song_3_id,
row.wrong_song_3_name,
row.wrong_song_3_artist,
row.wrong_song_3_preview_url
)
]
}));

// Construct final game object with players instead of player_ids
return {
game_id: firstRow.game_id,
status: firstRow.status as GameStatus,
creator_id: firstRow.creator_id,
playlist: {
id: firstRow.playlist_id,
name: firstRow.playlist_name,
cover: firstRow.playlist_cover
},
opponents: [opponent],
songs: rounds,
created_at: firstRow.created_at
};
};

export const mapDatabaseRowsToGames = (rows: DatabaseGameRow[]): Game[] => {
if (rows.length === 0) return [];

// Group rows by game_id
const gameRows = new Map<number, DatabaseGameRow[]>();
rows.forEach(row => {
const currentRows = gameRows.get(row.game_id) ?? [];
gameRows.set(row.game_id, [...currentRows, row]);
});

// Map each group of rows to a Game
return Array.from(gameRows.values())
.map(gameRows => mapDatabaseRowsToGame(gameRows));
};
22 changes: 20 additions & 2 deletions types/api/game.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type {GetUserResponse} from "@/types/api/users";

export interface SpotifySong {
track: Song;
}
Expand All @@ -23,13 +25,24 @@ export interface GameRound {

export interface Game {
game_id: number;
playlist_id: string;
player_ids: string[];
status: GameStatus;
creator_id: string; // who created the game and thus already played its turn
playlist: {
id: string;
name: string;
cover?: string;
};
opponents: GetUserResponse[];
songs: GameRound[];
created_at: string;
stats?: GameStats[];
}

export enum GameStatus {
PLAYING = 'playing',
FINISHED = 'finished'
}

export interface GameStats {
user_id: string;
score: number;
Expand All @@ -55,4 +68,9 @@ export interface GameInitResponse {
export interface GameInitRequest {
playlist_id: string;
opponent_id: string;
}

export interface GetGameResponse {
active: Game[];
past: Game[];
}
7 changes: 3 additions & 4 deletions types/api/users.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type url from "node:url";

export interface GetUserResponse {
id: string,
avatar_url?: url.URL | null,
avatar_url?: string,
username: string,
spotify_id?: string,
spotify_visibility: boolean,
daily_streak: number,
daily_streak_updated_at: string
daily_streak?: number,
daily_streak_updated_at?: string
}

0 comments on commit 0ab2ce5

Please sign in to comment.