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 = /^(?\d{4})-(?\d{1,2})-(?\d{1,2})$/; private readonly TIME_VALIDATOR = /^(?\d{1,2})(:|h)(?\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 { const ite = this.kv.list({ 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(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 { const res = await this.kv.get(this.gamePrimaryKey(id)); return res.value; } async deleteGameById(id: string): Promise { const precond = await this.kv.get(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 switchVisitors(id: string): Promise { const game = await this.getGameById(id); if (game === null) { return false; } game.id = ""; const [home, away] = [game.home, game.away]; const [homeScore, awayScore] = [game.homeScore, game.awayScore]; game.home = away; game.away = home; game.homeScore = awayScore; game.awayScore = homeScore; console.log(`game ${game}`); return this.updateGame(id, game); } async updateGame(id: string, partial: Partial): Promise { delete partial.id; const precond = await this.kv.get(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(); });