better documentation

This commit is contained in:
2025-09-11 20:13:45 +02:00
parent dadc36785e
commit 8ba34f78a9
5 changed files with 310 additions and 175 deletions

View File

@@ -12,6 +12,8 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"express": "^5.1.0", "express": "^5.1.0",
"joi": "^18.0.1",
"joi-to-swagger": "^6.2.0",
"mysql2": "^3.14.5", "mysql2": "^3.14.5",
"swagger-jsdoc": "^6.2.8", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1" "swagger-ui-express": "^5.0.1"
@@ -64,6 +66,54 @@
"openapi-types": ">=7" "openapi-types": ">=7"
} }
}, },
"node_modules/@hapi/address": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
"integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^11.0.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@hapi/formula": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz",
"integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/hoek": {
"version": "11.0.7",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz",
"integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/pinpoint": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz",
"integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/tlds": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.3.tgz",
"integrity": "sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@hapi/topo": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz",
"integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^11.0.2"
}
},
"node_modules/@jsdevtools/ono": { "node_modules/@jsdevtools/ono": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
@@ -77,6 +127,12 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -855,6 +911,39 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/joi": {
"version": "18.0.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz",
"integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/address": "^5.1.1",
"@hapi/formula": "^3.0.2",
"@hapi/hoek": "^11.0.7",
"@hapi/pinpoint": "^2.0.1",
"@hapi/tlds": "^1.1.1",
"@hapi/topo": "^6.0.2",
"@standard-schema/spec": "^1.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/joi-to-swagger": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/joi-to-swagger/-/joi-to-swagger-6.2.0.tgz",
"integrity": "sha512-gwfIr1TsbbvZWozB/sFqiD7POFcXeaLKp6QJKGFkVgdom2ie/4f75QQAanZc/Wlbnyk66e6kTZXO28i6pN3oQA==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.21"
},
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"joi": ">=17.1.1"
}
},
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -867,6 +956,12 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.get": { "node_modules/lodash.get": {
"version": "4.4.2", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",

View File

@@ -13,6 +13,8 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"express": "^5.1.0", "express": "^5.1.0",
"joi": "^18.0.1",
"joi-to-swagger": "^6.2.0",
"mysql2": "^3.14.5", "mysql2": "^3.14.5",
"swagger-jsdoc": "^6.2.8", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1" "swagger-ui-express": "^5.0.1"

View File

