Big update progress on encapsulating models

This commit is contained in:
Daniel Ledda
2020-07-16 08:05:37 +02:00
parent 0bddaa524b
commit cef6249c09
20 changed files with 522 additions and 515 deletions

View File

@@ -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.`);
}
}
}

View File

@@ -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;

View File

@@ -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<string, ScoreCellJSONRepresentation>;
}
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<string, CellDef>): ScoreCell[] {
private static generateCells(cellDefs: Record<string, CellDef>): 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;
export default ScoreBlockCalculator;

View File

@@ -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<string, BlockDef>): ScoreBlock[] {
private static generateBlocks(blockDefs: Record<string, BlockDef>): 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;

View File

@@ -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;
export default ScoreCellCalculator;

119
src/models/stats.ts → src/classes/StatsUpdater.ts Executable file → Normal file
View File

@@ -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<string, RulesetStats>
gamesPlayed: number;
}
interface RulesetStats {
blockStats: Record<string, BlockStats>;
wins: number;
runnerUps: number;
draws: number;
losses: number;
grandTotal: TotalFieldStats;
}
interface BlockStats {
cellStats: Record<string, CellStats>;
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<string, BlockResults>;
}
interface BlockResults {
cells: Record<string, CellResults>
}
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.`);
}
}
}
export default StatsUpdater;

View File

@@ -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;

29
src/errors.ts Normal file
View File

@@ -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";
}
}

View File

@@ -0,0 +1,15 @@
import StoredObject from "./StoredObject";
abstract class MongoStoredObject<RawDataInterface> implements StoredObject {
protected constructor(protected data: {_id: string} & RawDataInterface) {}
id(): string {
return this.data._id;
}
rawData(): RawDataInterface {
return this.data;
}
}
export default MongoStoredObject;

View File

@@ -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<IRawData, IStoredObject extends StoredObject>
implements StoredObjectCollection<IStoredObject> {
protected mongoDbClientCollection: mongo.Collection;
protected StoredObjectConstructor: StoredObjectConstructor<IRawData, IStoredObject>;
protected constructor(
collectionClient: mongo.Collection,
objectConstructor: StoredObjectConstructor<IRawData, IStoredObject>
) {
this.mongoDbClientCollection = collectionClient;
this.StoredObjectConstructor = objectConstructor;
}
private async create(objectData: Omit<IRawData, "_id">): Promise<IRawData> {
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<IRawData, "_id">): Promise<IStoredObject> {
return new this.StoredObjectConstructor(await this.create(objectData));
}
protected async findObjectById(id: string): Promise<IRawData | null> {
return tryQuery(async () =>
await this.mongoDbClientCollection.findOne({_id: id})
);
}
protected async findObjectByAttribute(attribute: string, value: any): Promise<IRawData | null> {
return tryQuery(async () =>
await this.mongoDbClientCollection.findOne({[attribute]: value})
);
}
async deleteById(objectId: StoredObjectId, returnObject?: boolean): Promise<IStoredObject | null | void> {
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<IStoredObject | null> {
const data = await this.findObjectById(id);
if (data) {
return new this.StoredObjectConstructor(data);
}
else {
return null;
}
};
async save(...objects: IStoredObject[]): Promise<void> {
await tryQuery(async () => {
for (const object of objects) {
await this.mongoDbClientCollection.findOneAndUpdate({_id: object.id()}, {...object.rawData()});
}
});
}
}
export default MongoStoredObjectCollection;

100
src/models/Stats.ts Executable file
View File

@@ -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<string, RulesetStats> & { [DEFAULT_RULESET_NAME]: DefaultRulesetStats }
gamesPlayed: number;
}
export interface RulesetStats {
blockStats: Record<string, BlockStats>;
wins: number;
runnerUps: number;
draws: number;
losses: number;
grandTotal: TotalFieldStats;
}
interface BlockStats {
cellStats: Record<string, CellStats>;
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<string, BlockResults>;
}
interface BlockResults {
cells: Record<string, CellResults>
}
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,
};

View File

@@ -0,0 +1,9 @@
export type StoredObjectConstructor<Data, Object> = new (data: Data, ...args: any[]) => Object;
export type StoredObjectId = string;
interface StoredObject {
id(): string;
rawData(): any;
}
export default StoredObject;

