feat(ttt): match history done
This commit is contained in:
parent
8f3ed71d8a
commit
2bf5e6e700
17 changed files with 1185 additions and 18 deletions
|
|
@ -21,11 +21,12 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_blocked_user_pair ON blocked (user, blocke
|
|||
|
||||
CREATE TABLE IF NOT EXISTS tictactoe (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
player1 TEXT NOT NULL,
|
||||
player2 TEXT NOT NULL,
|
||||
time TEXT NOT NULL default (datetime('now')),
|
||||
playerX TEXT NOT NULL,
|
||||
playerO TEXT NOT NULL,
|
||||
outcome TEXT NOT NULL,
|
||||
FOREIGN KEY(player1) REFERENCES user(id),
|
||||
FOREIGN KEY(player2) REFERENCES user(id)
|
||||
FOREIGN KEY(playerX) REFERENCES user(id),
|
||||
FOREIGN KEY(playerO) REFERENCES user(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pong (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
import UUID from '@shared/utils/uuid';
|
||||
import type { Database } from './_base';
|
||||
import { UserId } from './user';
|
||||
import { isNullish } from '@shared/utils';
|
||||
|
||||
export type TicTacToeOutcome = 'winX' | 'winO' | 'other';
|
||||
// describe every function in the object
|
||||
export interface ITicTacToeDb extends Database {
|
||||
setTTTGameOutcome(this: ITicTacToeDb, id: TTTGameId, player1: UserId, player2: UserId, outcome: string): void,
|
||||
setTTTGameOutcome(this: ITicTacToeDb, id: TTTGameId, player1: UserId, player2: UserId, outcome: TicTacToeOutcome): void,
|
||||
getAllTTTGameForUser(
|
||||
this: ITicTacToeDb,
|
||||
id: UserId,
|
||||
): (TicTacToeGame & { nameX: string, nameO: string })[],
|
||||
};
|
||||
|
||||
|
||||
export const TicTacToeImpl: Omit<ITicTacToeDb, keyof Database> = {
|
||||
/**
|
||||
* @brief Write the outcome of the specified game to the database.
|
||||
|
|
@ -14,21 +21,81 @@ export const TicTacToeImpl: Omit<ITicTacToeDb, keyof Database> = {
|
|||
* @param gameId The game we want to write the outcome of.
|
||||
*
|
||||
*/
|
||||
setTTTGameOutcome(this: ITicTacToeDb, id: TTTGameId, player1: UserId, player2: UserId, outcome: string): void {
|
||||
setTTTGameOutcome(this: ITicTacToeDb, id: TTTGameId, playerX: UserId, playerO: UserId, outcome: TicTacToeOutcome): void {
|
||||
// Find a way to retrieve the outcome of the game.
|
||||
this.prepare('INSERT INTO tictactoe (id, player1, player2, outcome) VALUES (@id, @player1, @player2, @outcome)').run({ id, player1, player2, outcome });
|
||||
this.prepare('INSERT INTO tictactoe (id, playerX, playerO, outcome) VALUES (@id, @playerX, @playerO, @outcome)').run({ id, playerX, playerO, outcome });
|
||||
},
|
||||
|
||||
getAllTTTGameForUser(
|
||||
this: ITicTacToeDb,
|
||||
id: UserId,
|
||||
): (TicTacToeGame & { nameX: string, nameO: string })[] {
|
||||
const q = this.prepare(`
|
||||
SELECT
|
||||
tictactoe.*,
|
||||
userX.name AS nameX,
|
||||
userO.name AS nameO
|
||||
FROM tictactoe
|
||||
INNER JOIN user AS userX
|
||||
ON tictactoe.playerX = userX.id
|
||||
INNER JOIN user AS userO
|
||||
ON tictactoe.playerO = userO.id
|
||||
WHERE
|
||||
tictactoe.playerX = @id
|
||||
OR tictactoe.playerO = @id;
|
||||
`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return q.all({ id }).map((s: any) => {
|
||||
const g: (TicTacToeGame & { nameX?: string, nameO?: string }) | undefined = TicTacToeGameFromRow(s);
|
||||
if (isNullish(g)) return undefined;
|
||||
g.nameX = s.nameX;
|
||||
g.nameO = s.nameO;
|
||||
if (isNullish(g.nameO) || isNullish(g.nameO)) return undefined;
|
||||
return g as TicTacToeGame & { nameX: string, nameO: string };
|
||||
}).filter(v => !isNullish(v));
|
||||
}
|
||||
};
|
||||
|
||||
export type TTTGameId = UUID & { readonly __brand: unique symbol };
|
||||
export type TTTGameId = UUID & { readonly __uuid: unique symbol };
|
||||
|
||||
export type TicTacToeGame = {
|
||||
readonly id: TTTGameId;
|
||||
readonly player1: UserId;
|
||||
readonly player2: UserId;
|
||||
readonly outcome: string;
|
||||
readonly time: Date;
|
||||
readonly playerX: UserId;
|
||||
readonly playerO: UserId;
|
||||
readonly outcome: TicTacToeOutcome;
|
||||
};
|
||||
|
||||
type TicTacToeGameTable = {
|
||||
id: string;
|
||||
time: string;
|
||||
playerX: UserId;
|
||||
playerO: UserId;
|
||||
outcome: string;
|
||||
};
|
||||
|
||||
function TicTacToeGameFromRow(r: Partial<TicTacToeGameTable> | undefined): TicTacToeGame | undefined {
|
||||
if (isNullish(r)) return undefined;
|
||||
if (isNullish(r.id)) return undefined;
|
||||
if (isNullish(r.playerX)) return undefined;
|
||||
if (isNullish(r.playerO)) return undefined;
|
||||
if (isNullish(r.outcome)) return undefined;
|
||||
if (isNullish(r.time)) return undefined;
|
||||
|
||||
if (r.outcome !== 'winX' && r.outcome !== 'winO' && r.outcome !== 'other') return undefined;
|
||||
const date = Date.parse(r.time);
|
||||
if (Number.isNaN(date)) return undefined;
|
||||
|
||||
|
||||
return {
|
||||
id: r.id as TTTGameId,
|
||||
playerX: r.playerX,
|
||||
playerO: r.playerO,
|
||||
outcome: r.outcome,
|
||||
time: new Date(date),
|
||||
};
|
||||
}
|
||||
|
||||
// this function will be able to be called from everywhere
|
||||
// export async function freeFloatingExportedFunction(): Promise<boolean> {
|
||||
// return false;
|
||||
|
|
|
|||
196
src/openapi.json
196
src/openapi.json
|
|
@ -1917,6 +1917,202 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/ttt/history/{user}": {
|
||||
"get": {
|
||||
"operationId": "tttHistory",
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "path",
|
||||
"name": "user",
|
||||
"required": true,
|
||||
"description": "'me' | <userid>"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg",
|
||||
"payload"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"success"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"ttthistory.success"
|
||||
]
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"gameId",
|
||||
"playerX",
|
||||
"playerO",
|
||||
"date",
|
||||
"outcome"
|
||||
],
|
||||
"properties": {
|
||||
"gameId": {
|
||||
"type": "string",
|
||||
"description": "gameId"
|
||||
},
|
||||
"playerX": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"playerO": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"outcome": {
|
||||
"enum": [
|
||||
"winX",
|
||||
"winO",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"notLoggedIn"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"auth.noCookie",
|
||||
"auth.invalidKind",
|
||||
"auth.noUser",
|
||||
"auth.invalid"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"notLoggedIn"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"auth.noCookie",
|
||||
"auth.invalidKind",
|
||||
"auth.noUser",
|
||||
"auth.invalid"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"failure"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"ttthistory.failure.notfound"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"openapi_other"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/pong/history/{user}": {
|
||||
"get": {
|
||||
"operationId": "pongHistory",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,201 @@
|
|||
"components": {
|
||||
"schemas": {}
|
||||
},
|
||||
"paths": {},
|
||||
"paths": {
|
||||
"/api/ttt/history/{user}": {
|
||||
"get": {
|
||||
"operationId": "tttHistory",
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "path",
|
||||
"name": "user",
|
||||
"required": true,
|
||||
"description": "'me' | <userid>"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg",
|
||||
"payload"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"success"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"ttthistory.success"
|
||||
]
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"gameId",
|
||||
"playerX",
|
||||
"playerO",
|
||||
"date",
|
||||
"outcome"
|
||||
],
|
||||
"properties": {
|
||||
"gameId": {
|
||||
"type": "string",
|
||||
"description": "gameId"
|
||||
},
|
||||
"playerX": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"playerO": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"outcome": {
|
||||
"enum": [
|
||||
"winX",
|
||||
"winO",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"notLoggedIn"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"auth.noCookie",
|
||||
"auth.invalidKind",
|
||||
"auth.noUser",
|
||||
"auth.invalid"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"notLoggedIn"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"auth.noCookie",
|
||||
"auth.invalidKind",
|
||||
"auth.noUser",
|
||||
"auth.invalid"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"failure"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"ttthistory.failure.notfound"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://local.maix.me:8888",
|
||||
|
|
|
|||
58
src/tic-tac-toe/src/routes/tttHistory.ts
Normal file
58
src/tic-tac-toe/src/routes/tttHistory.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { UserId } from '@shared/database/mixin/user';
|
||||
import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils';
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { Static, Type } from 'typebox';
|
||||
|
||||
const TTTHistoryParams = Type.Object({
|
||||
user: Type.String({ description: '\'me\' | <userid>' }),
|
||||
});
|
||||
|
||||
type TTTHistoryParams = Static<typeof TTTHistoryParams>;
|
||||
|
||||
const TTTHistoryResponse = {
|
||||
'200': typeResponse('success', 'ttthistory.success', {
|
||||
data: Type.Array(
|
||||
Type.Object({
|
||||
gameId: Type.String({ description: 'gameId' }),
|
||||
playerX: Type.Object({id: Type.String(), name: Type.String()}),
|
||||
playerO: Type.Object({id: Type.String(), name: Type.String()}),
|
||||
date: Type.String(),
|
||||
outcome: Type.Enum(['winX', 'winO', 'other']),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
'404': typeResponse('failure', 'ttthistory.failure.notfound'),
|
||||
};
|
||||
type TTTHistoryResponse = MakeStaticResponse<typeof TTTHistoryResponse>;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.get<{ Params: TTTHistoryParams }>(
|
||||
'/api/ttt/history/:user',
|
||||
{
|
||||
schema: {
|
||||
params: TTTHistoryParams,
|
||||
response: TTTHistoryResponse,
|
||||
operationId: 'tttHistory',
|
||||
},
|
||||
config: { requireAuth: true },
|
||||
},
|
||||
async function(req, res) {
|
||||
if (req.params.user === 'me') { req.params.user = req.authUser!.id; }
|
||||
const user = this.db.getUser(req.params.user);
|
||||
if (isNullish(user)) { return res.makeResponse(404, 'failure', 'ttthistory.failure.notfound'); }
|
||||
const data = this.db.getAllTTTGameForUser(req.params.user as UserId);
|
||||
if (isNullish(data)) { return res.makeResponse(404, 'failure', 'ttthistory.failure.notfound'); }
|
||||
|
||||
return res.makeResponse(200, 'success', 'ttthistory.success', {
|
||||
data: data.map(v => ({
|
||||
gameId: v.id,
|
||||
playerX: { id: v.playerX, name: v.nameX },
|
||||
playerO: { id: v.playerO, name: v.nameO },
|
||||
date: v.time.toString(),
|
||||
outcome: v.outcome,
|
||||
})),
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
export default route;
|
||||
Loading…
Add table
Add a link
Reference in a new issue