better documentation
This commit is contained in:
95
backend/package-lock.json
generated
95
backend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,32 +86,25 @@ 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 (?)
|
||||||
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)`,
|
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)`,
|
||||||
[username]
|
[username]
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user