@@ -1,7 +1,69 @@
import { Router } from "express"; import { Router } from "express";
import { pool } from './db.js'; import { pool } from './db.js';
import Joi from 'joi';
import j2s from "joi-to-swagger";
export class ApiError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
const router = Router(); const router = Router();
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
const userSchema = Joi.object({
username: Joi.string().min(3).max(32).required()
});
const createGameSchema = Joi.object({
user: Joi.number().integer().positive().required(),
is_local: Joi.boolean().default(false)
});
const addPlayerSchema = Joi.object({
user: Joi.number().integer().positive().required()
});
const lockGameSchema = Joi.object({
user: Joi.number().integer().positive().required()
});
const turnSchema = Joi.object({
user: Joi.number().integer().positive().required(),
round_number: Joi.number().integer().min(1).required(),
start_points: Joi.number().integer().min(0).required(),
first_throw: Joi.number().integer().min(0).required(),
second_throw: Joi.number().integer().min(0).required(),
third_throw: Joi.number().integer().min(0).required(),
end_points: Joi.number().integer().min(0).required()
});
const schemasByPath = {
"/api/users": userSchema,
"/api/games": createGameSchema,
"/api/games/:id/players": addPlayerSchema,
"/api/games/:id/lock": lockGameSchema,
"/api/games/:id/turns": turnSchema
};
export function generateRequestBodies() {
const requestBodies = {};
for (const path in schemasByPath) {
const { swagger } = j2s(schemasByPath[path]);
requestBodies[path] = {
content: {
"application/json": {
schema: swagger
}
}
};
}
return requestBodies;
}
/** /**
* @swagger * @swagger
@@ -24,30 +86,23 @@ router.get("/healthcheck", (req, res) => {
* @swagger * @swagger
* /api/users: * /api/users:
* post: * post:
* summary: Create a new user or return old user if username already exists * summary: Create a new user or return existing user if username already exists
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* username:
* type: string
* example:
* username: "john_doe"
* responses: * responses:
* 201: * 200:
* description: User created * description: User created or existing user returned
* content: * content:
* application/json: * application/json:
* example: * example:
* id: 1 * id: 1
* username: "john_doe" * username: "john_doe"
* 400:
* description: Validation error
*/ */
router.post("/users", async (req, res) => { router.post("/users", asyncHandler(async (req, res) => {
const { username} = req.body; const { error, value } = userSchema.validate(req.body);
if (error) throw new ApiError(400, error.details[0].message);
const { username } = value;
const [result] = await pool.query( const [result] = await pool.query(
`INSERT INTO users (username) `INSERT INTO users (username)
VALUES (?) VALUES (?)
@@ -57,7 +112,7 @@ router.post("/users", async (req, res) => {
const [userRow] = await pool.query("SELECT * FROM users WHERE id = ?", [result.insertId]); const [userRow] = await pool.query("SELECT * FROM users WHERE id = ?", [result.insertId]);
res.status(200).json(userRow[0]); res.status(200).json(userRow[0]);
}); }));
/** /**
* @swagger * @swagger
@@ -104,31 +159,20 @@ router.get("/games", async (req, res) => {
* current_playing_user: 1 * current_playing_user: 1
* turn_order: null * turn_order: null
*/ */
router.get("/games/:id", async (req, res) => { router.get("/games/:id", asyncHandler(async (req, res) => {
const [rows] = await pool.query("SELECT * FROM current_games WHERE id = ?", [req.params.id]); const gameId = parseInt(req.params.id, 10);
res.json(rows[0] || null); if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
});
const [rows] = await pool.query("SELECT * FROM current_games WHERE id = ?", [gameId]);
if (!rows.length) throw new ApiError(404, "Game not found");
res.json(rows[0]);
}));
/** /**
* @swagger * @swagger
* /api/games: * /api/games:
* post: * post:
* summary: Create a new game * summary: Create a new game
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* user:
* type: integer
* is_local:
* type: boolean
* default: false
* example:
* user: 1
* is_local: false
* responses: * responses:
* 201: * 201:
* description: Game created * description: Game created
@@ -145,13 +189,15 @@ router.get("/games/:id", async (req, res) => {
* - id: 1 * - id: 1
* username: "john_doe" * username: "john_doe"
*/ */
router.post("/games", async (req, res) => { router.post("/games", asyncHandler(async (req, res) => {
const { user, is_local = false } = req.body; const { error, value } = createGameSchema.validate(req.body);
if (error) throw new ApiError(400, error.details[0].message);
const { user, is_local } = value;
const conn = await pool.getConnection(); const conn = await pool.getConnection();
try { try {
await conn.beginTransaction(); await conn.beginTransaction();
const [gameResult] = await conn.query( const [gameResult] = await conn.query(
"INSERT INTO current_games (is_open, is_local, current_playing_user) VALUES (?, ?, ?)", "INSERT INTO current_games (is_open, is_local, current_playing_user) VALUES (?, ?, ?)",
[true, is_local, user] [true, is_local, user]
@@ -159,7 +205,10 @@ router.post("/games", async (req, res) => {
const gameId = gameResult.insertId; const gameId = gameResult.insertId;
await conn.query("INSERT INTO game_players (game, user, is_creator) VALUES (?, ?, ?)", [gameId, user, true]); await conn.query(
"INSERT INTO game_players (game, user, is_creator) VALUES (?, ?, ?)",
[gameId, user, true]
);
const [gameRow] = await conn.query("SELECT * FROM current_games WHERE id = ?", [gameId]); const [gameRow] = await conn.query("SELECT * FROM current_games WHERE id = ?", [gameId]);
const [players] = await conn.query( const [players] = await conn.query(
@@ -173,11 +222,11 @@ router.post("/games", async (req, res) => {
res.status(201).json({ game: gameRow[0], players }); res.status(201).json({ game: gameRow[0], players });
} catch (err) { } catch (err) {
await conn.rollback(); await conn.rollback();
throw err; throw new ApiError(500, "Failed to create game");
} finally { } finally {
conn.release(); conn.release();
} }
}); }));
/** /**
* @swagger * @swagger
@@ -190,17 +239,6 @@ router.post("/games", async (req, res) => {
* required: true * required: true
* schema: * schema:
* type: integer * type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* user:
* type: integer
* example:
* user: 2
* responses: * responses:
* 201: * 201:
* description: Player added * description: Player added
@@ -214,22 +252,24 @@ router.post("/games", async (req, res) => {
* - id: 2 * - id: 2
* username: "jane_doe" * username: "jane_doe"
*/ */
router.post("/games/:id/players", async (req, res) => { router.post("/games/:id/players", asyncHandler(async (req, res) => {
const { user } = req.body; const gameId = parseInt(req.params.id, 10);
const gameId = req.params.id; if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
const { error, value } = addPlayerSchema.validate(req.body);
if (error) throw new ApiError(400, error.details[0].message);
const { user } = value;
const conn = await pool.getConnection(); const conn = await pool.getConnection();
try { try {
await conn.beginTransaction(); await conn.beginTransaction();
const [gameRows] = await conn.query( const [gameRows] = await conn.query(
"SELECT * FROM current_games WHERE id = ? AND is_open = TRUE AND is_local = FALSE", "SELECT * FROM current_games WHERE id = ? AND is_open = TRUE AND is_local = FALSE",
[gameId] [gameId]
); );
if (gameRows.length === 0) { if (!gameRows.length) throw new ApiError(400, "Game is not open or is local");
return res.status(400).json({ error: "Game is not open or is local" });
}
await conn.query( await conn.query(
"INSERT IGNORE INTO game_players (game, user, is_creator) VALUES (?, ?, ?)", "INSERT IGNORE INTO game_players (game, user, is_creator) VALUES (?, ?, ?)",
@@ -247,11 +287,11 @@ router.post("/games/:id/players", async (req, res) => {
res.status(201).json({ game: gameId, players }); res.status(201).json({ game: gameId, players });
} catch (err) { } catch (err) {
await conn.rollback(); await conn.rollback();
throw err; throw new ApiError(500, "Failed to add player");
} finally { } finally {
conn.release(); conn.release();
} }
}); }));
/** /**
* @swagger * @swagger
@@ -264,17 +304,6 @@ router.post("/games/:id/players", async (req, res) => {
* required: true * required: true
* schema: * schema:
* type: integer * type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* user:
* type: integer
* example:
* user: 1
* responses: * responses:
* 200: * 200:
* description: Game locked with turn order * description: Game locked with turn order
@@ -283,26 +312,35 @@ router.post("/games/:id/players", async (req, res) => {
* example: * example:
* message: "Game locked" * message: "Game locked"
* turnOrder: [1,2,3] * turnOrder: [1,2,3]
* 400:
* description: Invalid game ID / Validation error
* 403:
* description: Only the creator can lock the game
* 500:
* description: Failed to lock game
*/ */
router.patch("/games/:id/lock", async (req, res) => { router.patch("/games/:id/lock", asyncHandler(async (req, res) => {
const { user } = req.body; const gameId = parseInt(req.params.id, 10);
const gameId = req.params.id; if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
const { error, value } = lockGameSchema.validate(req.body);
if (error) throw new ApiError(400, error.details[0].message);
const { user } = value;
const conn = await pool.getConnection(); const conn = await pool.getConnection();
try { try {
await conn.beginTransaction(); await conn.beginTransaction();
const [creatorRows] = await pool.query( const [creatorRows] = await conn.query(
"SELECT * FROM game_players WHERE game = ? AND user = ? AND is_creator = TRUE", "SELECT * FROM game_players WHERE game = ? AND user = ? AND is_creator = TRUE",
[gameId, user] [gameId, user]
); );
if (creatorRows.length === 0) { if (!creatorRows.length) throw new ApiError(403, "Only the creator can lock the game");
return res.status(403).json({ error: "Only the creator can lock the game" });
}
const [players] = await conn.query( const [players] = await conn.query(
`SELECT user FROM game_players WHERE game = ?`, "SELECT user FROM game_players WHERE game = ?",
[gameId] [gameId]
); );
@@ -313,7 +351,7 @@ router.patch("/games/:id/lock", async (req, res) => {
} }
await conn.query( await conn.query(
`UPDATE current_games SET is_open = FALSE, turn_order = ? WHERE id = ?`, "UPDATE current_games SET is_open = FALSE, turn_order = ? WHERE id = ?",
[JSON.stringify(turnOrder), gameId] [JSON.stringify(turnOrder), gameId]
); );
@@ -321,11 +359,11 @@ router.patch("/games/:id/lock", async (req, res) => {
res.json({ message: "Game locked", turnOrder }); res.json({ message: "Game locked", turnOrder });
} catch (err) { } catch (err) {
await conn.rollback(); await conn.rollback();
throw err; throw new ApiError(500, "Failed to lock game");
} finally { } finally {
conn.release(); conn.release();
} }
}); }));
/** /**
* @swagger * @swagger
@@ -349,50 +387,18 @@ router.patch("/games/:id/lock", async (req, res) => {
* - id: 2 * - id: 2
* username: "jane_doe" * username: "jane_doe"
*/ */
router.get("/games/:id/players", async (req, res) => { router.get("/games/:id/players", asyncHandler(async (req, res) => {
const gameId = parseInt(req.params.id, 10);
if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
const [players] = await pool.query( const [players] = await pool.query(
`SELECT u.id, u.username FROM game_players gp `SELECT u.id, u.username FROM game_players gp
JOIN users u ON gp.user = u.id JOIN users u ON gp.user = u.id
WHERE gp.game = ?`, WHERE gp.game = ?`,
[req.params.id] [gameId]
); );
res.json(players); res.json(players);
}); }));
/**
* @swagger
* /api/games/{id}/turns:
* get:
* summary: Get all turns for a game
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: List of turns
* content:
* application/json:
* example:
* - id: 1
* game: 1
* user: 1
* round_number: 1
* start_points: 501
* first_throw: 60
* second_throw: 45
* third_throw: 36
* end_points: 360
*/
router.get("/games/:id/turns", async (req, res) => {
const [rows] = await pool.query(
"SELECT * FROM turns WHERE game = ? ORDER BY round_number",
[req.params.id]
);
res.json(rows);
});
/** /**
* @swagger * @swagger
@@ -405,35 +411,6 @@ router.get("/games/:id/turns", async (req, res) => {
* required: true * required: true
* schema: * schema:
* type: integer * type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* user:
* type: integer
* round_number:
* type: integer
* start_points:
* type: integer
* first_throw:
* type: integer
* second_throw:
* type: integer
* third_throw:
* type: integer
* end_points:
* type: integer
* example:
* user: 1
* round_number: 1
* start_points: 501
* first_throw: 60
* second_throw: 45
* third_throw: 36
* end_points: 360
* responses: * responses:
* 201: * 201:
* description: Turn recorded * description: Turn recorded
@@ -442,10 +419,21 @@ router.get("/games/:id/turns", async (req, res) => {
* example: * example:
* turnId: 1 * turnId: 1
* nextPlayer: 2 * nextPlayer: 2
* 400:
* description: Validation error / Invalid game ID
* 403:
* description: Not your turn
* 500:
* description: Failed to record turn
*/ */
router.post("/games/:id/turns", async (req, res) => { router.post("/games/:id/turns", asyncHandler(async (req, res) => {
const { user, round_number, start_points, first_throw, second_throw, third_throw, end_points } = req.body; const gameId = parseInt(req.params.id, 10);
const gameId = req.params.id; if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
const { error, value } = turnSchema.validate(req.body);
if (error) throw new ApiError(400, error.details[0].message);
const { user, round_number, start_points, first_throw, second_throw, third_throw, end_points } = value;
const conn = await pool.getConnection(); const conn = await pool.getConnection();
try { try {
@@ -456,16 +444,12 @@ router.post("/games/:id/turns", async (req, res) => {
[gameId] [gameId]
); );
if (gameRows.length === 0) { if (!gameRows.length) throw new ApiError(404, "Game not found");
return res.status(404).json({ error: "Game not found" });
}
const game = gameRows[0]; const game = gameRows[0];
const turnOrder = JSON.parse(game.turn_order || '[]'); const turnOrder = JSON.parse(game.turn_order || '[]');
if (game.current_playing_user !== user) { if (game.current_playing_user !== user) throw new ApiError(403, "Not your turn");
return res.status(403).json({ error: "Not your turn" });
}
const [result] = await conn.query( const [result] = await conn.query(
`INSERT INTO turns `INSERT INTO turns
@@ -484,18 +468,45 @@ router.post("/games/:id/turns", async (req, res) => {
); );
await conn.commit(); await conn.commit();
res.status(201).json({ turnId: result.insertId, nextPlayer });
res.status(201).json({
turnId: result.insertId,
nextPlayer
});
} catch (err) { } catch (err) {
await conn.rollback(); await conn.rollback();
throw err; throw new ApiError(500, "Failed to record turn");
} finally { } finally {
conn.release(); conn.release();
} }
}); }));
/**
* @swagger
* /api/games/{id}/turns:
* get:
* summary: Get turns of a game
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: integer
* responses:
* 201:
* description: The turn
* content:
* application/json:
* example:
* turnId: 1
* nextPlayer: 2
*/
router.get("/games/:id/turns", asyncHandler(async (req, res) => {
const gameId = parseInt(req.params.id, 10);
if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");
const [rows] = await pool.query(
"SELECT * FROM turns WHERE game = ? ORDER BY round_number",
[gameId]
);
res.json(rows);
}));
/** /**
* @swagger * @swagger
@@ -524,9 +535,14 @@ router.post("/games/:id/turns", async (req, res) => {
* third_throw: 36 * third_throw: 36
* end_points: 360 * end_points: 360
*/ */
router.get("/turns/:id", async (req, res) => { router.get("/turns/:id", asyncHandler(async (req, res) => {
const [rows] = await pool.query("SELECT * FROM turns WHERE id = ?", [req.params.id]); const turnId = parseInt(req.params.id, 10);
res.json(rows[0] || null); if (isNaN(turnId)) throw new ApiError(400, "Invalid turn ID");
});
const [rows] = await pool.query("SELECT * FROM turns WHERE id = ?", [turnId]);
if (!rows.length) throw new ApiError(404, "Turn not found");
res.json(rows[0]);
}));
export default router; export default router;

View File

@@ -23,9 +23,9 @@ const migrations = [
await conn.query(` await conn.query(`
CREATE TABLE users ( CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT, id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(32) NOT NULL UNIQUE username VARCHAR(64) NOT NULL UNIQUE
) )
`); `); // max length is double the actual max length cause of unicode stuff
await conn.query(` await conn.query(`
CREATE TABLE games ( CREATE TABLE games (

View File

@@ -5,7 +5,7 @@ import https from 'https';
import express from 'express'; import express from 'express';
import cors from 'cors'; import cors from 'cors';
import path from 'path'; import path from 'path';
import apiRouter from './api.js'; import apiRouter, { ApiError, generateRequestBodies } from './api.js';
import { initDB } from './db.js'; import { initDB } from './db.js';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
@@ -13,6 +13,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.join(path.dirname(__filename), ".."); // going back a dir cuz code is in src/ const __dirname = path.join(path.dirname(__filename), ".."); // going back a dir cuz code is in src/
const app = express(); const app = express();
const specs = swaggerJsdoc({ const specs = swaggerJsdoc({
definition: { definition: {
openapi: "3.0.0", openapi: "3.0.0",
@@ -23,6 +24,17 @@ const specs = swaggerJsdoc({
}, },
apis: ["./src/api.js"], apis: ["./src/api.js"],
}); });
for (const [path, schema] of Object.entries(generateRequestBodies())) {
const swaggerPath = path.replace(/:(\w+)/g, '{$1}');
if (!specs.paths[swaggerPath]) continue;
const methodKeys = Object.keys(specs.paths[swaggerPath]);
methodKeys.forEach(method => {
if (["post", "put", "patch", "delete"].includes(method)) {
specs.paths[swaggerPath][method].requestBody = schema;
}
});
}
initDB(); initDB();
@@ -35,6 +47,16 @@ app.use(express.static(path.join(__dirname, "public"), {
app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(specs)); app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(specs));
app.use("/api", apiRouter); app.use("/api", apiRouter);
// has to be last
app.use((err, _req, res, _next) => {
if (err instanceof ApiError) {
return res.status(err.status).json({ status: "error", error: err.message });
}
console.error(err);
res.status(500).json({ status: "error", error: "Internal server error" })
})
const server = https.createServer({ const server = https.createServer({
key: fs.readFileSync('server.key'), key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.cert') cert: fs.readFileSync('server.cert')