Refactors the backend code to make more use of the mongodb. Drops support for the legacy v1 api

parent 124c27b8
......@@ -15,7 +15,6 @@ import { AdminRouter } from './routers/rest/AdminRouter';
import { ApiRouter } from './routers/rest/ApiRouter';
import { ExpiryQuizRouter } from './routers/rest/ExpiryQuizRouter';
import { I18nApiRouter } from './routers/rest/I18nApiRouter';
import { LegacyApiRouter } from './routers/rest/LegacyApi';
import { LibRouter } from './routers/rest/LibRouter';
import { LobbyRouter } from './routers/rest/LobbyRouter';
import { MemberRouter } from './routers/rest/MemberRouter';
......@@ -23,8 +22,6 @@ import { NicksRouter } from './routers/rest/NicksRouter';
import { QuizRouter } from './routers/rest/QuizRouter';
import { dynamicStatistics, staticStatistics } from './statistics';
declare var global: any;
export const routingControllerOptions: RoutingControllersOptions = {
defaults: {
nullResultCode: 405,
......@@ -37,7 +34,7 @@ export const routingControllerOptions: RoutingControllersOptions = {
defaultErrorHandler: false,
cors: options,
controllers: [
AdminRouter, ApiRouter, ExpiryQuizRouter, I18nApiRouter, LegacyApiRouter, LibRouter, LobbyRouter, MemberRouter, NicksRouter, QuizRouter,
AdminRouter, ApiRouter, ExpiryQuizRouter, I18nApiRouter, LibRouter, LobbyRouter, MemberRouter, NicksRouter, QuizRouter,
],
middlewares: [I18nMiddleware],
};
......@@ -103,7 +100,6 @@ class App {
new Integrations.OnUncaughtException(), new Integrations.OnUnhandledRejection(),
],
enabled: process.env.NODE_ENV === 'production',
debug: true,
});
}
}
......
......@@ -3,6 +3,7 @@ import { settings } from '../statistics';
class AMQPConnector {
private static _instance: AMQPConnector;
public readonly globalExchange: string = 'global';
private _channel: Channel;
......@@ -37,6 +38,13 @@ class AMQPConnector {
this.initConnection();
});
}
public buildQuizExchange(quizname: string): string {
if (!quizname) {
throw new Error(`Could not build exchange name. Quizname '${quizname}' is not supported.`);
}
return encodeURI(`quiz_${quizname.trim()}`);
}
}
export default AMQPConnector.getInstance();
import { EventEmitter } from 'events';
import { IStorageDAO } from '../interfaces/database/IStorageDAO';
export abstract class AbstractDAO<T> implements IStorageDAO<T> {
export abstract class AbstractDAO {
protected static instance;
protected _isInitialized: boolean;
......@@ -10,26 +9,12 @@ export abstract class AbstractDAO<T> implements IStorageDAO<T> {
return this._isInitialized;
}
protected _storage: T;
get storage(): T {
return this._storage;
}
private _updateEmitter = new EventEmitter();
get updateEmitter(): NodeJS.EventEmitter {
return this._updateEmitter;
}
protected constructor(storage: T) {
this._storage = storage;
}
public createDump(): any {
return this.storage;
}
protected isEmptyVars(...variables): boolean {
return variables.length > 0 && variables.filter(variable => this.isEmptyVar(variable)).length > 0;
}
......
import { ObjectID, ObjectId } from 'bson';
import { AssetEntity } from '../entities/AssetEntity';
import { DbCollection, DbEvent } from '../enums/DbOperation';
import { IAsset, IAssetSerialized } from '../interfaces/IAsset';
import LoggerService from '../services/LoggerService';
import { DeleteWriteOpResultObject } from 'mongodb';
import { Document } from 'mongoose';
import { IAssetSerialized } from '../interfaces/IAsset';
import { AssetModel, AssetModelItem } from '../models/AssetModel';
import { AbstractDAO } from './AbstractDAO';
import DbDAO from './DbDAO';
class AssetDAO extends AbstractDAO<Array<AssetEntity>> {
class AssetDAO extends AbstractDAO {
constructor() {
super([]);
super();
DbDAO.isDbAvailable.on(DbEvent.Connected, async (isConnected) => {
if (isConnected) {
const cursor = DbDAO.readMany(DbCollection.Assets, {});
cursor.forEach(doc => {
this.addAsset(doc);
}).then(() => LoggerService.info(`${this.constructor.name} initialized with ${this.storage.length} entries`));
}
AssetModel.find().exec().then(assets => {
assets.forEach(asset => this.addAsset(asset));
});
}
......@@ -29,46 +23,32 @@ class AssetDAO extends AbstractDAO<Array<AssetEntity>> {
return this.instance;
}
public addAsset(document: IAssetSerialized): void {
if (this.getAssetById(new ObjectId(document.id))) {
throw new Error(`Duplicate asset insertion: (id: ${document.id}, url: ${document.url})`);
}
const asset = new AssetEntity(document);
this.storage.push(asset);
this.updateEmitter.emit(DbEvent.Create, asset);
public addAsset(document: IAssetSerialized): Promise<Document & AssetModelItem> {
return AssetModel.create(document);
}
public updateAsset(id: ObjectId, updatedFields: any): void {
const asset = this.getAssetById(id);
if (!asset) {
throw new Error(`Unkown updated quiz: ${id.toHexString()}`);
}
Object.keys(updatedFields).forEach(key => asset[key] = updatedFields[key]);
this.updateEmitter.emit(DbEvent.Change, asset);
public updateAsset(id: ObjectId, updatedFields: any): Promise<Document & AssetModelItem> {
return AssetModel.findOneAndUpdate(id, updatedFields).exec();
}
public removeAllAssets(): void {
this.storage.forEach(asset => this.updateEmitter.emit(DbEvent.Delete, asset));
this.storage.splice(0, this.storage.length);
public removeAllAssets(): Promise<DeleteWriteOpResultObject['result'] & { deletedCount?: number }> {
return AssetModel.deleteMany({}).exec();
}
public removeAsset(id: ObjectId): void {
this.storage.splice(this.storage.findIndex(asset => asset.id.equals(id)), 1);
public removeAsset(id: ObjectId): Promise<DeleteWriteOpResultObject['result'] & { deletedCount?: number }> {
return AssetModel.deleteOne({ _id: id }).exec();
}
public getAssetByDigest(digest: string): IAsset {
return this.storage.find(val => val.digest === digest);
public getAssetByDigest(digest: string): Promise<AssetModelItem> {
return AssetModel.findOne({ digest }).exec();
}
public getAssetByUrl(url: string): IAsset {
return this.storage.find(asset => asset.url === url);
public getAssetByUrl(url: string): Promise<AssetModelItem> {
return AssetModel.findOne({ url }).exec();
}
private getAssetById(id: ObjectID): IAsset {
return this.storage.find(asset => asset.id.equals(id));
private getAssetById(id: ObjectID): Promise<AssetModelItem> {
return AssetModel.findById({ id }).exec();
}
}
......
import { ICasData } from '../interfaces/users/ICasData';
import { AbstractDAO } from './AbstractDAO';
class CasDAO extends AbstractDAO<{ [key: string]: ICasData }> {
class CasDAO extends AbstractDAO {
private _storage: object = {};
constructor() {
super({});
get storage(): object {
return this._storage;
}
public static getInstance(): CasDAO {
......
import { EventEmitter } from 'events';
import { Cursor, DeleteWriteOpResultObject, FilterQuery, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult } from 'mongodb';
import { Connection } from 'mongoose';
import { DbCollection, DbEvent } from '../enums/DbOperation';
import { IDbObject } from '../interfaces/database/IDbObject';
import { DbCollection } from '../enums/DbOperation';
import LoggerService from '../services/LoggerService';
import { AbstractDAO } from './AbstractDAO';
import MongoDBConnector from './MongoDBConnector';
class DbDAO extends AbstractDAO<object> {
class DbDAO extends AbstractDAO {
private static DB_RECONNECT_INTERVAL = 1000 * 60 * 5; // 5 Minutes
public readonly DB = MongoDBConnector.dbName;
......@@ -17,86 +14,19 @@ class DbDAO extends AbstractDAO<object> {
return this._dbCon;
}
private _isDbAvailable = new EventEmitter();
get isDbAvailable(): EventEmitter {
return this._isDbAvailable;
}
private _isConnected = false;
constructor(_storage: object) {
super(_storage);
constructor() {
super();
this.connectToDb();
}
public static getInstance(): DbDAO {
if (!this.instance) {
this.instance = new DbDAO({});
this.instance = new DbDAO();
}
return this.instance;
}
public create(collection: string, elem: IDbObject | object): Promise<InsertOneWriteOpResult<any>> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).insertOne(elem);
}
public readOne(collection: string, query: FilterQuery<any>, options?: FindOneOptions): Promise<any> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).findOne(query, options);
}
public readMany(collection: string, query: FilterQuery<any>): Cursor<any> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).find(query);
}
public updateOne(collection: string, query: FilterQuery<any>, update: object): Promise<UpdateWriteOpResult> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).updateOne(query, { $set: update });
}
public updateMany(collection: string, query: FilterQuery<any>, update: object): Promise<UpdateWriteOpResult> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).updateMany(query, { $set: update });
}
public deleteOne(collection: string, query: FilterQuery<any>): Promise<DeleteWriteOpResultObject> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).deleteOne(query);
}
public deleteMany(collection: string, query: FilterQuery<any>): Promise<DeleteWriteOpResultObject> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).deleteMany(query);
}
public clearStorage(): void {
}
private connectToDb(): Promise<void> {
return MongoDBConnector.connect(this.DB).then((db: Connection) => {
this._dbCon = db;
......@@ -115,20 +45,11 @@ class DbDAO extends AbstractDAO<object> {
});
LoggerService.info(`Db connected`);
this._isConnected = true;
this._isDbAvailable.emit(DbEvent.Connected, true);
db.on('error', () => {
this._isDbAvailable.emit(DbEvent.Connected, false);
});
db.on('error', () => {});
}).catch((error) => {
LoggerService.error(`Db connection failed with error ${error}, will retry in ${DbDAO.DB_RECONNECT_INTERVAL / 1000} seconds`);
this._isConnected = false;
this._isDbAvailable.emit(DbEvent.Connected, false);
setTimeout(this.connectToDb.bind(this), DbDAO.DB_RECONNECT_INTERVAL);
});
}
......
......@@ -6,14 +6,20 @@ import LoggerService from '../services/LoggerService';
import { availableLangs } from '../statistics';
import { AbstractDAO } from './AbstractDAO';
class I18nDAO extends AbstractDAO<object> {
class I18nDAO extends AbstractDAO {
private _storage: object;
get storage(): object {
return this._storage;
}
private readonly mergeRequestTitle = 'WIP: Update i18n keys';
private readonly commitMessage = 'Updates i18n keys';
private readonly gitlabAccessToken = process.env.GITLAB_TOKEN;
constructor(storage: object) {
super(storage);
super();
this._storage = storage;
}
public static getInstance(): I18nDAO {
......
import { AbstractDAO } from './AbstractDAO';
class MathjaxDAO extends AbstractDAO<object> {
class MathjaxDAO extends AbstractDAO {
private _storage: object = {};
constructor() {
super({});
get storage(): object {
return this._storage;
}
public static getInstance(): MathjaxDAO {
......
///<reference path="../lib/regExpEscape.ts" />
import { ObjectId } from 'bson';
import { MemberEntity } from '../entities/member/MemberEntity';
import { QuizEntity } from '../entities/quiz/QuizEntity';
import { DbCollection, DbEvent } from '../enums/DbOperation';
import { Document } from 'mongoose';
import { MessageProtocol, StatusProtocol } from '../enums/Message';
import { IMemberSerialized } from '../interfaces/entities/Member/IMemberSerialized';
import { IQuizEntity } from '../interfaces/quizzes/IQuizEntity';
import LoggerService from '../services/LoggerService';
import { IQuizResponse } from '../interfaces/quizzes/IQuizResponse';
import { MemberModel, MemberModelItem } from '../models/member/MemberModel';
import { AbstractDAO } from './AbstractDAO';
import DbDAO from './DbDAO';
import AMQPConnector from './AMQPConnector';
import QuizDAO from './quiz/QuizDAO';
class MemberDAO extends AbstractDAO<Array<MemberEntity>> {
class MemberDAO extends AbstractDAO {
public static getInstance(): MemberDAO {
if (!this.instance) {
......@@ -21,105 +18,192 @@ class MemberDAO extends AbstractDAO<Array<MemberEntity>> {
return this.instance;
}
constructor() {
super([]);
DbDAO.isDbAvailable.on(DbEvent.Connected, async (isConnected) => {
if (isConnected) {
const cursor = DbDAO.readMany(DbCollection.Members, {});
cursor.forEach(doc => {
this.addMember(doc);
}).then(() => LoggerService.info(`${this.constructor.name} initialized with ${this.storage.length} entries`));
}
});
}
public getMemberByName(name: string): MemberEntity {
return this.storage.find(val => val.name === name);
public getMemberByName(name: string): Promise<Document & MemberModelItem> {
return MemberModel.findOne({ name }).exec();
}
public addMember(memberSerialized: IMemberSerialized): void {
if (this.getMemberById(memberSerialized.id)) {
public async addMember(memberSerialized: IMemberSerialized): Promise<Document & MemberModelItem> {
if (memberSerialized.id && this.getMemberById(memberSerialized.id)) {
throw new Error(`Duplicate member insertion: (name: ${memberSerialized.name}, id: ${memberSerialized.id})`);
}
const member = new MemberEntity(memberSerialized);
this.storage.push(member);
this.updateEmitter.emit(DbEvent.Create, member);
if (QuizDAO.isInitialized) {
this.notifyQuizDAO(member);
} else {
QuizDAO.updateEmitter.once(DbEvent.Initialized, () => this.notifyQuizDAO(member));
}
}
public updateMember(id: ObjectId, updatedFields: { [key: string]: any }): void {
const member = this.getMemberById(id);
if (!member) {
throw new Error(`Unknown updated member: ${id.toHexString()}`);
}
const doc = await MemberModel.create(memberSerialized);
Object.keys(updatedFields).forEach(key => member[key] = updatedFields[key]);
AMQPConnector.channel.publish(AMQPConnector.buildQuizExchange(memberSerialized.currentQuizName), '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.Added,
payload: { member: doc.toJSON() },
})));
this.updateEmitter.emit(DbEvent.Change, member);
return doc;
}
public removeAllMembers(): void {
this.storage.forEach(member => {
this.updateEmitter.emit(DbEvent.Delete, member);
QuizDAO.getQuizByName(member.currentQuizName).onMemberRemoved(member);
public async removeAllMembers(): Promise<void> {
const members = await MemberModel.find().exec();
members.forEach(member => {
AMQPConnector.channel.publish(AMQPConnector.buildQuizExchange(member.currentQuizName), '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.Removed,
payload: { name: member.name },
})));
});
this.storage.splice(0, this.storage.length);
}
public removeMember(id: ObjectId | string): void {
const members = this.storage.splice(this.storage.findIndex(val => val.id.equals(id)), 1);
await MemberModel.deleteMany({}).exec();
}
if (members.length) {
this.updateEmitter.emit(DbEvent.Delete, members[0]);
const quiz = QuizDAO.getQuizByName(members[0].currentQuizName);
if (quiz) {
quiz.onMemberRemoved(members[0]);
}
}
public async removeMember(id: ObjectId | string): Promise<void> {
const member = await MemberModel.findByIdAndRemove(id).exec();
AMQPConnector.channel.publish(AMQPConnector.buildQuizExchange(member.currentQuizName), '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.Removed,
payload: { name: member.name },
})));
}
public getMembersOfQuiz(quizName: string): Array<MemberEntity> {
return this.storage.filter(val => !!val.currentQuizName.match(new RegExp(`^${RegExp.escape(quizName)}$`, 'i')));
public getMembersOfQuiz(quizName: string): Promise<Array<Document & MemberModelItem>> {
return MemberModel.find({ currentQuizName: quizName }).exec();
}
public getMemberByToken(token: string): MemberEntity {
return this.storage.find(val => val.token === token);
public getMemberByToken(token: string): Promise<Document & MemberModelItem> {
return MemberModel.findOne({ token }).exec();
}
public removeMembersOfQuiz(removedQuiz: QuizEntity | IQuizEntity): void {
DbDAO.deleteMany(DbCollection.Members, { currentQuizName: removedQuiz.name });
public async removeMembersOfQuiz(quizName: string): Promise<void> {
const members = await MemberModel.find({ currentQuizName: quizName }).exec();
members.forEach(member => {
AMQPConnector.channel.publish(AMQPConnector.buildQuizExchange(member.currentQuizName), '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.Removed,
payload: { name: member.name },
})));
});
await MemberModel.deleteMany({ currentQuizName: quizName }).exec();
}
public getMemberAmountPerQuizGroup(name: string, groups: Array<string>): object {
public async getMemberAmountPerQuizGroup(name: string, groups: Array<string>): Promise<object> {
const result = {};
groups.forEach(g => result[g] = 0);
this.getMembersOfQuiz(name).forEach(member => {
(await this.getMembersOfQuiz(name)).forEach(member => {
result[member.groupName]++;
});
return result;
}
private notifyQuizDAO(member: MemberEntity): void {
const quiz = QuizDAO.getQuizByName(member.currentQuizName);
if (!quiz) {
console.error(`The quiz '${member.currentQuizName}' for the member ${member.name} could not be found. Removing member.`);
DbDAO.deleteOne(DbCollection.Members, { _id: member.id });
return;
public resetMembersOfQuiz(name: string, questionAmount: number): Promise<any> {
return MemberModel.updateMany({ currentQuizName: name }, {
responses: this.generateResponseForQuiz(questionAmount),
}).exec();
}
public async setReadingConfirmation(member: Document & MemberModelItem): Promise<void> {
const quiz = await QuizDAO.getQuizByName(member.currentQuizName);
member.responses[quiz.currentQuestionIndex].readingConfirmation = true;
const queryPath = `responses.${quiz.currentQuestionIndex}.readingConfirmation`;
await MemberModel.updateOne({ _id: member._id }, { [queryPath]: true }).exec();
AMQPConnector.channel.publish(AMQPConnector.buildQuizExchange(quiz.name), '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.UpdatedResponse,
payload: {
nickname: member.name,
questionIndex: quiz.currentQuestionIndex,
update: { readingConfirmation: true },
},
})));
}
public async setConfidenceValue(member: Document & MemberModelItem, confidenceValue: number): Promise<void> {
const quiz = await QuizDAO.getQuizByName(member.currentQuizName);
member.responses[quiz.currentQuestionIndex].confidence = confidenceValue;
const queryPath = `responses.${quiz.currentQuestionIndex}.confidence`;
await MemberModel.updateOne({ _id: member._id }, { [queryPath]: confidenceValue }).exec();
AMQPConnector.channel.publish(AMQPConnector.buildQuizExchange(quiz.name), '.*', Buffer.from(JSON.stringify({
status: StatusProtocol.Success,
step: MessageProtocol.UpdatedResponse,
payload: {
nickname: member.name,
questionIndex: quiz.currentQuestionIndex,
update: { confidence: confidenceValue },
},
})));