feat(pong): added history api to get list of games
This commit is contained in:
parent
321f636672
commit
40dea32048
14 changed files with 1089 additions and 74 deletions
|
|
@ -19,6 +19,11 @@ export interface IPongDb extends Database {
|
|||
this: IPongDb,
|
||||
id: PongGameId,
|
||||
): PongGame | undefined,
|
||||
|
||||
getAllPongGameForUser(
|
||||
this: IPongDb,
|
||||
id: UserId,
|
||||
): (PongGame & { nameL: string, nameR: string })[],
|
||||
}
|
||||
|
||||
export const PongImpl: Omit<IPongDb, keyof Database> = {
|
||||
|
|
@ -49,6 +54,35 @@ export const PongImpl: Omit<IPongDb, keyof Database> = {
|
|||
const q = this.prepare('SELECT * FROM pong WHERE id = @id').get({ id }) as Partial<PongGameTable> | undefined;
|
||||
return pongGameFromRow(q);
|
||||
},
|
||||
|
||||
getAllPongGameForUser(
|
||||
this: IPongDb,
|
||||
id: UserId,
|
||||
): (PongGame & { nameL: string, nameR: string })[] {
|
||||
const q = this.prepare(`
|
||||
SELECT
|
||||
pong.*,
|
||||
userL.name AS nameL,
|
||||
userR.name AS nameR
|
||||
FROM pong
|
||||
INNER JOIN user AS userL
|
||||
ON pong.playerL = userL.id
|
||||
INNER JOIN user AS userR
|
||||
ON pong.playerR = userR.id
|
||||
WHERE
|
||||
pong.playerL = @id
|
||||
OR pong.playerR = @id;
|
||||
`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return q.all({ id }).map((s: any) => {
|
||||
const g: (PongGame & { nameL?: string, nameR?: string }) | undefined = pongGameFromRow(s);
|
||||
if (isNullish(g)) return undefined;
|
||||
g.nameL = s.nameL;
|
||||
g.nameR = s.nameR;
|
||||
if (isNullish(g.nameL) || isNullish(g.nameR)) return undefined;
|
||||
return g as PongGame & { nameL: string, nameR: string };
|
||||
}).filter(v => !isNullish(v));
|
||||
},
|
||||
};
|
||||
|
||||
export type PongGameId = UUID & { readonly __uuid: unique symbol };
|
||||
|
|
@ -59,6 +93,7 @@ export type PongGame = {
|
|||
readonly right: { id: UserId, score: number };
|
||||
readonly outcome: PongGameOutcome;
|
||||
readonly time: Date;
|
||||
readonly local: boolean;
|
||||
};
|
||||
|
||||
// this is an internal type, never to be seen outside
|
||||
|
|
@ -70,6 +105,7 @@ type PongGameTable = {
|
|||
scoreR: number,
|
||||
outcome: PongGameOutcome,
|
||||
time: string,
|
||||
local: number,
|
||||
};
|
||||
|
||||
function pongGameFromRow(r: Partial<PongGameTable> | undefined): PongGame | undefined {
|
||||
|
|
@ -81,6 +117,7 @@ function pongGameFromRow(r: Partial<PongGameTable> | undefined): PongGame | unde
|
|||
if (isNullish(r.scoreR)) return undefined;
|
||||
if (isNullish(r.outcome)) return undefined;
|
||||
if (isNullish(r.time)) return undefined;
|
||||
if (isNullish(r.local)) return undefined;
|
||||
|
||||
if (r.outcome !== 'winR' && r.outcome !== 'winL' && r.outcome !== 'other') return undefined;
|
||||
const date = Date.parse(r.time);
|
||||
|
|
@ -93,18 +130,6 @@ function pongGameFromRow(r: Partial<PongGameTable> | undefined): PongGame | unde
|
|||
right: { id: r.playerR, score: r.scoreR },
|
||||
outcome: r.outcome,
|
||||
time: new Date(date),
|
||||
local: r.local !== 0,
|
||||
};
|
||||
}
|
||||
|
||||
// this function will be able to be called from everywhere
|
||||
// export async function freeFloatingExportedFunction(): Promise<boolean> {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// this function will never be able to be called outside of this module
|
||||
// async function privateFunction(): Promise<string | undefined> {
|
||||
// return undefined;
|
||||
// }
|
||||
|
||||
// silence warnings
|
||||
// void privateFunction;
|
||||
|
|
|
|||
208
src/openapi.json
208
src/openapi.json
|
|
@ -1916,6 +1916,214 @@
|
|||
"openapi_other"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/pong/history/{user}": {
|
||||
"get": {
|
||||
"operationId": "pongHistory",
|
||||
"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": [
|
||||
"ponghistory.success"
|
||||
]
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"gameId",
|
||||
"left",
|
||||
"right",
|
||||
"local",
|
||||
"date",
|
||||
"outcome"
|
||||
],
|
||||
"properties": {
|
||||
"gameId": {
|
||||
"type": "string",
|
||||
"description": "gameId"
|
||||
},
|
||||
"left": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"score",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"score": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"score",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"score": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"local": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"outcome": {
|
||||
"enum": [
|
||||
"winL",
|
||||
"winR",
|
||||
"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": [
|
||||
"ponghistory.failure.notfound"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"openapi_other"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,213 @@
|
|||
"components": {
|
||||
"schemas": {}
|
||||
},
|
||||
"paths": {},
|
||||
"paths": {
|
||||
"/api/pong/history/{user}": {
|
||||
"get": {
|
||||
"operationId": "pongHistory",
|
||||
"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": [
|
||||
"ponghistory.success"
|
||||
]
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"gameId",
|
||||
"left",
|
||||
"right",
|
||||
"local",
|
||||
"date",
|
||||
"outcome"
|
||||
],
|
||||
"properties": {
|
||||
"gameId": {
|
||||
"type": "string",
|
||||
"description": "gameId"
|
||||
},
|
||||
"left": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"score",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"score": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"score",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"score": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"local": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"outcome": {
|
||||
"enum": [
|
||||
"winL",
|
||||
"winR",
|
||||
"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": [
|
||||
"ponghistory.failure.notfound"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://local.maix.me:8888",
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { Static, Type } from 'typebox';
|
||||
|
||||
export const PongReq = Type.Object({
|
||||
message: Type.String(),
|
||||
});
|
||||
|
||||
export type PongReq = Static<typeof PongReq>;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.post<{ Body: PongReq }>(
|
||||
'/api/pong/broadcast',
|
||||
{
|
||||
schema: {
|
||||
body: PongReq,
|
||||
hide: true,
|
||||
},
|
||||
config: { requireAuth: false },
|
||||
},
|
||||
async function(req, res) {
|
||||
void res;
|
||||
},
|
||||
);
|
||||
};
|
||||
export default route;
|
||||
|
||||
/**
|
||||
*
|
||||
* try this in a terminal
|
||||
*
|
||||
* curl -k --data-raw '{"message": "Message SENT from the terminal en REMOTE"}' 'https://local.maix.me:8888/api/pong/broadcast' -H "Content-Type: application/json"
|
||||
*
|
||||
* send message info to the fronatend via the route '/api/pong/broadcast'
|
||||
*/
|
||||
|
||||
// const route: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
// fastify.post('/api/chat/broadcast', {
|
||||
// schema: {
|
||||
// body: {
|
||||
// type: 'object',
|
||||
// required: ['nextGame'],
|
||||
// properties: {
|
||||
// nextGame: { type: 'string' }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }, async (req, reply) => {
|
||||
|
||||
// // Body only contains nextGame now
|
||||
// const gameLink: Promise<string> = Promise.resolve(req.body as string );
|
||||
|
||||
// // Broadcast nextGame
|
||||
// if (gameLink)
|
||||
// broadcastNextGame(fastify, gameLink);
|
||||
|
||||
// return reply.send({ status: 'ok' });
|
||||
// });
|
||||
// };
|
||||
// export default route;
|
||||
|
||||
68
src/pong/src/routes/pongHistory.ts
Normal file
68
src/pong/src/routes/pongHistory.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { UserId } from '@shared/database/mixin/user';
|
||||
import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils';
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { Static, Type } from 'typebox';
|
||||
|
||||
const PongHistoryParams = Type.Object({
|
||||
user: Type.String({ description: '\'me\' | <userid>' }),
|
||||
});
|
||||
|
||||
type PongHistoryParams = Static<typeof PongHistoryParams>;
|
||||
|
||||
const PongHistoryResponse = {
|
||||
'200': typeResponse('success', 'ponghistory.success', {
|
||||
data: Type.Array(
|
||||
Type.Object({
|
||||
gameId: Type.String({ description: 'gameId' }),
|
||||
left: Type.Object({
|
||||
score: Type.Integer(),
|
||||
id: Type.String(),
|
||||
name: Type.String(),
|
||||
}),
|
||||
right: Type.Object({
|
||||
score: Type.Integer(),
|
||||
id: Type.String(),
|
||||
name: Type.String(),
|
||||
}),
|
||||
local: Type.Boolean(),
|
||||
date: Type.String(),
|
||||
outcome: Type.Enum(['winL', 'winR', 'other']),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
'404': typeResponse('failure', 'ponghistory.failure.notfound'),
|
||||
};
|
||||
type PongHistoryResponse = MakeStaticResponse<typeof PongHistoryResponse>;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.get<{ Params: PongHistoryParams }>(
|
||||
'/api/pong/history/:user',
|
||||
{
|
||||
schema: {
|
||||
params: PongHistoryParams,
|
||||
response: PongHistoryResponse,
|
||||
operationId: 'pongHistory',
|
||||
},
|
||||
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', 'ponghistory.failure.notfound'); }
|
||||
const data = this.db.getAllPongGameForUser(req.params.user as UserId);
|
||||
if (isNullish(data)) { return res.makeResponse(404, 'failure', 'ponghistory.failure.notfound'); }
|
||||
|
||||
return res.makeResponse(200, 'success', 'ponghistory.success', {
|
||||
data: data.map(v => ({
|
||||
gameId: v.id,
|
||||
left: { score: v.left.score, id: v.left.id, name: v.nameL },
|
||||
right: { score: v.right.score, id: v.right.id, name: v.nameR },
|
||||
local: v.local,
|
||||
date: v.time.toString(),
|
||||
outcome: v.outcome,
|
||||
})),
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
export default route;
|
||||
|
|
@ -7,6 +7,8 @@ apis:
|
|||
root: ./chat/openapi.json
|
||||
ttt:
|
||||
root: ./tic-tac-toe/openapi.json
|
||||
pong:
|
||||
root: ./pong/openapi.json
|
||||
|
||||
rules:
|
||||
info-license: warn
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue