From cef6249c09cd2035bd2c631941ab8fe9620fc618 Mon Sep 17 00:00:00 2001 From: Daniel Ledda Date: Thu, 16 Jul 2020 08:05:37 +0200 Subject: [PATCH] Big update progress on encapsulating models --- src/classes/AccountStatsUpdater.ts | 31 ++ src/classes/PlayerStatsUpdater.ts | 24 ++ ...{ScoreBlock.ts => ScoreBlockCalculator.ts} | 22 +- src/classes/ScoreCalculator.ts | 8 +- .../{ScoreCell.ts => ScoreCellCalculator.ts} | 14 +- .../stats.ts => classes/StatsUpdater.ts} | 119 +----- src/controllers/signupController.ts | 5 +- src/errors.ts | 29 ++ src/models/MongoStoredObject.ts | 15 + src/models/MongoStoredObjectCollection.ts | 84 +++++ src/models/Stats.ts | 100 +++++ src/models/StoredObject.ts | 9 + src/models/StoredObjectCollection.ts | 9 + src/models/StoredPlayer.ts | 25 +- src/models/StoredUser.ts | 43 ++- src/models/savedGame.ts | 11 +- src/models/utils.ts | 115 +----- src/passport-config.ts | 12 +- src/routers/mainRouter.ts | 6 +- src/rulesets.ts | 356 +++++++----------- 20 files changed, 522 insertions(+), 515 deletions(-) create mode 100644 src/classes/AccountStatsUpdater.ts create mode 100644 src/classes/PlayerStatsUpdater.ts rename src/classes/{ScoreBlock.ts => ScoreBlockCalculator.ts} (84%) rename src/classes/{ScoreCell.ts => ScoreCellCalculator.ts} (91%) rename src/{models/stats.ts => classes/StatsUpdater.ts} (54%) mode change 100755 => 100644 create mode 100644 src/errors.ts create mode 100644 src/models/MongoStoredObject.ts create mode 100644 src/models/MongoStoredObjectCollection.ts create mode 100755 src/models/Stats.ts create mode 100644 src/models/StoredObject.ts create mode 100644 src/models/StoredObjectCollection.ts diff --git a/src/classes/AccountStatsUpdater.ts b/src/classes/AccountStatsUpdater.ts new file mode 100644 index 0000000..89d9a7d --- /dev/null +++ b/src/classes/AccountStatsUpdater.ts @@ -0,0 +1,31 @@ +import {Ruleset} from "../rulesets"; +import {AccountStats, OutcomeType, PlayerGameResults} from "../models/Stats"; +import {UpdateError} from "../errors"; +import StatsUpdater from "./StatsUpdater"; + +export class AccountStatsUpdater { + private data?: AccountStats; + private readonly updater: StatsUpdater; + constructor(data?: AccountStats) { + if (data) { + this.data = data; + } + this.updater = new StatsUpdater(); + } + + use(data: AccountStats) { + this.data = data; + this.updater.use(data); + } + + updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void { + if (this.data) { + this.updater.updateStats(playerGameResults, ruleset); + this.data.gamesPlayed += 1; + } + else { + throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data + to analyse.`); + } + } +} \ No newline at end of file diff --git a/src/classes/PlayerStatsUpdater.ts b/src/classes/PlayerStatsUpdater.ts new file mode 100644 index 0000000..f537d67 --- /dev/null +++ b/src/classes/PlayerStatsUpdater.ts @@ -0,0 +1,24 @@ +import {Ruleset} from "../rulesets"; +import StatsUpdater, {OutcomeType, PlayerGameResults, PlayerStats} from "../models/Stats"; + +class PlayerStatsUpdater { + private data?: PlayerStats; + private readonly updater: StatsUpdater; + constructor(data?: PlayerStats) { + if (data) { + this.data = data; + } + this.updater = new StatsUpdater(); + } + + use(data: PlayerStats) { + this.data = data; + this.updater.use(data); + } + + updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void { + this.updater.updateStats(playerGameResults, ruleset); + } +} + +export default PlayerStatsUpdater; \ No newline at end of file diff --git a/src/classes/ScoreBlock.ts b/src/classes/ScoreBlockCalculator.ts similarity index 84% rename from src/classes/ScoreBlock.ts rename to src/classes/ScoreBlockCalculator.ts index 2dc1ec7..619de91 100755 --- a/src/classes/ScoreBlock.ts +++ b/src/classes/ScoreBlockCalculator.ts @@ -1,12 +1,12 @@ -import ScoreCell, { +import ScoreCellCalculator, { createCellFromDef, ScoreCellValue, CellState, ScoreCellJSONRepresentation -} from "./ScoreCell"; +} from "./ScoreCellCalculator"; import {CellDef, BlockDef, BonusBlockDef, NoBonusBlockDef } from "../rulesets"; -export const createBlockFromDef = (blockId: string, blockDef: BlockDef) : ScoreBlock => { +export const createBlockFromDef = (blockId: string, blockDef: BlockDef) : ScoreBlockCalculator => { if (blockDef.hasBonus) { return new ScoreBlockWithBonus(blockId, blockDef); } @@ -19,16 +19,16 @@ export interface ScoreBlockJSONRepresentation { cells: Record; } -abstract class ScoreBlock { - protected cells: ScoreCell[]; +abstract class ScoreBlockCalculator { + protected cells: ScoreCellCalculator[]; protected id: string; protected constructor(blockId: string, blockDef: BlockDef) { - this.cells = ScoreBlock.generateCells(blockDef.cells); + this.cells = ScoreBlockCalculator.generateCells(blockDef.cells); this.id = blockId; } - private static generateCells(cellDefs: Record): ScoreCell[] { + private static generateCells(cellDefs: Record): ScoreCellCalculator[] { const cells = []; for (const cellId in cellDefs) { cells.push(createCellFromDef(cellId, cellDefs[cellId])); @@ -56,7 +56,7 @@ abstract class ScoreBlock { return this.getCellById(cellId).isStruck(); } - private getCellById(cellId: string): ScoreCell { + private getCellById(cellId: string): ScoreCellCalculator { const foundScoreCell = this.cells.find(cell => cell.getId() === cellId); if (foundScoreCell !== undefined) { return foundScoreCell; @@ -77,7 +77,7 @@ abstract class ScoreBlock { } } -class ScoreBlockWithBonus extends ScoreBlock { +class ScoreBlockWithBonus extends ScoreBlockCalculator { protected readonly bonus: number; protected readonly bonusFor: number; @@ -97,7 +97,7 @@ class ScoreBlockWithBonus extends ScoreBlock { } } -class ScoreBlockNoBonus extends ScoreBlock { +class ScoreBlockNoBonus extends ScoreBlockCalculator { constructor(blockId: string, blockDef: NoBonusBlockDef) { super(blockId, blockDef); } @@ -111,4 +111,4 @@ class ScoreBlockNoBonus extends ScoreBlock { } } -export default ScoreBlock; \ No newline at end of file +export default ScoreBlockCalculator; \ No newline at end of file diff --git a/src/classes/ScoreCalculator.ts b/src/classes/ScoreCalculator.ts index 86867e3..ae54435 100755 --- a/src/classes/ScoreCalculator.ts +++ b/src/classes/ScoreCalculator.ts @@ -1,5 +1,5 @@ import {BlockDef, Ruleset} from "../rulesets"; -import ScoreBlock, {createBlockFromDef, ScoreBlockJSONRepresentation} from "./ScoreBlock"; +import ScoreBlockCalculator, {createBlockFromDef, ScoreBlockJSONRepresentation} from "./ScoreBlockCalculator"; export type CellLocation = { blockId: string, cellId: string }; @@ -8,7 +8,7 @@ export interface ScoreCardJSONRepresentation { } class ScoreCalculator { - private readonly blocks: ScoreBlock[]; + private readonly blocks: ScoreBlockCalculator[]; constructor(gameSchema: Ruleset) { this.blocks = ScoreCalculator.generateBlocks(gameSchema.blocks); @@ -20,7 +20,7 @@ class ScoreCalculator { } } - private static generateBlocks(blockDefs: Record): ScoreBlock[] { + private static generateBlocks(blockDefs: Record): ScoreBlockCalculator[] { const blocks = []; for (const blockId in blockDefs) { blocks.push(createBlockFromDef(blockId, blockDefs[blockId])); @@ -56,7 +56,7 @@ class ScoreCalculator { return this.getBlockById(loc.blockId).cellWithIdIsStruck(loc.cellId); } - private getBlockById(blockId: string): ScoreBlock { + private getBlockById(blockId: string): ScoreBlockCalculator { const foundScoreBlock = this.blocks.find(block => block.getId() === blockId); if (foundScoreBlock !== undefined) { return foundScoreBlock; diff --git a/src/classes/ScoreCell.ts b/src/classes/ScoreCellCalculator.ts similarity index 91% rename from src/classes/ScoreCell.ts rename to src/classes/ScoreCellCalculator.ts index 048abda..b47c1d1 100755 --- a/src/classes/ScoreCell.ts +++ b/src/classes/ScoreCellCalculator.ts @@ -7,7 +7,7 @@ import { } from "../rulesets"; import { CellFlag, FieldType } from "../enums"; -export const createCellFromDef = (cellId: string, cellDef: CellDef): ScoreCell => { +export const createCellFromDef = (cellId: string, cellDef: CellDef): ScoreCellCalculator => { switch (cellDef.fieldType) { case FieldType.number: return new NumberScoreCell(cellId, cellDef); @@ -33,7 +33,7 @@ export interface ScoreCellJSONRepresentation { value: number | boolean | CellFlag.strike; } -abstract class ScoreCell { +abstract class ScoreCellCalculator { protected readonly id: string; protected static readonly fieldType: FieldType; protected struck: boolean; @@ -69,7 +69,7 @@ abstract class ScoreCell { } } -class NumberScoreCell extends ScoreCell { +class NumberScoreCell extends ScoreCellCalculator { protected static readonly fieldType = FieldType.number; constructor(cellId: string, cellDef: NumberCellDef) { @@ -82,7 +82,7 @@ class NumberScoreCell extends ScoreCell { } } -class BoolScoreCell extends ScoreCell { +class BoolScoreCell extends ScoreCellCalculator { protected static readonly fieldType = FieldType.bool; private readonly score: number; protected value: boolean; @@ -103,7 +103,7 @@ class BoolScoreCell extends ScoreCell { } } -class SuperkadiScoreCell extends ScoreCell { +class SuperkadiScoreCell extends ScoreCellCalculator { protected static readonly fieldType = FieldType.superkadi; private readonly score: number; protected value: number; @@ -124,7 +124,7 @@ class SuperkadiScoreCell extends ScoreCell { } } -class MultiplierScoreCell extends ScoreCell { +class MultiplierScoreCell extends ScoreCellCalculator { protected static readonly fieldType = FieldType.multiplier; protected readonly multiplier: number; protected value: number; @@ -145,4 +145,4 @@ class MultiplierScoreCell extends ScoreCell { } } -export default ScoreCell; \ No newline at end of file +export default ScoreCellCalculator; \ No newline at end of file diff --git a/src/models/stats.ts b/src/classes/StatsUpdater.ts old mode 100755 new mode 100644 similarity index 54% rename from src/models/stats.ts rename to src/classes/StatsUpdater.ts index a5c73e7..785f2a3 --- a/src/models/stats.ts +++ b/src/classes/StatsUpdater.ts @@ -1,66 +1,18 @@ import {Ruleset} from "../rulesets"; +import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator"; +import {UpdateError} from "../errors"; import {FieldType} from "../enums"; -import ScoreCalculator, {ScoreCardJSONRepresentation} from "../classes/ScoreCalculator"; +import { + BaseStats, + BestableFieldStats, + BoolFieldStats, + CellStats, + OutcomeType, + PlayerGameResults, RulesetStats, + TotalFieldStats +} from "../models/Stats"; -class UpdateError extends Error { - constructor(message: string) { - super(message); - this.name = "UpdateError"; - } -} - -export type OutcomeType = "win" | "loss" | "runnerUp" | "draw"; -export interface PlayerStats extends BaseStats { -} -export interface AccountStats extends BaseStats { - timesNoWinner: number; -} -interface BaseStats { - statsByRuleset: Record - gamesPlayed: number; -} -interface RulesetStats { - blockStats: Record; - wins: number; - runnerUps: number; - draws: number; - losses: number; - grandTotal: TotalFieldStats; -} -interface BlockStats { - cellStats: Record; - timesHadBonus?: number; - total: TotalFieldStats; -} -interface BaseCellStats { - runningTotal: number; -} -interface StrikeableFieldStats extends BaseCellStats { - timesStruck: number; -} -interface BestableFieldStats extends BaseCellStats { - best: number; - worst: number; -} -type TotalFieldStats = BestableFieldStats; -type BoolFieldStats = StrikeableFieldStats & { total: number }; -type NumberFieldStats = StrikeableFieldStats & BestableFieldStats; -type MultiplierFieldStats = NumberFieldStats; -type SuperkadiFieldStats = NumberFieldStats; -type CellStats = BoolFieldStats | NumberFieldStats | MultiplierFieldStats | SuperkadiFieldStats; - -export interface PlayerGameResults { - blocks: Record; -} -interface BlockResults { - cells: Record -} -interface CellResults { - value: CellValue; -} -type CellValue = "cellFlagStrike" | number | boolean; - -class BaseStatsUpdater { +class StatsUpdater { private data?: BaseStats; private validationRuleset?: Ruleset; private calculator?: ScoreCalculator; @@ -138,49 +90,4 @@ class BaseStatsUpdater { } } -export class PlayerStatsUpdater { - private data?: PlayerStats; - private readonly updater: BaseStatsUpdater; - constructor(data?: PlayerStats) { - if (data) { - this.data = data; - } - this.updater = new BaseStatsUpdater(); - } - - use(data: PlayerStats) { - this.data = data; - this.updater.use(data); - } - - updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void { - this.updater.updateStats(playerGameResults, ruleset); - } -} - -export class AccountStatsUpdater { - private data?: AccountStats; - private readonly updater: BaseStatsUpdater; - constructor(data?: AccountStats) { - if (data) { - this.data = data; - } - this.updater = new BaseStatsUpdater(); - } - - use(data: AccountStats) { - this.data = data; - this.updater.use(data); - } - - updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void { - if (this.data) { - this.updater.updateStats(playerGameResults, ruleset); - this.data.gamesPlayed += 1; - } - else { - throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data - to analyse.`); - } - } -} \ No newline at end of file +export default StatsUpdater; \ No newline at end of file diff --git a/src/controllers/signupController.ts b/src/controllers/signupController.ts index c7ceed0..3b8cef5 100755 --- a/src/controllers/signupController.ts +++ b/src/controllers/signupController.ts @@ -1,6 +1,6 @@ import passport from "passport"; -import DbUser from "../models/dbUser_old"; import {RequestHandler} from "express"; +import StoredUsers, {LoginDetails} from "../models/StoredUser"; export const showLoginPage: RequestHandler = (req, res) => { res.render("login.ejs"); @@ -20,7 +20,8 @@ export const showRegistrationPage: RequestHandler = (req, res) => { export const registerNewUser: RequestHandler = async (req, res) => { try { - const newUser = await DbUser.registerUser(req.body.username, req.body.email, req.body.password); + const loginDetails: LoginDetails = req.body as LoginDetails; + const newUser = await StoredUsers.registerUser(loginDetails); req.login(newUser, (err) => { if (err) { throw err; diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..b75fb38 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,29 @@ +export class KadiError extends Error {} + +export class UpdateError extends KadiError { + constructor(message: string) { + super(message); + this.name = "UpdateError"; + } +} + +export class GenericPersistenceError extends KadiError { + constructor(message: string) { + super(message); + this.name = "GenericPersistenceError"; + } +} + +export class MongoError extends GenericPersistenceError { + constructor(message: string) { + super(message); + this.name = "MongoError"; + } +} + +export class ModelParameterError extends GenericPersistenceError { + constructor(message: string) { + super(message); + this.name = "ModelParameterError"; + } +} diff --git a/src/models/MongoStoredObject.ts b/src/models/MongoStoredObject.ts new file mode 100644 index 0000000..fbbedbe --- /dev/null +++ b/src/models/MongoStoredObject.ts @@ -0,0 +1,15 @@ +import StoredObject from "./StoredObject"; + +abstract class MongoStoredObject implements StoredObject { + protected constructor(protected data: {_id: string} & RawDataInterface) {} + + id(): string { + return this.data._id; + } + + rawData(): RawDataInterface { + return this.data; + } +} + +export default MongoStoredObject; \ No newline at end of file diff --git a/src/models/MongoStoredObjectCollection.ts b/src/models/MongoStoredObjectCollection.ts new file mode 100644 index 0000000..52801d0 --- /dev/null +++ b/src/models/MongoStoredObjectCollection.ts @@ -0,0 +1,84 @@ +import StoredObject, {StoredObjectConstructor, StoredObjectId} from "./StoredObject"; +import mongo from "mongodb"; +import {tryQuery} from "./utils"; +import {MongoError} from "../errors"; +import StoredObjectCollection from "./StoredObjectCollection"; + + +abstract class MongoStoredObjectCollection + implements StoredObjectCollection { + + protected mongoDbClientCollection: mongo.Collection; + protected StoredObjectConstructor: StoredObjectConstructor; + + protected constructor( + collectionClient: mongo.Collection, + objectConstructor: StoredObjectConstructor + ) { + this.mongoDbClientCollection = collectionClient; + this.StoredObjectConstructor = objectConstructor; + } + + private async create(objectData: Omit): Promise { + return tryQuery(async () => { + const insertOneWriteOpResult = await this.mongoDbClientCollection.insertOne(objectData); + if (insertOneWriteOpResult.result.ok === 1) { + return insertOneWriteOpResult.ops[0] + } + else { + throw new MongoError(`Error creating the object: ${JSON.stringify(objectData)}`); + } + }); + } + + protected async newObject(objectData: Omit): Promise { + return new this.StoredObjectConstructor(await this.create(objectData)); + } + + protected async findObjectById(id: string): Promise { + return tryQuery(async () => + await this.mongoDbClientCollection.findOne({_id: id}) + ); + } + + protected async findObjectByAttribute(attribute: string, value: any): Promise { + return tryQuery(async () => + await this.mongoDbClientCollection.findOne({[attribute]: value}) + ); + } + + async deleteById(objectId: StoredObjectId, returnObject?: boolean): Promise { + let deletedObject; + if (returnObject ?? true) { + deletedObject = await this.findById(objectId); + } + const deleteWriteOpResult = await this.mongoDbClientCollection.deleteOne({_id: objectId}); + if (deleteWriteOpResult.result.ok === 1) { + return deletedObject; + } + else { + throw new MongoError(`Error deleting the object with id: ${JSON.stringify(objectId)}`); + } + } + + + async findById(id: string): Promise { + const data = await this.findObjectById(id); + if (data) { + return new this.StoredObjectConstructor(data); + } + else { + return null; + } + }; + + async save(...objects: IStoredObject[]): Promise { + await tryQuery(async () => { + for (const object of objects) { + await this.mongoDbClientCollection.findOneAndUpdate({_id: object.id()}, {...object.rawData()}); + } + }); + } +} + +export default MongoStoredObjectCollection; \ No newline at end of file diff --git a/src/models/Stats.ts b/src/models/Stats.ts new file mode 100755 index 0000000..a431811 --- /dev/null +++ b/src/models/Stats.ts @@ -0,0 +1,100 @@ +import {DEFAULT_RULESET_NAME, DefaultRulesetStats} from "../rulesets"; + + +export type OutcomeType = "win" | "loss" | "runnerUp" | "draw"; +export interface PlayerStats extends BaseStats {} +export interface AccountStats extends BaseStats { + timesNoWinner: number; +} +interface BaseStats { + statsByRuleset: Record & { [DEFAULT_RULESET_NAME]: DefaultRulesetStats } + gamesPlayed: number; +} +export interface RulesetStats { + blockStats: Record; + wins: number; + runnerUps: number; + draws: number; + losses: number; + grandTotal: TotalFieldStats; +} +interface BlockStats { + cellStats: Record; + timesHadBonus?: number; + total: TotalFieldStats; +} +interface BaseCellStats { + runningTotal: number; +} +interface StrikeableFieldStats extends BaseCellStats { + timesStruck: number; +} +interface BestableFieldStats extends BaseCellStats { + best: number; + worst: number; +} +type TotalFieldStats = BestableFieldStats; +type BoolFieldStats = StrikeableFieldStats & { total: number }; +type NumberFieldStats = StrikeableFieldStats & BestableFieldStats; +type MultiplierFieldStats = NumberFieldStats; +type SuperkadiFieldStats = NumberFieldStats; +type CellStats = BoolFieldStats | NumberFieldStats | MultiplierFieldStats | SuperkadiFieldStats; + + +export interface PlayerGameResults { + blocks: Record; +} +interface BlockResults { + cells: Record +} +interface CellResults { + value: CellValue; +} +type CellValue = "cellFlagStrike" | number | boolean; + + +function default + +function defaultBoolFieldStats(): BoolFieldStats { + +} + +function defaultNumberFieldStats(): NumberFieldStats { + +} + +function defaultMultiplierFieldStats(): MultiplierFieldStats { + +} + +function defaultSuperkadiFieldStats(): SuperkadiFieldStats { + +} + +function defaultBaseStatsData(): BaseStats { + return { + statsByRuleset: { + [DEFAULT_RULESET_NAME]: {} + wins: 0, + runnerUps: 0, + draws: 0, + losses: 0, + grandTotal: {}, + }, + gamesPlayed: 0; +}; +} + +function defaultAccountStatsData(): AccountStats { + return {...defaultBaseStatsData(), timesNoWinner: 0}; +} + +function defaultPlayerStatsData(): PlayerStats { + return defaultBaseStatsData(); +} + + +export default { + defaultPlayerStatsData: defaultPlayerStatsData, + defaultAccountStatsData: defaultAccountStatsData, +}; \ No newline at end of file diff --git a/src/models/StoredObject.ts b/src/models/StoredObject.ts new file mode 100644 index 0000000..8b1bbb7 --- /dev/null +++ b/src/models/StoredObject.ts @@ -0,0 +1,9 @@ +export type StoredObjectConstructor = new (data: Data, ...args: any[]) => Object; +export type StoredObjectId = string; + +interface StoredObject { + id(): string; + rawData(): any; +} + +export default StoredObject; \ No newline at end of file diff --git a/src/models/StoredObjectCollection.ts b/src/models/StoredObjectCollection.ts new file mode 100644 index 0000000..9be9334 --- /dev/null +++ b/src/models/StoredObjectCollection.ts @@ -0,0 +1,9 @@ +import StoredObject, {StoredObjectId} from "./StoredObject"; + +interface StoredObjectCollection { + findById(id: string): Promise; + deleteById(objectId: StoredObjectId, returnObject?: boolean): Promise; + save(...objects: StoredObjectInterface[]): Promise; +} + +export default StoredObjectCollection; \ No newline at end of file diff --git a/src/models/StoredPlayer.ts b/src/models/StoredPlayer.ts index ade2aae..7eb1cfa 100755 --- a/src/models/StoredPlayer.ts +++ b/src/models/StoredPlayer.ts @@ -1,16 +1,13 @@ -import { - OutcomeType, - PlayerGameResults, - PlayerStats, - PlayerStatsUpdater -} from "./stats"; -import { - getMongoObjectCollection, - MongoStoredObject, MongoStoredObjectCollection, StoredObject, StoredObjectCollection, tryQuery, -} from "./utils"; +import Stats, {OutcomeType, PlayerGameResults, PlayerStats} from "./Stats"; +import {getMongoObjectCollection} from "./utils"; import {CellValue} from "../controllers/statsController"; import mongo from "mongodb"; import {Ruleset} from "../rulesets"; +import StoredObjectCollection from "./StoredObjectCollection"; +import MongoStoredObjectCollection from "./MongoStoredObjectCollection"; +import StoredObject from "./StoredObject"; +import PlayerStatsUpdater from "../classes/PlayerStatsUpdater"; +import MongoStoredObject from "./MongoStoredObject"; export interface CellDetails { id: string; @@ -24,6 +21,7 @@ export interface StoredPlayerData { } interface StoredPlayerCollection extends StoredObjectCollection { + newPlayer(nick: string): Promise; } class MongoStoredPlayerCollection @@ -36,10 +34,9 @@ class MongoStoredPlayerCollection this.updater = new PlayerStatsUpdater(); } - newPlayer(nick: string): Promise { - return tryQuery(async () => { - return this.create({nick}); - }); + async newPlayer(nick: string): Promise { + const newPlayer = {nick, stats: Stats.makeBlankDataFields()}; + return this.newObject(newPlayer); } } diff --git a/src/models/StoredUser.ts b/src/models/StoredUser.ts index 6405b36..06fc979 100644 --- a/src/models/StoredUser.ts +++ b/src/models/StoredUser.ts @@ -1,9 +1,9 @@ import {SupportedLang} from "../enums"; -import Player, {MongoStoredPlayer, StoredPlayer, StoredPlayerData} from "./StoredPlayer"; -import {AccountStats} from "./stats"; +import {StoredPlayer} from "./StoredPlayer"; +import {AccountStats} from "./Stats"; import {SavedGameData, StoredSavedGame} from "./savedGame"; import { - GenericModelError, getMongoObjectCollection, + GenericPersistenceError, getMongoObjectCollection, MongoStoredObject, MongoStoredObjectCollection, StoredObject, @@ -57,7 +57,7 @@ export interface StoredUser extends StoredObject { } type GuestUpdateParams = { id: string, newNick: string }; -type LoginDetails = { username: string, email: string, password: string }; +export type LoginDetails = { username: string, email: string, password: string }; class MongoStoredUser extends MongoStoredObject implements StoredUser { constructor(data: StoredUserData) { @@ -165,17 +165,6 @@ class MongoStoredUserCollection extends MongoStoredObjectCollection { - const newPlayer = await StoredPlayers.newPlayer(loginDetails.username); - return this.create({ - username: loginDetails.username, - email: loginDetails.email, - password: loginDetails.password, - lang: SupportedLang.gb, - player: newPlayer.id() - }); - } - async registerUser(loginDetails: LoginDetails): Promise { const usernameTaken = await this.userWithUsernameExists(loginDetails.username); const emailTaken = await this.userWithEmailExists(loginDetails.email); @@ -183,13 +172,23 @@ class MongoStoredUserCollection extends MongoStoredObjectCollection - this.addNewUser({...loginDetails, password: hashedPassword}) - ); + return this.addNewUser({...loginDetails}) } } + private async addNewUser(loginDetails: LoginDetails): Promise { + const newPlayer = await StoredPlayers.newPlayer(loginDetails.username); + return tryQuery(async () => + this.create({ + username: loginDetails.username, + email: loginDetails.email, + password: await this.makePasswordSecure(loginDetails.password), + lang: SupportedLang.gb, + player: newPlayer.id() + }) + ); + } + async userWithEmailExists(email: string): Promise { const object = await this.findObjectByAttribute("email", email); return object !== null; @@ -206,9 +205,13 @@ class MongoStoredUserCollection extends MongoStoredObjectCollection { + return bcrypt.hash(password, 10); + } } const StoredUsers = new MongoStoredUserCollection(getMongoObjectCollection("users")); diff --git a/src/models/savedGame.ts b/src/models/savedGame.ts index 00acb89..abf0a44 100755 --- a/src/models/savedGame.ts +++ b/src/models/savedGame.ts @@ -1,15 +1,12 @@ -import mongoose, {Types} from "mongoose"; -import Player, {IPlayer} from "./StoredPlayer"; import {GameSubmission} from "../controllers/statsController"; -import {tryQuery, globalSchemaOptions, StoredObjectCollection, StoredObject} from "./utils"; -import DbUser from "./dbUser_old"; -import {Ruleset} from "../rulesets"; +import {StoredObjectCollection, StoredObject, StoredObjectId} from "./utils"; +import {PlayerGameResults} from "./Stats"; export interface SavedGameData { id: string; rulesetUsed: RulesetData; - players: mongoose.Types.ObjectId[]; - results: []; + players: StoredObjectId[]; + results: PlayerGameResults[]; } export interface StoredSavedGame extends StoredObject { diff --git a/src/models/utils.ts b/src/models/utils.ts index c7d9b2b..bdf755a 100644 --- a/src/models/utils.ts +++ b/src/models/utils.ts @@ -1,5 +1,6 @@ -import mongo, {MongoClient, Db} from "mongodb"; +import {MongoClient, Db} from "mongodb"; import Settings from "../server-config.json"; +import {GenericPersistenceError, MongoError} from "../errors"; let SessionDbClient: Db; export async function initMongoSessionClient() { @@ -18,121 +19,13 @@ export function getMongoObjectCollection(collectionName: string) { } } -export type StoredObjectId = string; - -export interface StoredObjectCollection { - findById(id: string): Promise; -} - -export abstract class MongoStoredObjectCollection implements StoredObjectCollection { - protected mongoDbClientCollection: mongo.Collection; - protected MongoStoredObject: new(data: D, ...args: any[]) => K; - protected constructor(collectionClient: mongo.Collection, objectConstructor: new (data: D, ...args: any[]) => K) { - this.mongoDbClientCollection = collectionClient; - this.MongoStoredObject = objectConstructor; - } - - protected async findObjectById(id: string): Promise { - return tryQuery(async () => - await this.mongoDbClientCollection.findOne({_id: id}) - ); - } - - protected async findObjectByAttribute(attribute: string, value: any): Promise { - return tryQuery(async () => - await this.mongoDbClientCollection.findOne({attribute: value}) - ); - } - - protected async create(objectData: Partial): Promise { - return tryQuery(async () => - await this.mongoDbClientCollection.insertOne(objectData) - ); - } - - async deleteById(objectId: StoredObjectId): Promise { - const deletedObject = this.findById(objectId); - await this.mongoDbClientCollection.deleteOne({_id: objectId}); - return deletedObject; - } - - async findById(id: string): Promise { - const data = await this.findObjectById(id); - return new this.MongoStoredObject(data); - }; - - async save(...objects: K[]): Promise { - await tryQuery(async () => { - for (const object of objects) { - await this.mongoDbClientCollection.findOneAndUpdate({_id: object.id()}, {...object.rawData()}); - } - }); - } -} - -export interface StoredObject { - id(): string; - rawData(): any; -} - -export abstract class MongoStoredObject implements StoredObject { - protected constructor(protected data: {_id: string} & T) {} - - id(): string { - return this.data._id; - } - - rawData(): T { - return this.data; - } -} - - -export class MongoError extends Error { - constructor(message: string) { - super(message); - this.name = "MongoError"; - } -} - -export class GenericModelError extends MongoError { - constructor(message: string) { - super(message); - this.name = "GenericModelError"; - } -} - -export class ModelParameterError extends GenericModelError { - constructor(message: string) { - super(message); - this.name = "ModelParameterError"; - } -} - -type CallbackWrapper = (query: () => T) => Promise; +type CallbackWrapper = (query: () => T) => Promise; export const tryQuery: CallbackWrapper = async (cb) => { try { return cb(); } catch (err) { - throw new GenericModelError(err); - } -}; - -export const globalSchemaOptions = { - toObject: { - transform: function (doc: any, ret: any) { - ret.id = ret._id; - delete ret._id; - delete ret.__v; - } - }, - toJSON: { - transform: function (doc: any, ret: any) { - ret.id = ret._id; - delete ret._id; - delete ret.__v; - }, + throw new GenericPersistenceError(err); } }; \ No newline at end of file diff --git a/src/passport-config.ts b/src/passport-config.ts index cdc9142..ecf1299 100755 --- a/src/passport-config.ts +++ b/src/passport-config.ts @@ -1,8 +1,8 @@ import passport from "passport"; import {Strategy as LocalStrategy, VerifyFunction} from "passport-local"; import bcrypt from "bcrypt"; -import DbUser, {IDbUser} from "./models/dbUser_old"; import {NextFunction, Request, Response} from "express"; +import StoredUsers, {StoredUser} from "./models/StoredUser"; export const requireAuthenticated = (req: Request, res: Response, next: NextFunction) => { if (req.isAuthenticated()) { @@ -23,12 +23,12 @@ export const requireNotAuthenticated = (req: Request, res: Response, next: NextF }; const authenticateUser: VerifyFunction = async (email, password, done) => { - const user = await DbUser.findByEmail(email); + const user = await StoredUsers.findByEmail(email); if (!user) { return done(null, false, { message: "A user with that email does not exist."} ); } try { - if (await bcrypt.compare(password, user.password)) { + if (await bcrypt.compare(password, (await user.getLoginDetails()).password)) { return done(null, user); } else { return done(null, false, {message: "Password incorrect"}); @@ -41,11 +41,11 @@ const authenticateUser: VerifyFunction = async (email, password, done) => { export const initialisePassport = () => { passport.use(new LocalStrategy({ usernameField: "email" }, authenticateUser)); - passport.serializeUser((user: IDbUser, done) => { - done(null, user.id) + passport.serializeUser((user: StoredUser, done) => { + done(null, user.id()) }); passport.deserializeUser(async (id: string, done) => { - const user: IDbUser | null = await DbUser.getSerializedAuthUser(id); + const user: StoredUser | null = await StoredUsers.getSerializedAuthUser(id); done(null, user); }); }; \ No newline at end of file diff --git a/src/routers/mainRouter.ts b/src/routers/mainRouter.ts index e6dc5e8..8c44ac2 100755 --- a/src/routers/mainRouter.ts +++ b/src/routers/mainRouter.ts @@ -1,8 +1,8 @@ import express from "express"; import {requireAuthenticated} from "../passport-config"; import routers from "./routers"; -import {IDbUser} from "../models/dbUser_old"; import {ModelParameterError} from "../models/utils"; +import {LoginDetails} from "../models/StoredUser"; const router = express.Router(); @@ -10,13 +10,13 @@ router.use("/account", routers.signup); router.use("/api", routers.api); router.get("/game", requireAuthenticated, (req, res) => { res.render("gameIndex.ejs", { - username: (req.user as IDbUser).username, + username: (req.user as LoginDetails).username, rootUrl: req.app.locals.rootUrl }); }); router.get("/**", requireAuthenticated, (req, res) => { res.render("frontendIndex.ejs", { - username: (req.user as IDbUser).username, + username: (req.user as LoginDetails).username, rootUrl: req.app.locals.rootUrl }); }); diff --git a/src/rulesets.ts b/src/rulesets.ts index 6be4eed..e6c2b80 100755 --- a/src/rulesets.ts +++ b/src/rulesets.ts @@ -1,36 +1,38 @@ -import { FieldType } from "./enums"; +import {FieldType} from "./enums"; +import RulesetsPage from "../../frontend/src/Components/RulesetsPage"; +import {RulesetStats} from "./models/Stats"; export const defaultCellValues = { - [FieldType.number]: 0, - [FieldType.bool]: false, - [FieldType.subtotal]: 0, - [FieldType.total]: 0, - [FieldType.bonus]: 0, - [FieldType.superkadi]: 0, - [FieldType.multiplier]: 0, + [FieldType.number]: 0, + [FieldType.bool]: false, + [FieldType.subtotal]: 0, + [FieldType.total]: 0, + [FieldType.bonus]: 0, + [FieldType.superkadi]: 0, + [FieldType.multiplier]: 0, }; export interface Ruleset { - id: string; - label: string; - blocks: Record; + id: string; + label: string; + blocks: Record; } export type BlockDef = BonusBlockDef | NoBonusBlockDef; export interface NoBonusBlockDef extends DefaultBlockDef { - hasBonus: false; + hasBonus: false; } export interface BonusBlockDef extends DefaultBlockDef { - hasBonus: true; - bonusScore: number; - bonusFor: number; + hasBonus: true; + bonusScore: number; + bonusFor: number; } interface DefaultBlockDef { - label: string; - cells: Record; + label: string; + cells: Record; } export type CellDef = @@ -40,246 +42,152 @@ export type CellDef = | SuperkadiCellDef; export interface BoolCellDef extends DefaultCellDef { - fieldType: FieldType.bool; - score: number; + fieldType: FieldType.bool; + score: number; } export interface MultiplierCellDef extends DefaultCellDef { - fieldType: FieldType.multiplier; - multiplier: number; - maxMultiples: number; + fieldType: FieldType.multiplier; + multiplier: number; + maxMultiples: number; } export interface SuperkadiCellDef extends DefaultCellDef { - fieldType: FieldType.superkadi; - score: number; - maxSuperkadis: number; + fieldType: FieldType.superkadi; + score: number; + maxSuperkadis: number; } export interface NumberCellDef extends DefaultCellDef { - fieldType: FieldType.number; + fieldType: FieldType.number; } interface DefaultCellDef { - label: string; + label: string; } // ----- Predefined sets +export const DEFAULT_RULESET_NAME = "DEFAULT_RULESET"; const defaultDiceCount = 5; -const DEFAULT_RULESET = "DEFAULT_RULESET"; const gameSchemas: Ruleset[] = [ - { - id: DEFAULT_RULESET, - label: "Standard Kadi Rules (en)", - blocks: { - top: { - label: "Upper", - hasBonus: true, - bonusScore: 35, - bonusFor: 63, - cells: { - aces: { - fieldType: FieldType.multiplier, - label: "Aces", - multiplier: 1, - maxMultiples: defaultDiceCount, - }, + { + id: DEFAULT_RULESET_NAME, + label: "Standard Kadi Rules (en)", + blocks: { + top: { + label: "Upper", + hasBonus: true, + bonusScore: 35, + bonusFor: 63, + cells: { + aces: { + fieldType: FieldType.multiplier, + label: "Aces", + multiplier: 1, + maxMultiples: defaultDiceCount, + }, - twos: { - fieldType: FieldType.multiplier, - label: "Twos", - multiplier: 2, - maxMultiples: defaultDiceCount, - }, + twos: { + fieldType: FieldType.multiplier, + label: "Twos", + multiplier: 2, + maxMultiples: defaultDiceCount, + }, - threes: { - fieldType: FieldType.multiplier, - label: "Threes", - multiplier: 3, - maxMultiples: defaultDiceCount, - }, + threes: { + fieldType: FieldType.multiplier, + label: "Threes", + multiplier: 3, + maxMultiples: defaultDiceCount, + }, - fours: { - fieldType: FieldType.multiplier, - label: "Fours", - multiplier: 4, - maxMultiples: defaultDiceCount, - }, + fours: { + fieldType: FieldType.multiplier, + label: "Fours", + multiplier: 4, + maxMultiples: defaultDiceCount, + }, - fives: { - fieldType: FieldType.multiplier, - label: "Fives", - multiplier: 5, - maxMultiples: defaultDiceCount, - }, + fives: { + fieldType: FieldType.multiplier, + label: "Fives", + multiplier: 5, + maxMultiples: defaultDiceCount, + }, - sixes: { - fieldType: FieldType.multiplier, - label: "Sixes", - multiplier: 6, - maxMultiples: defaultDiceCount, - }, + sixes: { + fieldType: FieldType.multiplier, + label: "Sixes", + multiplier: 6, + maxMultiples: defaultDiceCount, + }, + }, + }, + bottom: { + label: "Lower", + hasBonus: false, + cells: { + threeKind: { + fieldType: FieldType.number, + label: "Three of a Kind", + }, + + fourKind: { + fieldType: FieldType.number, + label: "Four of a Kind", + }, + + fullHouse: { + fieldType: FieldType.bool, + label: "Full House", + score: 25, + }, + + smlStraight: { + fieldType: FieldType.bool, + label: "Small Straight", + score: 30, + }, + + lgSraight: { + fieldType: FieldType.bool, + label: "Large Straight", + score: 40, + }, + + superkadi: { + fieldType: FieldType.superkadi, + label: "Super Kadis", + score: 50, + maxSuperkadis: 5, + }, + + chance: { + fieldType: FieldType.number, + label: "Chance", + }, + }, + }, }, - }, - bottom: { - label: "Lower", - hasBonus: false, - cells: { - threeKind: { - fieldType: FieldType.number, - label: "Three of a Kind", - }, - - fourKind: { - fieldType: FieldType.number, - label: "Four of a Kind", - }, - - fullHouse: { - fieldType: FieldType.bool, - label: "Full House", - score: 25, - }, - - smlStraight: { - fieldType: FieldType.bool, - label: "Small Straight", - score: 30, - }, - - lgSraight: { - fieldType: FieldType.bool, - label: "Large Straight", - score: 40, - }, - - kadi: { - fieldType: FieldType.superkadi, - label: "Super Kadis", - score: 50, - maxSuperkadis: 5, - }, - - chance: { - fieldType: FieldType.number, - label: "Chance", - }, - }, - }, - }, - }, - { - id: DEFAULT_RULESET, - label: "Standard-Kadi-Regelwerk (de)", - blocks: { - top: { - label: "Oben", - hasBonus: true, - bonusScore: 35, - bonusFor: 63, - cells: { - aces: { - fieldType: FieldType.multiplier, - label: "Einser", - multiplier: 1, - maxMultiples: defaultDiceCount, - }, - - twos: { - fieldType: FieldType.multiplier, - label: "Zweier", - multiplier: 2, - maxMultiples: defaultDiceCount, - }, - - threes: { - fieldType: FieldType.multiplier, - label: "Dreier", - multiplier: 3, - maxMultiples: defaultDiceCount, - }, - - fours: { - fieldType: FieldType.multiplier, - label: "Vierer", - multiplier: 4, - maxMultiples: defaultDiceCount, - }, - - fives: { - fieldType: FieldType.multiplier, - label: "Fünfer", - multiplier: 5, - maxMultiples: defaultDiceCount, - }, - - sixes: { - fieldType: FieldType.multiplier, - label: "Sechser", - multiplier: 6, - maxMultiples: defaultDiceCount, - }, - }, - }, - bottom: { - label: "Unten", - hasBonus: false, - cells: { - threeKind: { - fieldType: FieldType.number, - label: "Dreierpasch", - }, - fourKind: { - fieldType: FieldType.number, - label: "Viererpasch", - }, - fullSouse: { - fieldType: FieldType.bool, - label: "Full House", - score: 25, - }, - smlStraight: { - fieldType: FieldType.bool, - label: "Kleine Straße", - score: 30, - }, - lgStraight: { - fieldType: FieldType.bool, - label: "Große Straße", - score: 40, - }, - kadi: { - fieldType: FieldType.superkadi, - label: "Ultrakadi", - score: 50, - maxSuperkadis: 5, - }, - change: { - fieldType: FieldType.number, - label: "Chance", - }, - }, - }, - }, - }, + } ]; export function getGameSchemaById(schemaId: string): Ruleset { - for (const schema of gameSchemas) { - if (schema.id === schemaId) { - return schema; + for (const schema of gameSchemas) { + if (schema.id === schemaId) { + return schema; + } } - } - throw new RangeError("No such GameSchema with id '" + schemaId + "'!"); + throw new RangeError("No such GameSchema with id '" + schemaId + "'!"); } export interface SchemaListing { - id: string; - label: string; + id: string; + label: string; } export function getSchemaListings(): SchemaListing[] { - return gameSchemas.map((s) => ({ id: s.id, label: s.label })); + return gameSchemas.map((s) => ({id: s.id, label: s.label})); }