Skip to content
This repository has been archived by the owner on Jul 31, 2024. It is now read-only.

Commit

Permalink
Merge pull request #19 from Vyctor661/games
Browse files Browse the repository at this point in the history
Games
  • Loading branch information
vycdev authored Jul 7, 2020
2 parents f7f2aed + 068af4e commit b379d8a
Show file tree
Hide file tree
Showing 17 changed files with 311 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ export const up = async (knex: Knex) => {
return knex.schema.createTable("games", table => {
table.integer("gameid").notNullable();
table.integer("userid").notNullable();
table.integer("date").notNullable();
table.bigInteger("date").notNullable();
table.integer("wpm").notNullable();
table.integer("uncorrectedwpm").notNullable();
table.integer("rawwpm").notNullable();
table.integer("accuracy").notNullable();
});
};
Expand Down
15 changes: 15 additions & 0 deletions packages/api/db/migrations/20200706102336_create_pbs_table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Knex from "knex";

export const up = async (knex: Knex) => {
return knex.schema.createTable("pbs", table => {
table.integer("userid").notNullable();
table.bigInteger("date").notNullable();
table.integer("wpm").notNullable();
table.integer("rawwpm").notNullable();
table.integer("accuracy").notNullable();
});
};

export const down = async (knex: Knex) => {
return knex.schema.dropTable("pbs");
};
2 changes: 1 addition & 1 deletion packages/api/knexfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const configs: Record<string, Config> = {

production: {
...options,
connection: process.env.DATABASE_URL
connection: process.env.DATABASE_URL + "?SSL=no-verify"
}
};

Expand Down
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"format": "prettier --write \"**/*.+(js|json|ts)\"",
"start:dev": "node -r ./register.js src/index.ts",
"dev": "nodemon",
"start": "node ./dist/index.js",
"start": "node ./dist/src/index.js",
"fresh-testdb": "dropdb --if-exists test-king-typer && createdb test-king-typer",
"test": "pnpm run fresh-testdb && cross-env NODE_ENV=test mocha -r ./register.js 'src/**/*.test.ts' --exit",
"build": "tsc "
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/modules/apiRouter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import Router from "./Router";

import authRouter from "./auth/router";
import gamesRouter from "./games/router";
import usersRouter from "./users/router";

const apiRouter = new Router({ prefix: "/api" });

apiRouter.use(authRouter);
apiRouter.use(gamesRouter);
apiRouter.use(usersRouter);

export default apiRouter.routes();
20 changes: 20 additions & 0 deletions packages/api/src/modules/games/actions/checkPB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Game from "../types/Game";
import knex from "../../../../db/knex";
import PB from "../types/PB";

export default async (rawGame: Game) => {
const { gameid: _, ...game } = rawGame;
const playerPBs = await knex<PB>("pbs").where({ userid: game.userid });
if (playerPBs.length === 0) {
await knex<PB>("pbs").insert(game);
return true;
}
const playerPB = playerPBs.reduce((acc, cur) =>
acc.wpm > cur.wpm ? acc : cur
);
if (game.wpm > playerPB.wpm) {
await knex<PB>("pbs").insert(game);
return true;
}
return false;
};
20 changes: 20 additions & 0 deletions packages/api/src/modules/games/actions/createGame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Game from "../types/Game";
import { highestGameId } from "./highestGameId";
import knex from "../../../../db/knex";

export const createGame = async (
userid: number,
wpm: number,
rawwpm: number,
accuracy: number
): Promise<Game> => {
const newGame = {
gameid: (await highestGameId()) + 1,
userid,
wpm,
rawwpm,
accuracy,
date: Date.now()
};
return (await knex<Game>("games").insert(newGame, "*"))[0];
};
7 changes: 7 additions & 0 deletions packages/api/src/modules/games/actions/getPB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import PB from "../types/PB";
import knex from "../../../../db/knex";

