feat(ttt): match history done

This commit is contained in:
Maix0 2026-01-06 23:44:02 +01:00 committed by Maix0
parent 8f3ed71d8a
commit 2bf5e6e700
17 changed files with 1185 additions and 18 deletions

View file

@ -44,7 +44,7 @@ prune:
logs:
@$(MAKE) --no-print-directory -f ./Docker.mk logs
sqlite:
sql:
docker compose exec auth apk add sqlite
-docker compose exec -it auth sqlite3 /volumes/database/database.db
@ -125,11 +125,6 @@ npm@openapi: openapi.jar
openapi.jar:
wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.15.0/openapi-generator-cli-7.15.0.jar -O ./openapi.jar
# this convert the .dbml file to an actual sql file that SQLite can handle :)
sql:
@echo "if the command isn't found, contact maieul :)"
dbml_sqlite -t -f -w ./src/@shared/src/database/init.sql ./src/@shared/src/database/init.dbml
tmux:
@tmux new-session -d -s $(PROJECT)
@tmux send-keys -t $(PROJECT):0 'vim' C-m

View file

@ -66,5 +66,10 @@ models/StatusOtp200ResponseAnyOf1.ts
models/StatusOtp200ResponseAnyOfPayload.ts
models/StatusOtp401Response.ts
models/StatusOtp500Response.ts
models/TttHistory200Response.ts
models/TttHistory200ResponsePayload.ts
models/TttHistory200ResponsePayloadDataInner.ts
models/TttHistory200ResponsePayloadDataInnerPlayerX.ts
models/TttHistory404Response.ts
models/index.ts
runtime.ts

View file

