324 lines
8.9 KiB
TypeScript
324 lines
8.9 KiB
TypeScript
import { Game, PreGame } from "./types.ts";
|
|
import {
|
|
assertEquals,
|
|
assertNotEquals,
|
|
} from "https://deno.land/std@0.224.0/assert/mod.ts";
|
|
|
|
type GamePrimaryKey = ["phenix", "games", string];
|
|
|
|
type date = [number, number, number];
|
|
type time = [number, number];
|
|
type DateIndex = ["phenix", "indexes", "game-date", ...date, ...time];
|
|
|
|
export class GameService {
|
|
readonly kv: Deno.Kv;
|
|
private readonly DATE_VALIDATOR =
|
|
/^(?<year>\d{4})-(?<month>\d{1,2})-(?<day>\d{1,2})$/;
|
|
private readonly TIME_VALIDATOR = /^(?<hour>\d{1,2})(:|h)(?<minute>\d{1,2})$/;
|
|
private readonly VALID_TEAMS: string[] = [
|
|
"jaune",
|
|
"rouge",
|
|
"bleu",
|
|
"gris",
|
|
"vert",
|
|
"noir",
|
|
"orange",
|
|
"mauve",
|
|
];
|
|
|
|
constructor(kv: Deno.Kv) {
|
|
this.kv = kv;
|
|
}
|
|
|
|
private chronologicalGameOrder(game1: Game, game2: Game): number {
|
|
if (game1.date < game2.date) {
|
|
return 1;
|
|
}
|
|
if (game1.date > game2.date) {
|
|
return -1;
|
|
}
|
|
if (game1.time < game2.time) {
|
|
return 1;
|
|
}
|
|
if (game1.time > game2.time) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
async getAllGames(): Promise<Game[]> {
|
|
const ite = this.kv.list<Game>({ prefix: ["phenix", "games"] });
|
|
return (await Array.fromAsync(ite))
|
|
.map((e) => e.value)
|
|
.sort(this.chronologicalGameOrder);
|
|
}
|
|
|
|
private gamePrimaryKey(id: string): GamePrimaryKey {
|
|
return ["phenix", "games", id];
|
|
}
|
|
|
|
private dateIndex(date: string, time: string): DateIndex | null {
|
|
let destructedDate = this.DATE_VALIDATOR.exec(date)?.groups;
|
|
let destructedTime = this.TIME_VALIDATOR.exec(time)?.groups;
|
|
|
|
if (!destructedDate || !destructedTime) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
"phenix",
|
|
"indexes",
|
|
"game-date",
|
|
parseInt(destructedDate.year),
|
|
parseInt(destructedDate.month),
|
|
parseInt(destructedDate.day),
|
|
parseInt(destructedTime.hour),
|
|
parseInt(destructedTime.minute),
|
|
];
|
|
}
|
|
|
|
private cleanTime(raw: string): string {
|
|
const groups = this.TIME_VALIDATOR.exec(raw)!.groups!;
|
|
return `${groups.hour.padStart(2, "0")}:${groups.minute.padStart(2, "0")}`;
|
|
}
|
|
|
|
private cleanDate(raw: string): string {
|
|
const groups = this.DATE_VALIDATOR.exec(raw)!.groups!;
|
|
return `${groups.year}-${groups.month.padStart(2, "0")}-${groups.day.padStart(2, "0")}`;
|
|
}
|
|
|
|
private extractYearFromDate(date: string): number {
|
|
return parseInt(this.DATE_VALIDATOR.exec(date)!.groups!.year);
|
|
}
|
|
|
|
private createGame(preGame: PreGame): Game {
|
|
const ret = {
|
|
...preGame,
|
|
id: crypto.randomUUID(),
|
|
practice: false,
|
|
rain: false,
|
|
year: this.extractYearFromDate(preGame.date),
|
|
homeScore: null,
|
|
awayScore: null,
|
|
};
|
|
ret.time = this.cleanTime(ret.time);
|
|
ret.date = this.cleanDate(ret.date);
|
|
|
|
return ret;
|
|
}
|
|
|
|
private validatePregate(preGame: PreGame): string {
|
|
if (!this.DATE_VALIDATOR.test(preGame.date)) {
|
|
return `Invalide date format ${preGame.date}`;
|
|
}
|
|
if (!this.TIME_VALIDATOR.test(preGame.time)) {
|
|
return `Invalide time format ${preGame.time}`;
|
|
}
|
|
if (!this.VALID_TEAMS.includes(preGame.away)) {
|
|
return `Invalide away format ${preGame.away}`;
|
|
}
|
|
if (!this.VALID_TEAMS.includes(preGame.home)) {
|
|
return `Invalide home format ${preGame.home}`;
|
|
}
|
|
if (preGame.home === preGame.away) {
|
|
return `Home and away teams must be different (received ${preGame.home} for both)`;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
async createNewGame(preGame: PreGame): Promise<[Game | null, string]> {
|
|
if (this.validatePregate(preGame) !== "") {
|
|
return [null, this.validatePregate(preGame)];
|
|
}
|
|
const game = this.createGame(preGame);
|
|
const dateIndex = this.dateIndex(game.date, game.time);
|
|
if (!dateIndex) {
|
|
return [null, "Cound not parse date or time"];
|
|
}
|
|
const precondition = await this.kv.get<string>(dateIndex);
|
|
if (precondition.value !== null) {
|
|
return [null, "game already exists " + precondition.value];
|
|
}
|
|
|
|
const res = await this.kv
|
|
.atomic()
|
|
.check(precondition)
|
|
.set(this.gamePrimaryKey(game.id), game)
|
|
.set(dateIndex, game.id)
|
|
.commit();
|
|
if (!res.ok) {
|
|
return [null, "Transaction failed"];
|
|
}
|
|
|
|
return [game, ""];
|
|
}
|
|
|
|
async getGameById(id: string): Promise<Game | null> {
|
|
const res = await this.kv.get<Game>(this.gamePrimaryKey(id));
|
|
return res.value;
|
|
}
|
|
|
|
async deleteGameById(id: string): Promise<boolean> {
|
|
const precond = await this.kv.get<Game>(this.gamePrimaryKey(id));
|
|
if (precond.value === null) {
|
|
return false;
|
|
}
|
|
|
|
const res = await this.kv
|
|
.atomic()
|
|
.check(precond)
|
|
.delete(this.gamePrimaryKey(precond.value.id))
|
|
.delete(this.dateIndex(precond.value.date, precond.value.time)!)
|
|
.commit();
|
|
return res.ok;
|
|
}
|
|
|
|
async updateGame(id: string, partial: Partial<Game>): Promise<boolean> {
|
|
delete partial.id;
|
|
const precond = await this.kv.get<Game>(this.gamePrimaryKey(id));
|
|
if (precond.value === null) {
|
|
return false;
|
|
}
|
|
const res = await this.kv
|
|
.atomic()
|
|
.check(precond)
|
|
.delete(this.gamePrimaryKey(precond.value.id))
|
|
.delete(this.dateIndex(precond.value.date, precond.value.time)!)
|
|
.set(
|
|
this.dateIndex(precond.value.date, precond.value.time)!,
|
|
precond.value.id,
|
|
)
|
|
.set(this.gamePrimaryKey(precond.value.id), {
|
|
...precond.value,
|
|
...partial,
|
|
})
|
|
.commit();
|
|
return res.ok;
|
|
}
|
|
}
|
|
|
|
Deno.test("saving simple games", async (t) => {
|
|
const kv = await Deno.openKv();
|
|
const service = new GameService(kv);
|
|
|
|
async function deleteAllKeys() {
|
|
const keys = kv.list({ prefix: [] });
|
|
for await (const entry of keys) {
|
|
await kv.delete(entry.key);
|
|
}
|
|
}
|
|
|
|
function validePreGame(seed: number): PreGame {
|
|
let year = ((645 * seed + 162) % 10000) + 1;
|
|
let month = ((335 * seed + 71) % 12) + 1;
|
|
let day = ((72625 * seed + 176) % 28) + 1;
|
|
return {
|
|
date: `${(year + "").padStart(4, "0")}-${(month + "").padStart(2, "0")}-${(day + "").padStart(2, "0")}`,
|
|
time: "18:45",
|
|
away: "jaune",
|
|
home: "rouge",
|
|
};
|
|
}
|
|
|
|
await deleteAllKeys();
|
|
|
|
await t.step("creating games", async () => {
|
|
const preGame = validePreGame(7276);
|
|
const [game, error] = await service.createNewGame(preGame);
|
|
assertEquals(error, "");
|
|
const ret = await service.getGameById(game!.id);
|
|
assertEquals(game, ret);
|
|
});
|
|
|
|
await deleteAllKeys();
|
|
|
|
await t.step("creating games with old date format", async () => {
|
|
const preGame = validePreGame(7276);
|
|
preGame.date = "2017-8-5";
|
|
const [game, error] = await service.createNewGame(preGame);
|
|
assertEquals(error, "");
|
|
assertEquals(game?.date, "2017-08-05");
|
|
});
|
|
|
|
await deleteAllKeys();
|
|
|
|
await t.step("creating games with old time format", async () => {
|
|
const preGame = validePreGame(7276);
|
|
preGame.time = "9:00";
|
|
const [game, error] = await service.createNewGame(preGame);
|
|
assertEquals(error, "");
|
|
assertEquals(game?.time, "09:00");
|
|
});
|
|
|
|
await deleteAllKeys();
|
|
|
|
await t.step(
|
|
"saving two games in the same timeslot should fail",
|
|
async () => {
|
|
const preGame = validePreGame(7276);
|
|
let [_, error] = await service.createNewGame(preGame);
|
|
assertEquals(error, "");
|
|
[_, error] = await service.createNewGame(preGame);
|
|
assertNotEquals(error, "");
|
|
},
|
|
);
|
|
|
|
await deleteAllKeys();
|
|
|
|
await t.step("saving games with bad fields", async () => {
|
|
let i = 0;
|
|
for (const key of Object.keys(validePreGame(7276))) {
|
|
let [_, error] = await service.createNewGame({
|
|
...validePreGame(7262 + i),
|
|
[key]: `Broken ${key}`,
|
|
});
|
|
assertNotEquals(error, "");
|
|
i++;
|
|
}
|
|
});
|
|
|
|
await t.step("saving games with the same home and away teams", async () => {
|
|
const preGame = validePreGame(7276);
|
|
preGame.home = preGame.away;
|
|
let [_, error] = await service.createNewGame(preGame);
|
|
assertNotEquals(error, "");
|
|
});
|
|
|
|
await deleteAllKeys();
|
|
|
|
await t.step("listing games", async () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
await service.createNewGame(validePreGame(666 + i));
|
|
}
|
|
assertEquals((await service.getAllGames()).length, 10);
|
|
});
|
|
|
|
await deleteAllKeys();
|
|
|
|
await t.step("deleting games", async () => {
|
|
let [game, error] = await service.createNewGame(validePreGame(8992));
|
|
await service.deleteGameById(game!.id);
|
|
assertEquals((await service.getAllGames()).length, 0);
|
|
|
|
[game, error] = await service.createNewGame(validePreGame(8992));
|
|
assertEquals(error, "");
|
|
});
|
|
|
|
await deleteAllKeys();
|
|
|
|
await t.step("updating game scores", async () => {
|
|
let [game, _] = await service.createNewGame(validePreGame(8992));
|
|
const ret = await service.updateGame(game!.id, {
|
|
homeScore: 10,
|
|
awayScore: 32,
|
|
});
|
|
assertEquals(ret, true);
|
|
game = await service.getGameById(game!.id);
|
|
assertEquals(game!.homeScore, 10);
|
|
assertEquals(game!.awayScore, 32);
|
|
});
|
|
|
|
await deleteAllKeys();
|
|
await kv.close();
|
|
});
|