export default async (userid: number) => {
const pbs = await knex<PB>("pbs").where({ userid });
return pbs.length !== 0 ? pbs.sort((a, b) => a.wpm - b.wpm) : null;
};
9 changes: 9 additions & 0 deletions packages/api/src/modules/games/actions/highestGameId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import knex from "../../../../db/knex";
import Game from "../types/Game";

export const highestGameId = async () => {
const games = await knex<Game>("games");
const gameIds = games.map(l => l.gameid);
const highestGameId = gameIds.length === 0 ? 1 : Math.max(...gameIds);
return highestGameId;
};
14 changes: 14 additions & 0 deletions packages/api/src/modules/games/actions/removeOldGame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Game from "../types/Game";
import knex from "../../../../db/knex";

export const removeOldGame = async (userid: number): Promise<void> => {
const userGames = await knex<Game>("games").where({ userid });
if (userGames.length >= 11) {
const removalGames = userGames.slice(0, -10);
for (const game of removalGames) {
await knex<Game>("games")
.delete()
.where({ gameid: game.gameid });
}
}
};
66 changes: 66 additions & 0 deletions packages/api/src/modules/games/router.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import request from "supertest";
import { expect } from "chai";

import { server } from "../../index";
import users from "../../../db/seeds/examples/users";
import { createGame } from "./actions/createGame";
import knex from "../../../db/knex";
import Game from "./types/Game";
import { removeOldGame } from "./actions/removeOldGame";

const agent = request.agent(server);

const newGame = {
userid: 1,
wpm: 90,
rawwpm: 100,
accuracy: 90,
seconds: 60
};

describe("Game routes", async () => {
// We don't need to rerun migrations or seeds because we did in the auth route

it("Creates a game", async function() {
this.timeout(5000);
const { email, password } = users[0];

await agent.post(`/api/auth/login`).send({ email, password });

const response = await agent
.post(`/api/games/newGame/`)
.send(newGame)
.set("Accept", "application/text")
.expect("Content-Type", /text/)
.expect(201);

expect(response.text).to.deep.equal("Successfully created a game!");
});

it("Deletes games past 10 games", async function() {
this.timeout(5000);

await createGame(13, 0, 0, 0);
await createGame(13, 0, 0, 0);
await createGame(13, 0, 0, 0);
await createGame(13, 0, 0, 0);
await createGame(13, 0, 0, 0);
await createGame(13, 0, 0, 0);
await createGame(13, 0, 0, 0);
await createGame(13, 0, 0, 0);
await createGame(13, 0, 0, 0);
await createGame(13, 0, 0, 0);
await createGame(13, 0, 0, 0);
await createGame(13, 0, 0, 0);

await Promise.all([
removeOldGame(13),
removeOldGame(13),
removeOldGame(13)
]);

const games = await knex<Game>("games").where({ userid: 13 });

expect(games.length).to.equal(10);
});
});
20 changes: 20 additions & 0 deletions packages/api/src/modules/games/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Router from "../Router";
import { requireAuthenticated } from "../auth/middleware/requireAuthenticated";
import { createGame } from "./actions/createGame";
import { removeOldGame } from "./actions/removeOldGame";
import checkPB from "./actions/checkPB";

const router = new Router({ prefix: "/games" });

router.post("/newGame", requireAuthenticated(), async (ctx, next) => {
const { wpm, rawwpm, accuracy } = ctx.request.body;
const { user } = ctx.session!;
const newGame = await createGame(user, wpm, rawwpm, accuracy);
await removeOldGame(user);
await checkPB(newGame);
ctx.status = 201;
ctx.body = "Successfully created a game!";
await next();
});

export default router.routes();
8 changes: 8 additions & 0 deletions packages/api/src/modules/games/types/Game.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default interface Game {
gameid: number;
userid: number;
date: number;
wpm: number;
rawwpm: number;
accuracy: number;
}
7 changes: 7 additions & 0 deletions packages/api/src/modules/games/types/PB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default interface PB {
userid: number;
date: number;
wpm: number;
rawwpm: number;
accuracy: number;
}
18 changes: 18 additions & 0 deletions packages/api/src/modules/users/actions/userGames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import User from "../types/User";
import Game from "../../games/types/Game";
import knex from "../../../../db/knex";