@ -63,6 +63,8 @@ import type {
StatusOtp200Response,
StatusOtp401Response,
StatusOtp500Response,
TttHistory200Response,
TttHistory404Response,
} from '../models/index';
import {
AllowGuestMessage200ResponseFromJSON,
@ -161,6 +163,10 @@ import {
StatusOtp401ResponseToJSON,
StatusOtp500ResponseFromJSON,
StatusOtp500ResponseToJSON,
TttHistory200ResponseFromJSON,
TttHistory200ResponseToJSON,
TttHistory404ResponseFromJSON,
TttHistory404ResponseToJSON,
} from '../models/index';
export interface ChangeDescOperationRequest {
@ -199,6 +205,10 @@ export interface SigninRequest {
loginRequest: LoginRequest;
}
export interface TttHistoryRequest {
user: string;
}
/**
*
*/
@ -1027,4 +1037,58 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
return await response.value();
}
/**
*/
async tttHistoryRaw(requestParameters: TttHistoryRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<TttHistory200Response | StatusOtp401Response | TttHistory404Response>> {
if (requestParameters['user'] == null) {
throw new runtime.RequiredError(
'user',
'Required parameter "user" was null or undefined when calling tttHistory().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
let urlPath = `/api/ttt/history/{user}`;
urlPath = urlPath.replace(`{${"user"}}`, encodeURIComponent(String(requestParameters['user'])));
const response = await this.request({
path: urlPath,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
// CHANGED: Handle all status codes defined in the OpenAPI spec, not just 2xx responses
// This allows typed access to error responses (4xx, 5xx) and other status codes.
// The code routes responses based on the actual HTTP status code and returns
// appropriately typed ApiResponse wrappers for each status code.
if (response.status === 200) {
// Object response for status 200
return new runtime.JSONApiResponse(response, (jsonValue) => TttHistory200ResponseFromJSON(jsonValue));
}
if (response.status === 401) {
// Object response for status 401
return new runtime.JSONApiResponse(response, (jsonValue) => StatusOtp401ResponseFromJSON(jsonValue));
}
if (response.status === 404) {
// Object response for status 404
return new runtime.JSONApiResponse(response, (jsonValue) => TttHistory404ResponseFromJSON(jsonValue));
}
// CHANGED: Throw error if status code is not handled by any of the defined responses
// This ensures all code paths return a value and provides clear error messages for unexpected status codes
// Only throw if responses were defined but none matched the actual status code
throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 401, 404`);
}
/**
*/
async tttHistory(requestParameters: TttHistoryRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<TttHistory200Response | StatusOtp401Response | TttHistory404Response> {
const response = await this.tttHistoryRaw(requestParameters, initOverrides);
return await response.value();
}
}

View file

@ -0,0 +1,110 @@
/* tslint:disable */
/* eslint-disable */
/**
* @fastify/swagger
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 9.6.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { TttHistory200ResponsePayload } from './TttHistory200ResponsePayload';
import {
TttHistory200ResponsePayloadFromJSON,
TttHistory200ResponsePayloadFromJSONTyped,
TttHistory200ResponsePayloadToJSON,
TttHistory200ResponsePayloadToJSONTyped,
} from './TttHistory200ResponsePayload';
/**
*
* @export
* @interface TttHistory200Response
*/
export interface TttHistory200Response {
/**
*
* @type {string}
* @memberof TttHistory200Response
*/
kind: TttHistory200ResponseKindEnum;
/**
*
* @type {string}
* @memberof TttHistory200Response
*/
msg: TttHistory200ResponseMsgEnum;
/**
*
* @type {TttHistory200ResponsePayload}
* @memberof TttHistory200Response
*/
payload: TttHistory200ResponsePayload;
}
/**
* @export
*/
export const TttHistory200ResponseKindEnum = {
Success: 'success'
} as const;
export type TttHistory200ResponseKindEnum = typeof TttHistory200ResponseKindEnum[keyof typeof TttHistory200ResponseKindEnum];
/**
* @export
*/
export const TttHistory200ResponseMsgEnum = {
TtthistorySuccess: 'ttthistory.success'
} as const;
export type TttHistory200ResponseMsgEnum = typeof TttHistory200ResponseMsgEnum[keyof typeof TttHistory200ResponseMsgEnum];
/**
* Check if a given object implements the TttHistory200Response interface.
*/
export function instanceOfTttHistory200Response(value: object): value is TttHistory200Response {
if (!('kind' in value) || value['kind'] === undefined) return false;
if (!('msg' in value) || value['msg'] === undefined) return false;
if (!('payload' in value) || value['payload'] === undefined) return false;
return true;
}
export function TttHistory200ResponseFromJSON(json: any): TttHistory200Response {
return TttHistory200ResponseFromJSONTyped(json, false);
}
export function TttHistory200ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): TttHistory200Response {
if (json == null) {
return json;
}
return {
'kind': json['kind'],
'msg': json['msg'],
'payload': TttHistory200ResponsePayloadFromJSON(json['payload']),
};
}
export function TttHistory200ResponseToJSON(json: any): TttHistory200Response {
return TttHistory200ResponseToJSONTyped(json, false);
}
export function TttHistory200ResponseToJSONTyped(value?: TttHistory200Response | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'kind': value['kind'],
'msg': value['msg'],
'payload': TttHistory200ResponsePayloadToJSON(value['payload']),
};
}

View file

@ -0,0 +1,74 @@
/* tslint:disable */
/* eslint-disable */
/**
* @fastify/swagger
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 9.6.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { TttHistory200ResponsePayloadDataInner } from './TttHistory200ResponsePayloadDataInner';
import {
TttHistory200ResponsePayloadDataInnerFromJSON,
TttHistory200ResponsePayloadDataInnerFromJSONTyped,
TttHistory200ResponsePayloadDataInnerToJSON,
TttHistory200ResponsePayloadDataInnerToJSONTyped,
} from './TttHistory200ResponsePayloadDataInner';
/**
*
* @export
* @interface TttHistory200ResponsePayload
*/
export interface TttHistory200ResponsePayload {
/**
*
* @type {Array<TttHistory200ResponsePayloadDataInner>}
* @memberof TttHistory200ResponsePayload
*/
data: Array<TttHistory200ResponsePayloadDataInner>;
}
/**
* Check if a given object implements the TttHistory200ResponsePayload interface.
*/
export function instanceOfTttHistory200ResponsePayload(value: object): value is TttHistory200ResponsePayload {
if (!('data' in value) || value['data'] === undefined) return false;
return true;
}
export function TttHistory200ResponsePayloadFromJSON(json: any): TttHistory200ResponsePayload {
return TttHistory200ResponsePayloadFromJSONTyped(json, false);
}
export function TttHistory200ResponsePayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): TttHistory200ResponsePayload {
if (json == null) {
return json;
}
return {
'data': ((json['data'] as Array<any>).map(TttHistory200ResponsePayloadDataInnerFromJSON)),
};
}
export function TttHistory200ResponsePayloadToJSON(json: any): TttHistory200ResponsePayload {
return TttHistory200ResponsePayloadToJSONTyped(json, false);
}
export function TttHistory200ResponsePayloadToJSONTyped(value?: TttHistory200ResponsePayload | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'data': ((value['data'] as Array<any>).map(TttHistory200ResponsePayloadDataInnerToJSON)),
};
}

View file

@ -0,0 +1,122 @@
/* tslint:disable */
/* eslint-disable */
/**
* @fastify/swagger
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 9.6.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { TttHistory200ResponsePayloadDataInnerPlayerX } from './TttHistory200ResponsePayloadDataInnerPlayerX';
import {
TttHistory200ResponsePayloadDataInnerPlayerXFromJSON,
TttHistory200ResponsePayloadDataInnerPlayerXFromJSONTyped,
TttHistory200ResponsePayloadDataInnerPlayerXToJSON,
TttHistory200ResponsePayloadDataInnerPlayerXToJSONTyped,
} from './TttHistory200ResponsePayloadDataInnerPlayerX';
/**
*
* @export
* @interface TttHistory200ResponsePayloadDataInner
*/
export interface TttHistory200ResponsePayloadDataInner {
/**
* gameId
* @type {string}
* @memberof TttHistory200ResponsePayloadDataInner
*/
gameId: string;
/**
*
* @type {TttHistory200ResponsePayloadDataInnerPlayerX}
* @memberof TttHistory200ResponsePayloadDataInner
*/
playerX: TttHistory200ResponsePayloadDataInnerPlayerX;
/**
*
* @type {TttHistory200ResponsePayloadDataInnerPlayerX}
* @memberof TttHistory200ResponsePayloadDataInner
*/
playerO: TttHistory200ResponsePayloadDataInnerPlayerX;
/**
*
* @type {string}
* @memberof TttHistory200ResponsePayloadDataInner
*/
date: string;
/**
*
* @type {string}
* @memberof TttHistory200ResponsePayloadDataInner
*/
outcome: TttHistory200ResponsePayloadDataInnerOutcomeEnum;
}
/**
* @export
*/
export const TttHistory200ResponsePayloadDataInnerOutcomeEnum = {
WinX: 'winX',
WinO: 'winO',
Other: 'other'
} as const;
export type TttHistory200ResponsePayloadDataInnerOutcomeEnum = typeof TttHistory200ResponsePayloadDataInnerOutcomeEnum[keyof typeof TttHistory200ResponsePayloadDataInnerOutcomeEnum];
/**
* Check if a given object implements the TttHistory200ResponsePayloadDataInner interface.
*/
export function instanceOfTttHistory200ResponsePayloadDataInner(value: object): value is TttHistory200ResponsePayloadDataInner {
if (!('gameId' in value) || value['gameId'] === undefined) return false;
if (!('playerX' in value) || value['playerX'] === undefined) return false;
if (!('playerO' in value) || value['playerO'] === undefined) return false;
if (!('date' in value) || value['date'] === undefined) return false;
if (!('outcome' in value) || value['outcome'] === undefined) return false;
return true;
}
export function TttHistory200ResponsePayloadDataInnerFromJSON(json: any): TttHistory200ResponsePayloadDataInner {
return TttHistory200ResponsePayloadDataInnerFromJSONTyped(json, false);
}
export function TttHistory200ResponsePayloadDataInnerFromJSONTyped(json: any, ignoreDiscriminator: boolean): TttHistory200ResponsePayloadDataInner {
if (json == null) {
return json;
}
return {
'gameId': json['gameId'],
'playerX': TttHistory200ResponsePayloadDataInnerPlayerXFromJSON(json['playerX']),
'playerO': TttHistory200ResponsePayloadDataInnerPlayerXFromJSON(json['playerO']),
'date': json['date'],
'outcome': json['outcome'],
};
}
export function TttHistory200ResponsePayloadDataInnerToJSON(json: any): TttHistory200ResponsePayloadDataInner {
return TttHistory200ResponsePayloadDataInnerToJSONTyped(json, false);
}
export function TttHistory200ResponsePayloadDataInnerToJSONTyped(value?: TttHistory200ResponsePayloadDataInner | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'gameId': value['gameId'],
'playerX': TttHistory200ResponsePayloadDataInnerPlayerXToJSON(value['playerX']),
'playerO': TttHistory200ResponsePayloadDataInnerPlayerXToJSON(value['playerO']),
'date': value['date'],
'outcome': value['outcome'],
};
}

View file

@ -0,0 +1,75 @@
/* tslint:disable */
/* eslint-disable */
/**
* @fastify/swagger
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 9.6.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
/**
*
* @export
* @interface TttHistory200ResponsePayloadDataInnerPlayerX
*/
export interface TttHistory200ResponsePayloadDataInnerPlayerX {
/**
*
* @type {string}
* @memberof TttHistory200ResponsePayloadDataInnerPlayerX
*/
id: string;
/**
*
* @type {string}
* @memberof TttHistory200ResponsePayloadDataInnerPlayerX
*/
name: string;
}
/**
* Check if a given object implements the TttHistory200ResponsePayloadDataInnerPlayerX interface.
*/
export function instanceOfTttHistory200ResponsePayloadDataInnerPlayerX(value: object): value is TttHistory200ResponsePayloadDataInnerPlayerX {
if (!('id' in value) || value['id'] === undefined) return false;
if (!('name' in value) || value['name'] === undefined) return false;
return true;
}
export function TttHistory200ResponsePayloadDataInnerPlayerXFromJSON(json: any): TttHistory200ResponsePayloadDataInnerPlayerX {
return TttHistory200ResponsePayloadDataInnerPlayerXFromJSONTyped(json, false);
}
export function TttHistory200ResponsePayloadDataInnerPlayerXFromJSONTyped(json: any, ignoreDiscriminator: boolean): TttHistory200ResponsePayloadDataInnerPlayerX {
if (json == null) {
return json;
}
return {
'id': json['id'],
'name': json['name'],
};
}
export function TttHistory200ResponsePayloadDataInnerPlayerXToJSON(json: any): TttHistory200ResponsePayloadDataInnerPlayerX {
return TttHistory200ResponsePayloadDataInnerPlayerXToJSONTyped(json, false);
}
export function TttHistory200ResponsePayloadDataInnerPlayerXToJSONTyped(value?: TttHistory200ResponsePayloadDataInnerPlayerX | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'name': value['name'],
};
}

View file

@ -0,0 +1,93 @@
/* tslint:disable */
/* eslint-disable */
/**
* @fastify/swagger
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 9.6.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
/**
*
* @export
* @interface TttHistory404Response
*/
export interface TttHistory404Response {
/**
*
* @type {string}
* @memberof TttHistory404Response
*/
kind: TttHistory404ResponseKindEnum;
/**
*
* @type {string}
* @memberof TttHistory404Response
*/
msg: TttHistory404ResponseMsgEnum;
}
/**
* @export
*/
export const TttHistory404ResponseKindEnum = {
Failure: 'failure'
} as const;
export type TttHistory404ResponseKindEnum = typeof TttHistory404ResponseKindEnum[keyof typeof TttHistory404ResponseKindEnum];
/**
* @export
*/
export const TttHistory404ResponseMsgEnum = {
TtthistoryFailureNotfound: 'ttthistory.failure.notfound'
} as const;
export type TttHistory404ResponseMsgEnum = typeof TttHistory404ResponseMsgEnum[keyof typeof TttHistory404ResponseMsgEnum];
/**
* Check if a given object implements the TttHistory404Response interface.
*/
export function instanceOfTttHistory404Response(value: object): value is TttHistory404Response {
if (!('kind' in value) || value['kind'] === undefined) return false;
if (!('msg' in value) || value['msg'] === undefined) return false;
return true;
}
export function TttHistory404ResponseFromJSON(json: any): TttHistory404Response {
return TttHistory404ResponseFromJSONTyped(json, false);
}
export function TttHistory404ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): TttHistory404Response {
if (json == null) {
return json;
}
return {
'kind': json['kind'],
'msg': json['msg'],
};
}
export function TttHistory404ResponseToJSON(json: any): TttHistory404Response {
return TttHistory404ResponseToJSONTyped(json, false);
}
export function TttHistory404ResponseToJSONTyped(value?: TttHistory404Response | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'kind': value['kind'],
'msg': value['msg'],
};
}

View file

@ -65,3 +65,8 @@ export * from './StatusOtp200ResponseAnyOf1';
export * from './StatusOtp200ResponseAnyOfPayload';
export * from './StatusOtp401Response';
export * from './StatusOtp500Response';
export * from './TttHistory200Response';
export * from './TttHistory200ResponsePayload';
export * from './TttHistory200ResponsePayloadDataInner';
export * from './TttHistory200ResponsePayloadDataInnerPlayerX';
export * from './TttHistory404Response';

View file

@ -8,6 +8,7 @@ import './ttt/ttt.ts'
import './profile/profile.ts'
import './logout/logout.ts'
import './pongHistory/pongHistory.ts'
import './tttHistory/tttHistory.ts'
// ---- Initial load ----
setTitle("");

View file

@ -0,0 +1,18 @@
<div class="fixed inset-0 flex items-center justify-center bg-[#43536b]">
<div
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-gray-200 w-212.5 p-6 rounded-xl shadow-2xl text-center z-50">
<h2 class="text-2xl font-bold text-center mb-4 text-gray-700">
TicTacToe Match History For
<span id="name" class="text-amber-500"></span>
</h2>
<div id="matchList" class="max-w-3xl mx-auto space-y-2 max-h-[50dvh] overflow-scroll"></div>
<div style="display: none">
<div class="grid grid-cols-[1fr_auto_1fr] items-center bg-zinc-800 rounded-lg px-4 py-3"></div>
<div class="text-right"></div>
<div class="text-left"></div>
<div class="text-green-400"></div>
<div class="text-red-400"></div>
<div class="text-semibold"></div>
</div>
</div>
</div>

View file

@ -0,0 +1,89 @@
import { addRoute, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing";
import page from './tttHistory.html?raw';
import { isNullish } from "@app/utils";
import client from "@app/api";
import { updateUser } from "@app/auth";
function getHHMM(d: Date): string {
let h = d.getHours();
let m = d.getMinutes();
return `${h < 9 ? '0' : ''}${h}:${m < 9 ? '0' : ''}${m}`
}
async function tttHistory(_url: string, args: RouteHandlerParams): Promise<RouteHandlerReturn> {
if (isNullish(args.userid))
args.userid = 'me';
let user = await updateUser();
if (isNullish(user)) {
return { html: '<span> You aren\'t logged in </span>' };
}
let userInfoRes = await client.getUser({user: args.userid});
if (userInfoRes.kind !== 'success')
{
return { html: '<span> You tried to open a game history with no data :(</span>' };
}
let userInfo = userInfoRes.payload;
let res = await client.tttHistory({ user: args.userid });
if (res.kind === 'failure' || res.kind === 'notLoggedIn') {
// todo: make a real page on no data
return { html: '<span> You tried to open a game history with no data :(</span>' };
}
let games = res.payload.data;
games.reverse();
let gameElement = games.map(g => {
let rdate = Date.parse(g.date);
if (Number.isNaN(rdate)) return undefined;
let date = new Date(rdate);
const e = document.createElement('div');
let color = 'bg-amber-200';
// maybe we do want local games ? then put the check here :D
// if (!g.local) {
if (true) {
let youwin = false;
if (g.playerX.id === user.id && g.outcome === 'winX')
youwin = true;
else if (g.playerO.id === user.id && g.outcome === 'winO')
youwin = true;
if (youwin)
color = 'bg-green-300';
else
color = 'bg-red-300';
}
e.className =
'grid grid-cols-[1fr_auto_1fr] items-center rounded-lg px-4 py-3 ' + color;
e.innerHTML = `
<div class="text-right">
<div class="font-semibold ${g.outcome === 'winX' ? 'text-green-600' : 'text-red-600'}">${g.playerX.name}</div>
<div class="text-lg ${g.outcome === 'winX' ? 'text-green-600' : 'text-red-600'}">${g.outcome === 'winX' ? 'WON' : 'LOST'}</div>
</div>
<div class="text-center text-sm text-gray-800 px-4 whitespace-nowrap">${date.toDateString()}<br />${getHHMM(date)}</div>
<div class="text-left">
<div class="font-semibold ${g.outcome === 'winO' ? 'text-green-600' : 'text-red-600'}">${g.playerO.name}</div>
<div class="text-lg ${g.outcome === 'winO' ? 'text-green-600' : 'text-red-600'}">${g.outcome === 'winO' ? 'WON' : 'LOST'}</div>
</div>`;
return e;
}).filter(v => !isNullish(v));
return {
html: page, postInsert: async (app) => {
if (!app) return;
const matchBox = app.querySelector<HTMLDivElement>("#matchList");
if (!matchBox) return;
gameElement.forEach(c => matchBox.appendChild(c));
const userBox = app.querySelector<HTMLDivElement>("#name");
if (!userBox) return;
userBox.innerText = userInfo.name;
}
};
}
addRoute('/ttt/games', tttHistory);
addRoute('/ttt/games/:userid', tttHistory);

View file

@ -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 (

View file

@ -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;

View file

@ -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",

View file

@ -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",

View 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;