View File

@@ -0,0 +1,9 @@
import StoredObject, {StoredObjectId} from "./StoredObject";
interface StoredObjectCollection<StoredObjectInterface extends StoredObject> {
findById(id: string): Promise<StoredObjectInterface | null>;
deleteById(objectId: StoredObjectId, returnObject?: boolean): Promise<StoredObjectInterface | null | void>;
save(...objects: StoredObjectInterface[]): Promise<void>;
}
export default StoredObjectCollection;

View File

@@ -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<StoredPlayer> {
newPlayer(nick: string): Promise<StoredPlayer>;
}
class MongoStoredPlayerCollection
@@ -36,10 +34,9 @@ class MongoStoredPlayerCollection
this.updater = new PlayerStatsUpdater();
}
newPlayer(nick: string): Promise<StoredPlayer> {
return tryQuery(async () => {
return this.create({nick});
});
async newPlayer(nick: string): Promise<StoredPlayer> {
const newPlayer = {nick, stats: Stats.makeBlankDataFields()};
return this.newObject(newPlayer);
}
}

View File

@@ -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<StoredUserData> implements StoredUser {
constructor(data: StoredUserData) {
@@ -165,17 +165,6 @@ class MongoStoredUserCollection extends MongoStoredObjectCollection<StoredUserDa
);
}
private async addNewUser(loginDetails: LoginDetails): Promise<StoredUser> {
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<StoredUser> {
const usernameTaken = await this.userWithUsernameExists(loginDetails.username);
const emailTaken = await this.userWithEmailExists(loginDetails.email);
@@ -183,13 +172,23 @@ class MongoStoredUserCollection extends MongoStoredObjectCollection<StoredUserDa
throw new CredentialsTakenError(usernameTaken, emailTaken);
}
else {
const hashedPassword = await bcrypt.hash(loginDetails.password, 10);
return tryQuery(() =>
this.addNewUser({...loginDetails, password: hashedPassword})
);
return this.addNewUser({...loginDetails})
}
}
private async addNewUser(loginDetails: LoginDetails): Promise<StoredUser> {
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<boolean> {
const object = await this.findObjectByAttribute("email", email);
return object !== null;
@@ -206,9 +205,13 @@ class MongoStoredUserCollection extends MongoStoredObjectCollection<StoredUserDa
return dbResult;
}
else {
throw new GenericModelError("User not found!");
throw new GenericPersistenceError("User not found!");
}
}
async makePasswordSecure(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
}
const StoredUsers = new MongoStoredUserCollection(getMongoObjectCollection("users"));

View File

@@ -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 {

View File

@@ -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<K> {
findById(id: string): Promise<K | null>;
}
export abstract class MongoStoredObjectCollection<D, K extends StoredObject> implements StoredObjectCollection<K> {
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<any | null> {
return tryQuery(async () =>
await this.mongoDbClientCollection.findOne({_id: id})
);
}
protected async findObjectByAttribute(attribute: string, value: any): Promise<any | null> {
return tryQuery(async () =>
await this.mongoDbClientCollection.findOne({attribute: value})
);
}
protected async create(objectData: Partial<D>): Promise<K> {
return tryQuery(async () =>
await this.mongoDbClientCollection.insertOne(objectData)
);
}
async deleteById(objectId: StoredObjectId): Promise<K | null> {
const deletedObject = this.findById(objectId);
await this.mongoDbClientCollection.deleteOne({_id: objectId});
return deletedObject;
}
async findById(id: string): Promise<K | null> {
const data = await this.findObjectById(id);
return new this.MongoStoredObject(data);
};
async save(...objects: K[]): Promise<void> {
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<T> 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 = <T>(query: () => T) => Promise<any>;
type CallbackWrapper = <T>(query: () => T) => Promise<T>;
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);
}
};

View File

@@ -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);
});
};

View File

@@ -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
});
});

View File

@@ -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<string, BlockDef>;
id: string;
label: string;
blocks: Record<string, BlockDef>;
}
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<string, CellDef>;
label: string;
cells: Record<string, CellDef>;
}
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}));
}