diff --git a/src/controllers/kadiUserController.ts b/src/Controllers/kadiUserController.ts similarity index 82% rename from src/controllers/kadiUserController.ts rename to src/Controllers/kadiUserController.ts index 5c64769..20a8851 100755 --- a/src/controllers/kadiUserController.ts +++ b/src/Controllers/kadiUserController.ts @@ -27,7 +27,7 @@ export const changeLang: RequestHandler = async (req, res) => { export const addGuest: RequestHandler = async (req, res) => { const user = (req.user as KadiUser); if (req.body.guestName) { - const newGuest: Player = await KadiUserCollection.addGuestForUser(user, req.body.guestName); + const newGuest: Player = await KadiUserCollection().addGuestForUser(user, req.body.guestName); res.send({ username: user.getUsername(), userId: user.getId(), @@ -47,9 +47,9 @@ export const updateGuest: RequestHandler = async (req, res) => { const {id: guestId} = req.params; if (req.body.newName) { const {newName} = req.body; - const guest = await PlayerCollection.read(guestId); + const guest = await PlayerCollection().read(guestId); guest.setNick(newName); - const updatedGuest = await PlayerCollection.save(guest); + const updatedGuest = await PlayerCollection().save(guest); res.status(200).send({ userId: user.getId(), username: user.getUsername(), @@ -67,7 +67,7 @@ export const updateGuest: RequestHandler = async (req, res) => { export const getGuest: RequestHandler = async (req, res) => { const user = (req.user as KadiUser); const {id: guestId} = req.params; - const guest = await PlayerCollection.read(guestId); + const guest = await PlayerCollection().read(guestId); res.status(200).send({ userId: user.getId(), username: user.getUsername(), @@ -78,7 +78,7 @@ export const getGuest: RequestHandler = async (req, res) => { export const deleteGuest: RequestHandler = async (req, res) => { const user = (req.user as KadiUser); const {id: guestId} = req.params; - const deletedGuest = await KadiUserCollection.deleteGuestFromUser(user, guestId); + const deletedGuest = await KadiUserCollection().deleteGuestFromUser(user, guestId); res.status(200).send({ userId: user.getId(), username: user.getUsername(), @@ -88,7 +88,7 @@ export const deleteGuest: RequestHandler = async (req, res) => { export const getGuests: RequestHandler = async (req, res) => { const user = (req.user as KadiUser); - const guests = await KadiUserCollection.getAllGuestsForUser(user); + const guests = await KadiUserCollection().getAllGuestsForUser(user); res.status(200).send({ userId: user.getId(), username: user.getUsername(), @@ -98,7 +98,7 @@ export const getGuests: RequestHandler = async (req, res) => { export const getAllPlayersAssociatedWithAccount: RequestHandler = async (req, res) => { const user = (req.user as KadiUser); - const guests = await KadiUserCollection.getAllGuestsForUser(user); - const mainPlayer = await KadiUserCollection.getMainPlayerForUser(user); + const guests = await KadiUserCollection().getAllGuestsForUser(user); + const mainPlayer = await KadiUserCollection().getMainPlayerForUser(user); res.status(200).send({guests, mainPlayer}); }; \ No newline at end of file diff --git a/src/Controllers/rulesetController.ts b/src/Controllers/rulesetController.ts new file mode 100644 index 0000000..e6aa69f --- /dev/null +++ b/src/Controllers/rulesetController.ts @@ -0,0 +1,7 @@ +import {RequestHandler} from "express"; +import RulesetCollection from "../ObjectCollections/RulesetCollection"; + +export const getRuleset: RequestHandler = async (req, res) => { + const ruleset = await RulesetCollection().read(req.params.id); + res.json(ruleset.getSchemaJSON()); +}; \ No newline at end of file diff --git a/src/controllers/signupController.ts b/src/Controllers/signupController.ts similarity index 91% rename from src/controllers/signupController.ts rename to src/Controllers/signupController.ts index 7d442ac..1d17078 100755 --- a/src/controllers/signupController.ts +++ b/src/Controllers/signupController.ts @@ -24,7 +24,7 @@ export const showRegistrationPage: RequestHandler = (req, res) => { export const registerNewUser: RequestHandler = async (req, res) => { try { const loginDetails: LoginDetails = req.body as LoginDetails; - const newUser = await KadiUserCollection.registerUser(loginDetails); + const newUser = await KadiUserCollection().registerUser(loginDetails); req.login(newUser, (err) => { if (err) { throw err; @@ -54,7 +54,7 @@ export const logoutUser: RequestHandler = (req, res) => { }; export const authenticateKadiUser: VerifyFunction = async (email, password, done) => { - const user = await KadiUserCollection.findByEmail(email); + const user = await KadiUserCollection().findByEmail(email); if (!user) { return done(null, false, { message: "A user with that email does not exist."} ); } @@ -75,6 +75,6 @@ export async function serializeKadiUser(user: KadiUser, done: (err: any, id?: un } export async function deserializeKadiUser(id: string, done: (err: any, id?: unknown) => void): Promise { - const user: KadiUser | null = await KadiUserCollection.getSerializedAuthUser(id); + const user: KadiUser | null = await KadiUserCollection().read(id); done(null, user); } \ No newline at end of file diff --git a/src/controllers/statsController.ts b/src/Controllers/statsController.ts similarity index 56% rename from src/controllers/statsController.ts rename to src/Controllers/statsController.ts index 533307b..3cb8c55 100755 --- a/src/controllers/statsController.ts +++ b/src/Controllers/statsController.ts @@ -9,12 +9,18 @@ import RulesetCollection from "../ObjectCollections/RulesetCollection"; import Ruleset from "../Objects/Ruleset"; import {OutcomeType} from "../Objects/DefaultStatsMongoData"; -export interface GameSubmission { - rulesetId: string; +interface GameSubmission { + ruleset: string; players: { id: string; nick: string }[]; results: PlayerGameResult[]; } +export interface ProcessedGameSubmission { + ruleset: string; + players: {id: string, nick: string}[]; + results: ScoredResultsWithOutcome[]; +} + type PlayerGameResult = { playerId: string; blocks: Record }; type Block = { cells: Record }; type Cell = { value: CellValue }; @@ -27,11 +33,11 @@ enum ResultType { } type ScoredResults = {score: number, results: PlayerGameResult}; -type ScoredResultsWithOutcome = {score: number, results: PlayerGameResult & {outcome: OutcomeType}}; +export type ScoredResultsWithOutcome = {score: number, results: PlayerGameResult & {outcome: OutcomeType}}; export const listGames: RequestHandler = async (req, res) => { const user = req.user as KadiUser; - const gamesList = await KadiUserCollection.getSavedGamesForUser(user); + const gamesList = await KadiUserCollection().getSavedGamesForUser(user); if (gamesList) { res.json({ games: gamesList }); } @@ -47,17 +53,32 @@ export const saveGame: RequestHandler = async (req, res) => { if (newGuests.length > 0) { fillOutSubmissionWithNewIds(submission, newGuests); } - const newGame = await KadiUserCollection.addGameForUser(user, submission); - processStats(await RulesetCollection.read(submission.rulesetId), submission.results, user); + const rulesetUsed = await RulesetCollection().read(submission.ruleset); + const scoredResultsWithOutcomes = await processStats(rulesetUsed, submission.results, user); + const newGame = await KadiUserCollection().addGameForUser(user, {...submission, results: scoredResultsWithOutcomes}); res.send({ message: "Game submitted successfully!", newGame: newGame }); }; +export const getStats: RequestHandler = async (req, res) => { + const user = req.user as KadiUser; + const stats = await KadiUserCollection().getAllStatsForUser(user); + if (stats) { + res.json({ + pStats: stats.pStats.map(pStats => ({...pStats, stats: pStats.stats.getData()})), + accStats: stats.accStats.getData() + }); + } + else { + res.sendStatus(404); + } +}; + async function addNewGuests(submission: GameSubmission, user: KadiUser): Promise { const newGuestIds: Player[] = []; for (const playerDetails of submission.players) { const isNewPlayer = playerDetails.id === playerDetails.nick; if (isNewPlayer) { - const newGuest: Player = await KadiUserCollection.addGuestForUser(user, playerDetails.nick); + const newGuest: Player = await KadiUserCollection().addGuestForUser(user, playerDetails.nick); newGuestIds.push(newGuest); } } @@ -68,17 +89,17 @@ function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: P for (const newGuest of newGuestList) { const gameResultsFromNewGuest = submission.results.find((result) => result.playerId === newGuest.getNick()); if (gameResultsFromNewGuest) { - gameResultsFromNewGuest.playerId = newGuest.getId(); + gameResultsFromNewGuest.playerId = newGuest.getId().toString(); } const playerEntryForNewGuest = submission.players.find((player) => player.id === newGuest.getNick()); if (playerEntryForNewGuest) { - playerEntryForNewGuest.id = newGuest.getId(); + playerEntryForNewGuest.id = newGuest.getId().toString(); } } return submission; } -async function processStats(ruleset: Ruleset, results: PlayerGameResult[], account: KadiUser) { +async function processStats(ruleset: Ruleset, results: PlayerGameResult[], account: KadiUser): Promise { const calc = new ScoreCalculator(ruleset); let playerScoreList: ScoredResults[] = []; for (const result of results) { @@ -88,59 +109,60 @@ async function processStats(ruleset: Ruleset, results: PlayerGameResult[], accou results: result }); } - const playerScoreListWithOutcomes = updateScoreListWithOutcomes(playerScoreList, ruleset); - updateStatsForIndividualPlayers(playerScoreListWithOutcomes, ruleset); + const playerScoreListWithOutcomes = updateScoreListWithOutcomes(playerScoreList); + await updateStatsForIndividualPlayers(playerScoreListWithOutcomes, ruleset); const gameResults = playerScoreListWithOutcomes.map(scoredResults => scoredResults.results); - await KadiUserCollection.updateAccountStats(account.getId(), gameResults, ruleset); + await KadiUserCollection().updateAccountStats(account.getId(), gameResults, ruleset); + return playerScoreListWithOutcomes; } -function updateScoreListWithOutcomes(playerScoreList: ScoredResults[], rulesetUsed: Ruleset): ScoredResultsWithOutcome[] { - playerScoreList = sortDescendingByScore(playerScoreList); - const playerScoreListWithOutcomes: ScoredResultsWithOutcome[] = playerScoreList.map(scoredResults => { +function updateScoreListWithOutcomes(scoreResultsList: ScoredResults[]): ScoredResultsWithOutcome[] { + scoreResultsList = sortDescendingByScore(scoreResultsList); + const playerScoreListWithOutcomes: ScoredResultsWithOutcome[] = scoreResultsList.map(scoredResults => { const newResults = {...scoredResults.results, outcome: OutcomeType.loss}; return {...scoredResults, results: newResults}; }); let runnerUpsStart: number; if (playerScoreListWithOutcomes[0].score !== playerScoreListWithOutcomes[1].score) { playerScoreListWithOutcomes[0].results.outcome = OutcomeType.win; - runnerUpsStart = 1; } else { - runnerUpsStart = updateScoreListWithDraws(playerScoreListWithOutcomes, rulesetUsed); + runnerUpsStart = updateScoreListWithDraws(playerScoreListWithOutcomes); } - const losersStart = updateScoreListWithRunnerUps(playerScoreListWithOutcomes.slice(runnerUpsStart), rulesetUsed); - updateScoreListWithLosses(playerScoreListWithOutcomes.slice(losersStart), rulesetUsed); + const losersStart = updateScoreListWithRunnerUps(playerScoreListWithOutcomes, runnerUpsStart); + updateScoreListWithLosses(playerScoreListWithOutcomes, losersStart); return playerScoreListWithOutcomes; } -function updateScoreListWithDraws(playerScoreList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): number { - for (let i = 0; i < playerScoreList.length; i++) { - if (playerScoreList[i].score === playerScoreList[0].score) { - playerScoreList[i].results.outcome = OutcomeType.draw; +function updateScoreListWithDraws(scoreResultsList: ScoredResultsWithOutcome[]): number { + for (let i = 0; i < scoreResultsList.length; i++) { + if (scoreResultsList[i].score === scoreResultsList[0].score) { + scoreResultsList[i].results.outcome = OutcomeType.draw; } else { return i; } } - return playerScoreList.length; + return scoreResultsList.length; } -function updateScoreListWithRunnerUps(playerScoreList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): number { - for (let i = 0; i < playerScoreList.length; i++) { - if (playerScoreList[i].score === playerScoreList[0].score) { - playerScoreList[i].results.outcome = OutcomeType.runnerUp; +function updateScoreListWithRunnerUps(scoreResultsList: ScoredResultsWithOutcome[], runnerUpsStartIndex: number): number { + scoreResultsList[runnerUpsStartIndex].results.outcome = OutcomeType.runnerUp; + for (let i = runnerUpsStartIndex + 1; i < scoreResultsList.length; i++) { + if (scoreResultsList[i].score === scoreResultsList[i - 1].score) { + scoreResultsList[i].results.outcome = OutcomeType.runnerUp; } else { return i; } } - return playerScoreList.length; + return scoreResultsList.length; } -function updateScoreListWithLosses(scoreResultsList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset) { - for (const lostPlayerResults of scoreResultsList) { - lostPlayerResults.results.outcome = OutcomeType.loss; +function updateScoreListWithLosses(scoreResultsList: ScoredResultsWithOutcome[], losersStartIndex: number) { + for (let i = losersStartIndex; i < scoreResultsList.length; i++) { + scoreResultsList[i].results.outcome = OutcomeType.loss; } } @@ -148,9 +170,9 @@ function sortDescendingByScore(playerScoreList: ScoredResults[]) { return playerScoreList.sort((a, b) => b.score - a.score); } -function updateStatsForIndividualPlayers(playerScoreListWithOutcomes: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): void { +async function updateStatsForIndividualPlayers(playerScoreListWithOutcomes: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): Promise { for (const scoredResults of playerScoreListWithOutcomes) { - PlayerCollection.updateStatsForPlayer( + await PlayerCollection().updateStatsForPlayer( scoredResults.results.playerId, {...scoredResults.results, outcome: scoredResults.results.outcome}, rulesetUsed); diff --git a/src/ObjectCollections/CollectionRegistry.ts b/src/ObjectCollections/CollectionRegistry.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/ObjectCollections/CollectionRegistry.ts @@ -0,0 +1 @@ + diff --git a/src/ObjectCollections/KadiUserCollection.ts b/src/ObjectCollections/KadiUserCollection.ts index ddc8ec0..6b57122 100644 --- a/src/ObjectCollections/KadiUserCollection.ts +++ b/src/ObjectCollections/KadiUserCollection.ts @@ -1,5 +1,4 @@ -import {CredentialsTakenError, GenericPersistenceError} from "../errors"; -import mongo from "mongodb"; +import {CredentialsTakenError} from "../errors"; import bcrypt from "bcrypt"; import {SupportedLang} from "../enums"; import {AccountStatsMongoData, defaultAccountStatsMongoData, OutcomeType, PlayerGameResults} from "../Objects/DefaultStatsMongoData"; @@ -11,13 +10,14 @@ import SavedGameCollection from "./SavedGameCollection"; import SavedGame from "../Objects/SavedGame"; import PlayerCollection from "../ObjectCollections/PlayerCollection"; import Player from "../Objects/Player"; -import {GameSubmission} from "../controllers/statsController"; +import {ProcessedGameSubmission} from "../Controllers/statsController"; import Ruleset from "../Objects/Ruleset"; import RulesetCollection from "./RulesetCollection"; import AccountStats from "../Objects/AccountStats"; +import PlayerStats from "../Objects/PlayerStats"; export interface KadiUserMongoData { - id: string; + id: ActiveRecordId; username: string; email: string; password: string; @@ -30,8 +30,20 @@ export interface KadiUserMongoData { } class KadiUserCollection extends MongoStoredObjectCollection { - constructor(collectionClient: mongo.Collection) { - super(collectionClient); + private static instance?: KadiUserCollection; + private constructor() { + super(); + } + + static getInstance(): KadiUserCollection { + if (KadiUserCollection.instance === undefined) { + KadiUserCollection.instance = new KadiUserCollection(); + } + return KadiUserCollection.instance; + } + + async init() { + this.mongoDbClientCollection = getMongoObjectCollection("users") } private kadiUserFrom(data: KadiUserMongoData): KadiUser { @@ -70,7 +82,7 @@ class KadiUserCollection extends MongoStoredObjectCollection } private async addNewUser(loginDetails: LoginDetails): Promise { - const newPlayer = await PlayerCollection.create(loginDetails.username); + const newPlayer = await PlayerCollection().create(loginDetails.username); const securePassword = await this.makePasswordSecure(loginDetails.password); const newUser = await this.mongoCreate({ username: loginDetails.username, @@ -96,58 +108,52 @@ class KadiUserCollection extends MongoStoredObjectCollection return object !== null; } - async getSerializedAuthUser(id: string): Promise { - const foundUser = await this.mongoRead(id); - if (foundUser) { - return this.kadiUserFrom(foundUser); - } - else { - throw new GenericPersistenceError("User not found!"); - } - } - async makePasswordSecure(password: string): Promise { return bcrypt.hash(password, 10); } async addGuestForUser(userOrId: OrId, newGuestNick: string): Promise { - const newGuest = await PlayerCollection.create(newGuestNick); - await this.mongoDbClientCollection.findOneAndUpdate( + const newGuest = await PlayerCollection().create(newGuestNick); + await this.mongoDbClientCollection!.findOneAndUpdate( {_id: this.idFromRecordOrId(userOrId)}, {$push: {guests: newGuest.getId()}}); return newGuest; } async deleteGuestFromUser(userOrId: OrId, guestOrGuestId: OrId): Promise { - const deletedGuest = await PlayerCollection.delete(this.idFromRecordOrId(guestOrGuestId)); - await this.mongoDbClientCollection.findOneAndUpdate( + const deletedGuest = await PlayerCollection().delete(this.idFromRecordOrId(guestOrGuestId)); + await this.mongoDbClientCollection!.findOneAndUpdate( {_id: this.idFromRecordOrId(userOrId)}, - {$pull: {guests: this.idFromRecordOrId(guestOrGuestId)}}); + {$pull: {guests: this.idFromRecordOrId(deletedGuest)}}); return deletedGuest; } - async getAllGuestsForUser(userOrId: OrId): Promise[]> { + async getAllGuestsForUser(userOrId: OrId): Promise { const guestIdList = (await this.mongoRead(this.idFromRecordOrId(userOrId)))?.guests; - return guestIdList.map(async (guestId) => { - return await PlayerCollection.read(guestId); - }); + return Promise.all( + guestIdList.map(async (guestId) => { + return await PlayerCollection().read(guestId); + } + )); } async getMainPlayerForUser(userOrId: OrId): Promise { const userData = await this.mongoRead(this.idFromRecordOrId(userOrId)); - return PlayerCollection.read(userData?.player); + return PlayerCollection().read(userData?.player); } - async getSavedGamesForUser(userOrId: OrId): Promise[]> { + async getSavedGamesForUser(userOrId: OrId): Promise { const savedGameIds = (await this.mongoRead(this.idFromRecordOrId(userOrId)))?.savedGames; - return savedGameIds.map(async (savedGameId) => { - return await SavedGameCollection.read(savedGameId); - }); + return Promise.all( + savedGameIds.map(async (savedGameId) => { + return await SavedGameCollection().read(savedGameId); + } + )); } - async addGameForUser(userOrId: OrId, gameSubmission: GameSubmission): Promise { - const newGame = await SavedGameCollection.create(gameSubmission); - await this.mongoDbClientCollection.findOneAndUpdate( + async addGameForUser(userOrId: OrId, submission: ProcessedGameSubmission): Promise { + const newGame = await SavedGameCollection().create(submission); + await this.mongoDbClientCollection!.findOneAndUpdate( {_id: this.idFromRecordOrId(userOrId)}, {$push: {savedGames: newGame.getId()}}); return newGame; @@ -155,16 +161,36 @@ class KadiUserCollection extends MongoStoredObjectCollection async updateAccountStats(userOrId: OrId, gameResults: (PlayerGameResults & {outcome: OutcomeType})[], rulesetUsedOrId: OrId): Promise { const userId = this.idFromRecordOrId(userOrId); - const rulesetUsed = rulesetUsedOrId instanceof Ruleset ? rulesetUsedOrId : await RulesetCollection.read(rulesetUsedOrId); - const accountStatsMongoData = await this.mongoRead(userId); - const accountStatsObject = new AccountStats(accountStatsMongoData.accountStats); + const rulesetUsed = rulesetUsedOrId instanceof Ruleset ? rulesetUsedOrId : await RulesetCollection().read(rulesetUsedOrId); + const accountStatsMongoData = (await this.mongoRead(userId)).accountStats; + const accountStatsObject = new AccountStats(accountStatsMongoData); accountStatsObject.updateStats(gameResults, rulesetUsed); this.mongoUpdate({ - id: this.idFromRecordOrId(userId), + id: userId, accountStats: accountStatsObject.getData() }); } + + async getAllStatsForUser(userOrId: OrId): Promise { + const players = [...(await this.getAllGuestsForUser(userOrId)), await this.getMainPlayerForUser(userOrId)]; + const playerStats = players.map(player => ({ + nick: player.getNick(), + playerId: player.getId(), + stats: player.getStats() + })); + const accountStatsMongoData = (await this.mongoRead(this.idFromRecordOrId(userOrId))).accountStats; + const accountStats = new AccountStats(accountStatsMongoData); + return {pStats: playerStats, accStats: accountStats}; + } } -const KadiUserCollectionSingleton = new KadiUserCollection(getMongoObjectCollection("users")); -export default KadiUserCollectionSingleton; \ No newline at end of file +export interface StatsListing { + accStats: AccountStats; + pStats: { + nick: string, + playerId: ActiveRecordId, + stats: PlayerStats + }[]; +} + +export default KadiUserCollection.getInstance; \ No newline at end of file diff --git a/src/ObjectCollections/MongoStoredObjectCollection.ts b/src/ObjectCollections/MongoStoredObjectCollection.ts index d1c28ff..a474093 100644 --- a/src/ObjectCollections/MongoStoredObjectCollection.ts +++ b/src/ObjectCollections/MongoStoredObjectCollection.ts @@ -5,35 +5,47 @@ import ActiveRecord, {ActiveRecordId} from "../Objects/ActiveRecord"; abstract class MongoStoredObjectCollection { + protected mongoDbClientCollection?: mongo.Collection; + protected constructor() {} - protected constructor(protected mongoDbClientCollection: mongo.Collection) {} + abstract init(): Promise; protected async mongoCreate(objectData: Omit): Promise { return tryQuery(async () => { - const insertOneWriteOpResult = await this.mongoDbClientCollection.insertOne(objectData); + const insertOneWriteOpResult = await this.mongoDbClientCollection!.insertOne(objectData); if (insertOneWriteOpResult.result.ok === 1) { - return insertOneWriteOpResult.ops[0] + const newObject = insertOneWriteOpResult.ops[0]; + newObject.id = newObject._id; + newObject._id = undefined; + return insertOneWriteOpResult.ops[0]; } else { throw new MongoError(`Error creating the object: ${JSON.stringify(objectData)}`); } }); } - protected async mongoRead(id: string): Promise { + protected async mongoRead(id: ActiveRecordId): Promise { return tryQuery(async () => { - const result = await this.mongoDbClientCollection.findOne({_id: id}); + const result = await this.mongoDbClientCollection!.findOne({_id: new mongo.ObjectID(id)}); if (result) { + result.id = result._id; + result._id = undefined; return result; } else { - throw new InvalidIdError(`Object in collection "${typeof this}" with id ${JSON.stringify(id)} not found!`); + throw new InvalidIdError(`Object in collection "${this.constructor.name}" with id ${JSON.stringify(id)} not found!`); } }); } protected async mongoFindByAttribute(attribute: string, value: any): Promise { - return tryQuery(async () => - await this.mongoDbClientCollection.findOne({[attribute]: value}) - ); + return tryQuery(async () => { + const result = await this.mongoDbClientCollection!.findOne({[attribute]: value}); + if (result) { + result.id = result._id; + result._id = undefined; + } + return result; + }); } protected async mongoDelete(objectId: ActiveRecordId, returnObject?: boolean): Promise { @@ -41,7 +53,7 @@ abstract class MongoStoredObjectCollection & {id: ActiveRecordId}) { + protected async mongoUpdate(object: Partial) { await tryQuery(() => - this.mongoDbClientCollection.findOneAndUpdate({_id: object.id}, {$set: {...object, id: undefined}}) + this.mongoDbClientCollection!.findOneAndUpdate({_id: object.id}, {$set: {...object, id: undefined}}) ); } protected idFromRecordOrId(recordOrRecordId: T | ActiveRecordId): ActiveRecordId { - return typeof recordOrRecordId === "string" ? recordOrRecordId : recordOrRecordId.getId(); + return recordOrRecordId instanceof mongo.ObjectId || typeof recordOrRecordId === "string" ? + recordOrRecordId : + recordOrRecordId.getId(); } } - export default MongoStoredObjectCollection; \ No newline at end of file diff --git a/src/ObjectCollections/PlayerCollection.ts b/src/ObjectCollections/PlayerCollection.ts index 9560ad2..4d7992c 100644 --- a/src/ObjectCollections/PlayerCollection.ts +++ b/src/ObjectCollections/PlayerCollection.ts @@ -1,5 +1,4 @@ import MongoStoredObjectCollection from "./MongoStoredObjectCollection"; -import mongo from "mongodb"; import { defaultPlayerStatsMongoData, OutcomeType, @@ -14,18 +13,30 @@ import Ruleset from "../Objects/Ruleset"; import RulesetCollection from "./RulesetCollection"; export interface PlayerMongoData { - id: string; + id: ActiveRecordId; nick: string; - stats?: PlayerStatsMongoData; + stats: PlayerStatsMongoData; } class PlayerCollection extends MongoStoredObjectCollection { - constructor(collectionClient: mongo.Collection) { - super(collectionClient); + private static instance?: PlayerCollection; + constructor() { + super(); + } + + static getInstance(): PlayerCollection { + if (PlayerCollection.instance === undefined) { + PlayerCollection.instance = new PlayerCollection(); + } + return PlayerCollection.instance; + } + + async init() { + this.mongoDbClientCollection = getMongoObjectCollection("players"); } private playerFrom(data: PlayerMongoData): Player { - return new Player(data.id, data.nick, data.stats ? new PlayerStats(data.stats) : undefined); + return new Player(data.id, data.nick, new PlayerStats(data.stats)); } async create(nick: string): Promise { @@ -47,16 +58,15 @@ class PlayerCollection extends MongoStoredObjectCollection { } async updateStatsForPlayer(playerOrId: OrId, gameResults: PlayerGameResults & {outcome: OutcomeType}, rulesetUsedOrId: OrId): Promise { - playerOrId = playerOrId instanceof Player ? playerOrId : await this.read(playerOrId); - rulesetUsedOrId = rulesetUsedOrId instanceof Ruleset ? rulesetUsedOrId : await RulesetCollection.read(rulesetUsedOrId); - playerOrId.updateStats(gameResults, rulesetUsedOrId); + const player = playerOrId instanceof Player ? playerOrId : await this.read(playerOrId); + const ruleset = rulesetUsedOrId instanceof Ruleset ? rulesetUsedOrId : await RulesetCollection().read(rulesetUsedOrId); + player.updateStats(gameResults, ruleset); this.mongoUpdate({ - id: this.idFromRecordOrId(playerOrId), - stats: playerOrId.getStats()?.getData() + id: this.idFromRecordOrId(player), + stats: player.getStats()?.getData() }); } } -const PlayerCollectionSingleton = new PlayerCollection(getMongoObjectCollection("players")); -export default PlayerCollectionSingleton; \ No newline at end of file +export default PlayerCollection.getInstance; \ No newline at end of file diff --git a/src/ObjectCollections/RulesetCollection.ts b/src/ObjectCollections/RulesetCollection.ts index 7e0d66b..9426656 100644 --- a/src/ObjectCollections/RulesetCollection.ts +++ b/src/ObjectCollections/RulesetCollection.ts @@ -1,21 +1,33 @@ import MongoStoredObjectCollection from "./MongoStoredObjectCollection"; -import mongo from "mongodb"; import {DEFAULT_RULESET, DEFAULT_RULESET_NAME, RulesetSchema} from "../rulesets"; import {getMongoObjectCollection} from "../database"; import Ruleset from "../Objects/Ruleset"; +import {ActiveRecordId} from "../Objects/ActiveRecord"; -type RulesetMongoData = RulesetSchema; +type RulesetMongoData = RulesetSchema & {id: ActiveRecordId}; class RulesetCollection extends MongoStoredObjectCollection { - constructor(collectionClient: mongo.Collection) { - super(collectionClient); + private static instance?: RulesetCollection; + constructor() { + super(); + } + + static getInstance(): RulesetCollection { + if (RulesetCollection.instance === undefined) { + RulesetCollection.instance = new RulesetCollection(); + } + return RulesetCollection.instance; + } + + async init() { + this.mongoDbClientCollection = getMongoObjectCollection("rulesets"); } private async rulesetFrom(data: RulesetMongoData): Promise { return new Ruleset(data.id, data); } - async read(id: string): Promise { + async read(id: ActiveRecordId): Promise { if (id === DEFAULT_RULESET_NAME) { return new Ruleset(DEFAULT_RULESET_NAME, DEFAULT_RULESET); } @@ -26,5 +38,4 @@ class RulesetCollection extends MongoStoredObjectCollection { } } -const RulesetCollectionSingleton = new RulesetCollection(getMongoObjectCollection("users")); -export default RulesetCollectionSingleton; \ No newline at end of file +export default RulesetCollection.getInstance; \ No newline at end of file diff --git a/src/ObjectCollections/SavedGameCollection.ts b/src/ObjectCollections/SavedGameCollection.ts index 15d6d63..c49ad0d 100644 --- a/src/ObjectCollections/SavedGameCollection.ts +++ b/src/ObjectCollections/SavedGameCollection.ts @@ -1,54 +1,63 @@ import MongoStoredObjectCollection from "./MongoStoredObjectCollection"; -import mongo from "mongodb"; import {ActiveRecordId} from "../Objects/ActiveRecord"; import PlayerCollection from "./PlayerCollection"; import SavedGame from "../Objects/SavedGame"; import {getMongoObjectCollection} from "../database"; -import {PlayerGameResults} from "../Objects/DefaultStatsMongoData"; import RulesetCollection from "./RulesetCollection"; -import {GameSubmission} from "../controllers/statsController"; +import {ProcessedGameSubmission, ScoredResultsWithOutcome} from "../Controllers/statsController"; export interface SavedGameMongoData { id: string; - rulesetUsed: ActiveRecordId; + ruleset: ActiveRecordId; players: ActiveRecordId[]; - results: PlayerGameResults[]; + results: ScoredResultsWithOutcome[]; } class SavedGameCollection extends MongoStoredObjectCollection { - constructor(collectionClient: mongo.Collection) { - super(collectionClient); + private static instance?: SavedGameCollection; + constructor() { + super(); + } + + static getInstance(): SavedGameCollection { + if (SavedGameCollection.instance === undefined) { + SavedGameCollection.instance = new SavedGameCollection(); + } + return SavedGameCollection.instance; + } + + async init() { + this.mongoDbClientCollection = getMongoObjectCollection("savedGames"); } private async savedGameFrom(data: SavedGameMongoData): Promise { const playerList: {name: string, id: ActiveRecordId}[] = []; for (const playerId of data.players) { - const player = await PlayerCollection.read(playerId); + const player = await PlayerCollection().read(playerId); playerList.push({name: player.getNick(), id: playerId}) } - const rulesetUsed = await RulesetCollection.read(data.rulesetUsed); + const rulesetUsed = await RulesetCollection().read(data.ruleset); return new SavedGame( data.id, - {name: rulesetUsed.getName(), id: data.rulesetUsed}, + {name: rulesetUsed.getName(), id: data.ruleset}, playerList, data.results); } - async read(id: string): Promise { + async read(id: ActiveRecordId): Promise { const foundGame = await this.mongoRead(id); return this.savedGameFrom(foundGame); } - async create(gameSubmission: GameSubmission): Promise { - const pids = gameSubmission.players.map(playerIdAndNick => playerIdAndNick.id); + async create(submission: ProcessedGameSubmission): Promise { + const pids = submission.players.map(playerIdAndNick => playerIdAndNick.id); return this.savedGameFrom( await this.mongoCreate({ - rulesetUsed: gameSubmission.rulesetId, + ruleset: submission.ruleset, players: pids, - results: gameSubmission.results}) + results: submission.results}) ); } } -const SavedGameCollectionSingleton = new SavedGameCollection(getMongoObjectCollection("users")); -export default SavedGameCollectionSingleton; \ No newline at end of file +export default SavedGameCollection.getInstance; \ No newline at end of file diff --git a/src/Objects/AccountStats.ts b/src/Objects/AccountStats.ts index b89756d..a166625 100644 --- a/src/Objects/AccountStats.ts +++ b/src/Objects/AccountStats.ts @@ -10,6 +10,7 @@ class AccountStats { constructor(data: AccountStatsMongoData) { this.data = data; this.updater = new StatsUpdater(); + this.updater.use(data); } use(data: AccountStatsMongoData) { @@ -21,8 +22,8 @@ class AccountStats { if (this.data) { for (const playerGameResult of playerGameResults) { this.updater.updateStats(playerGameResult, ruleset); - this.data.gamesPlayed += 1; } + this.data.gamesPlayed += 1; } else { throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data diff --git a/src/Objects/ActiveRecord.ts b/src/Objects/ActiveRecord.ts index 1f69067..de91f1a 100644 --- a/src/Objects/ActiveRecord.ts +++ b/src/Objects/ActiveRecord.ts @@ -1,4 +1,6 @@ -export type ActiveRecordId = string; +import mongo from "mongodb"; + +export type ActiveRecordId = mongo.ObjectId | string; export type OrId = T | ActiveRecordId; interface ActiveRecord { diff --git a/src/Objects/DefaultStatsMongoData.ts b/src/Objects/DefaultStatsMongoData.ts index e942ef9..f5b64f9 100755 --- a/src/Objects/DefaultStatsMongoData.ts +++ b/src/Objects/DefaultStatsMongoData.ts @@ -1,12 +1,11 @@ import {CellDef, DEFAULT_RULESET, DEFAULT_RULESET_NAME, RulesetSchema} from "../rulesets"; import {FieldType} from "../enums"; - export enum OutcomeType { - win, - loss, - runnerUp, - draw, + win = "win", + loss = "loss", + runnerUp = "runnerUp", + draw = "draw", } export interface PlayerStatsMongoData extends BaseStatsMongoData {} diff --git a/src/Objects/KadiUser.ts b/src/Objects/KadiUser.ts index 74d7dea..8ce1eca 100644 --- a/src/Objects/KadiUser.ts +++ b/src/Objects/KadiUser.ts @@ -5,7 +5,7 @@ export type LoginDetails = { username: string, email: string, password: string } class KadiUser implements ActiveRecord { constructor( - private id: string, + private id: ActiveRecordId, private username: string, private email: string, private password: string, diff --git a/src/Objects/Player.ts b/src/Objects/Player.ts index b198b9e..369f494 100755 --- a/src/Objects/Player.ts +++ b/src/Objects/Player.ts @@ -1,21 +1,14 @@ -import {CellValue} from "../controllers/statsController"; -import {RulesetSchema} from "../rulesets"; import ActiveRecord, {ActiveRecordId} from "./ActiveRecord"; import {UpdateError} from "../errors"; import {OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData"; import PlayerStats from "./PlayerStats"; import Ruleset from "./Ruleset"; -export interface CellDetails { - id: string; - value: CellValue; -} - export class Player implements ActiveRecord { constructor( private id: ActiveRecordId, private nick: string, - private stats?: PlayerStats + private stats: PlayerStats ) {} getId(): ActiveRecordId { @@ -30,7 +23,7 @@ export class Player implements ActiveRecord { this.nick = newNick; } - getStats(): PlayerStats | undefined { + getStats(): PlayerStats { return this.stats; } diff --git a/src/Objects/PlayerStats.ts b/src/Objects/PlayerStats.ts index 0fc9266..787abd6 100644 --- a/src/Objects/PlayerStats.ts +++ b/src/Objects/PlayerStats.ts @@ -9,6 +9,7 @@ class PlayerStats { constructor(data: PlayerStatsMongoData) { this.data = data; this.updater = new StatsUpdater(); + this.updater.use(data); } use(data: PlayerStatsMongoData) { @@ -18,6 +19,7 @@ class PlayerStats { updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void { this.updater.updateStats(playerGameResults, ruleset); + this.data.gamesPlayed += 1; } getData(): PlayerStatsMongoData { diff --git a/src/Objects/Ruleset.ts b/src/Objects/Ruleset.ts index c1d16f2..8f1aeab 100644 --- a/src/Objects/Ruleset.ts +++ b/src/Objects/Ruleset.ts @@ -26,6 +26,10 @@ export class Ruleset implements ActiveRecord { getCellsInBlock(blockId: string): Record { return Object.assign({}, this.schema.blocks[blockId].cells); } + + getSchemaJSON(): RulesetSchema { + return Object.assign({}, this.schema); + } } export default Ruleset; \ No newline at end of file diff --git a/src/Objects/SavedGame.ts b/src/Objects/SavedGame.ts index c9290d1..af85c83 100755 --- a/src/Objects/SavedGame.ts +++ b/src/Objects/SavedGame.ts @@ -1,12 +1,12 @@ import ActiveRecord, {ActiveRecordId} from "./ActiveRecord"; -import {PlayerGameResults} from "./DefaultStatsMongoData"; +import {ScoredResultsWithOutcome} from "../Controllers/statsController"; class SavedGame implements ActiveRecord { constructor( private id: string, private rulesetUsed: {name: string, id: ActiveRecordId}, private players: {name: string, id: ActiveRecordId}[], - private results: PlayerGameResults[], + private results: ScoredResultsWithOutcome[], ) {} getId() { diff --git a/src/Objects/ScoreCellCalculator.ts b/src/Objects/ScoreCellCalculator.ts index b47c1d1..a7593db 100755 --- a/src/Objects/ScoreCellCalculator.ts +++ b/src/Objects/ScoreCellCalculator.ts @@ -60,6 +60,7 @@ abstract class ScoreCellCalculator { } hydrateWithJSON(jsonRep: ScoreCellJSONRepresentation): void { + this.reset(); if (jsonRep.value === CellFlag.strike) { this.struck = true; } @@ -67,6 +68,11 @@ abstract class ScoreCellCalculator { this.value = jsonRep.value; } } + + reset(): void { + this.struck = false; + this.value = 0; + } } class NumberScoreCell extends ScoreCellCalculator { @@ -78,7 +84,12 @@ class NumberScoreCell extends ScoreCellCalculator { } getScore(): number { - return this.value as number; + if (this.isStruck()) { + return 0; + } + else { + return this.value as number; + } } } @@ -101,6 +112,11 @@ class BoolScoreCell extends ScoreCellCalculator { return 0; } } + + reset(): void { + super.reset(); + this.value = false; + } } class SuperkadiScoreCell extends ScoreCellCalculator { diff --git a/src/Objects/StatsUpdater.ts b/src/Objects/StatsUpdater.ts index 05342d3..421269a 100644 --- a/src/Objects/StatsUpdater.ts +++ b/src/Objects/StatsUpdater.ts @@ -1,4 +1,3 @@ -import {RulesetSchema} from "../rulesets"; import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator"; import {UpdateError} from "../errors"; import {FieldType} from "../enums"; @@ -8,7 +7,8 @@ import { BoolFieldStatsMongoData, CellStatsMongoData, OutcomeType, - PlayerGameResults, RulesetStatsMongoData, + PlayerGameResults, + RulesetStatsMongoData, TotalFieldStatsMongoData } from "./DefaultStatsMongoData"; import Ruleset from "./Ruleset"; @@ -30,15 +30,15 @@ class StatsUpdater { this.validationRuleset = ruleset; this.calculator = new ScoreCalculator(ruleset); this.calculator.hydrateWithJSON(playerGameResults as ScoreCardJSONRepresentation); - this.currentStatsObject = this.data.statsByRuleset[ruleset.getId()]; + this.currentStatsObject = this.data.statsByRuleset[ruleset.getId().toString()]; for (const blockId in ruleset.getBlocks()) { this.updateBlockStats(blockId); } this.updateTotalFieldStats(this.currentStatsObject.grandTotal, this.calculator.getTotal()); - this.currentStatsObject.wins += Number(playerGameResults.outcome === "win"); - this.currentStatsObject.draws += Number(playerGameResults.outcome === "draw"); - this.currentStatsObject.runnerUps += Number(playerGameResults.outcome === "runnerUp"); - this.currentStatsObject.losses += Number(playerGameResults.outcome === "loss"); + this.currentStatsObject.wins += Number(playerGameResults.outcome === OutcomeType.win); + this.currentStatsObject.draws += Number(playerGameResults.outcome === OutcomeType.draw); + this.currentStatsObject.runnerUps += Number(playerGameResults.outcome === OutcomeType.runnerUp); + this.currentStatsObject.losses += Number(playerGameResults.outcome === OutcomeType.loss); } else { throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data @@ -66,9 +66,10 @@ class StatsUpdater { } private updateCellStatsByIds(ids: {cellId: string, blockId: string}) { - const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.getId()}); + const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.getId().toString()}); const cellFieldType = this.validationRuleset?.getBlocks()[ids.blockId].cells[ids.cellId].fieldType; const cellScore = this.calculator!.getCellScoreByLocation({...ids}); + cellStats.runningTotal += cellScore; if (cellScore > 0 && cellFieldType === FieldType.bool) { (cellStats as BoolFieldStatsMongoData).total += 1; } diff --git a/src/routers/apiRouter.ts b/src/Routers/apiRouter.ts similarity index 72% rename from src/routers/apiRouter.ts rename to src/Routers/apiRouter.ts index 4f766ed..6019b33 100755 --- a/src/routers/apiRouter.ts +++ b/src/Routers/apiRouter.ts @@ -1,7 +1,8 @@ import express from "express"; -import * as statsController from "../controllers/statsController"; -import * as KadiUserController from "../controllers/kadiUserController" +import * as statsController from "../Controllers/statsController"; +import * as KadiUserController from "../Controllers/kadiUserController" import {requireAuthenticated} from "./routerMiddleware"; +import * as rulesetController from "../Controllers/rulesetController"; const router = express.Router(); @@ -21,4 +22,8 @@ router.delete("/guest/:id", requireAuthenticated, KadiUserController.deleteGuest router.get("/games", requireAuthenticated, statsController.listGames); router.post("/games", requireAuthenticated, statsController.saveGame); +//Stats +router.get("/stats", requireAuthenticated, statsController.getStats); +router.get("/ruleset/:id", rulesetController.getRuleset); + export default router; \ No newline at end of file diff --git a/src/routers/mainRouter.ts b/src/Routers/mainRouter.ts similarity index 93% rename from src/routers/mainRouter.ts rename to src/Routers/mainRouter.ts index f633118..d58c4a4 100755 --- a/src/routers/mainRouter.ts +++ b/src/Routers/mainRouter.ts @@ -1,4 +1,4 @@ -import express, {NextFunction} from "express"; +import express from "express"; import routers from "./routers"; import {LoginDetails} from "../Objects/KadiUser"; import {requireAuthenticated} from "./routerMiddleware"; @@ -22,6 +22,7 @@ router.get("/**", requireAuthenticated, (req, res) => { }); const topLevelErrorHandler: express.ErrorRequestHandler = (err, req, res, next) => { + console.log(err.message); if (err instanceof GenericPersistenceError) { res.status(500).send({message: "An internal error occurred accessing the database."}); } diff --git a/src/routers/routerMiddleware.ts b/src/Routers/routerMiddleware.ts similarity index 100% rename from src/routers/routerMiddleware.ts rename to src/Routers/routerMiddleware.ts diff --git a/src/routers/routers.ts b/src/Routers/routers.ts similarity index 100% rename from src/routers/routers.ts rename to src/Routers/routers.ts diff --git a/src/routers/signupRouter.ts b/src/Routers/signupRouter.ts similarity index 71% rename from src/routers/signupRouter.ts rename to src/Routers/signupRouter.ts index 877591a..0cf9809 100755 --- a/src/routers/signupRouter.ts +++ b/src/Routers/signupRouter.ts @@ -1,17 +1,13 @@ import express from "express"; -import {requireAuthenticated, requireNotAuthenticated} from "../passport-config"; -import * as signup from "../controllers/signupController"; +import * as signup from "../Controllers/signupController"; +import {requireAuthenticated, requireNotAuthenticated} from "./routerMiddleware"; const router = express.Router(); router.get("/login", requireNotAuthenticated, signup.showLoginPage); - router.post("/login", requireNotAuthenticated, signup.loginUser); - router.get("/register", requireNotAuthenticated, signup.showRegistrationPage); - router.post("/register", requireNotAuthenticated, signup.registerNewUser); - router.get("/logout", requireAuthenticated, signup.logoutUser); export default router; \ No newline at end of file diff --git a/src/database.ts b/src/database.ts index c90d072..867ac27 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,6 +1,10 @@ import {MongoClient, Db} from "mongodb"; import Settings from "./server-config.json"; import {GenericPersistenceError, MongoError} from "./errors"; +import KadiUserCollection from "./ObjectCollections/KadiUserCollection"; +import PlayerCollection from "./ObjectCollections/PlayerCollection"; +import RulesetCollection from "./ObjectCollections/RulesetCollection"; +import SavedGameCollection from "./ObjectCollections/SavedGameCollection"; let SessionDbClient: Db; @@ -21,6 +25,13 @@ export function getMongoObjectCollection(collectionName: string) { } } +export async function initCollections() { + KadiUserCollection().init(); + PlayerCollection().init(); + SavedGameCollection().init(); + RulesetCollection().init(); +} + type CallbackWrapper = (query: () => T) => Promise; export const tryQuery: CallbackWrapper = async (cb) => { try { diff --git a/src/index.ts b/src/index.ts index d1eff1a..ec3b6bf 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,16 @@ -import express, {NextFunction, Request, Response} from "express"; +import express from "express"; import Settings from "./server-config.json"; import flash from "express-flash"; import passport from "passport"; import session from "express-session"; -import MainRouter from "./routers/mainRouter"; -import {initMongoSessionClient} from "./database"; +import MainRouter from "./Routers/mainRouter"; +import {initCollections, initMongoSessionClient} from "./database"; import {Strategy as LocalStrategy} from "passport-local"; -import {authenticateKadiUser, deserializeKadiUser, serializeKadiUser} from "./controllers/signupController"; +import {authenticateKadiUser, deserializeKadiUser, serializeKadiUser} from "./Controllers/signupController"; async function startApp() { await initMongoSessionClient(); + await initCollections(); passport.use(new LocalStrategy({ usernameField: "email" }, authenticateKadiUser)); passport.serializeUser(serializeKadiUser); passport.deserializeUser(deserializeKadiUser); diff --git a/tsconfig.json b/tsconfig.json index 2444026..4a25d6e 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ "allowJs": true, /* Allow javascript files to be compiled. */