export default async <T extends keyof Omit<User, "password">>(
property: T,
value: User[T]
) => {
if (!property || !value) return null;
const result = await knex<User>("users")
.select()
.first()
.where({ [property]: value });
if (!result) return null;

const userGames = await knex<Game>("games").where({ userid: result.id });
return userGames;
};
47 changes: 47 additions & 0 deletions packages/api/src/modules/users/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import request from "supertest";
import { expect } from "chai";

import { server } from "../../index";
import knex from "../../../db/knex";

const agent = request.agent(server);

Expand Down Expand Up @@ -42,4 +43,50 @@ describe("Users routes", async () => {
message: "That username seems to be already taken"
});
});

describe("Game stats", async () => {
before(async () => {
await agent
.post("/api/games/newGame")
.send({ wpm: 60, rawwpm: 80, accuracy: 75 });

await agent
.post("/api/games/newGame")
.send({ wpm: 90, rawwpm: 100, accuracy: 90 });
});

it("Gets the games of a user", async () => {
const response = await agent
.get(`/api/users/userGames/`)
.set("Accept", "application/json")
.expect("Content-Type", /json/)
.expect(200);

expect(response.body.games.length).to.equal(2);
});

it("Gets the game stats of a user", async () => {
const response = await agent
.get(`/api/users/userGameStats/5`)
.set("Accept", "application/json")
.expect("Content-Type", /json/)
.expect(200);

expect(response.body).to.deep.equal({
averageAccuracy: 82.5,
averageWPM: 75,
averageRawWPM: 90
});
});

it("Gets the PB of a user", async () => {
const response = await agent
.get(`/api/users/userPBs/5`)
.set("Accept", "application/json")
.expect("Content-Type", /json/)
.expect(200);

expect(response.body[0].wpm).to.equal(60);
});
});
});
54 changes: 54 additions & 0 deletions packages/api/src/modules/users/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { HttpError } from "../../common/error/classes/httpError";
import { validateSchema } from "../schema/middleware/validateSchema";
import { registerBody } from "./schema/registerBody";
import { RegisterBody } from "./types/RegisterBody";
import { requireAuthenticated } from "../auth/middleware/requireAuthenticated";
import userGames from "./actions/userGames";
import getPBs from "../games/actions/getPB";

const router = new Router({ prefix: "/users" });

Expand All @@ -27,4 +30,55 @@ router.post(
}
);

router.get("/userGames", requireAuthenticated(), async (ctx, next) => {
const { user } = ctx.session!;

const games = await userGames("id", user);

ctx.status = 200;
ctx.body = { games };

await next();
});

router.get("/userGameStats/:id", async (ctx, next) => {
const { id } = ctx.params;

const games = await userGames("id", id);

if (!games || games.length === 0) {
ctx.status = 400;
return (ctx.body = "No user with that ID exists!");
}

const averageWPM =
games.map(l => l.wpm).reduce((acc, cur) => acc + cur) / games.length;
const averageRawWPM =
games.map(l => l.rawwpm).reduce((acc, cur) => acc + cur) / games.length;
const averageAccuracy =
games.map(l => l.accuracy).reduce((acc, cur) => acc + cur) /
games.length;

ctx.status = 200;
ctx.body = { averageAccuracy, averageRawWPM, averageWPM };

await next();
});

router.get("/userPBs/:id", async (ctx, next) => {
const { id } = ctx.params;

const game = await getPBs(id);

if (!game) {
ctx.status = 400;
return (ctx.body = "That user does not have any PBs!");
}

ctx.status = 200;
ctx.body = game;

await next();
});

export default router.routes();

0 comments on commit b379d8a

Please sign in to comment.