Database Connection Update #4

Merged
humenius merged 49 commits from feature/database-connection into master 2021-02-08 03:16:51 +01:00
14 changed files with 169 additions and 116 deletions
Showing only changes of commit e3951d4793 - Show all commits

View File

@@ -1,3 +1,3 @@
export interface Predicate<T, O> { export interface Predicate<T, O> {
process(input: T): O; process(input: T): O;
} }

View File

@@ -1,13 +1,13 @@
export class Sort { export class Sort {
public static descending = (a: number, b: number) => { public static descending = (a: number, b: number) => {
if (a < b) { if (a < b) {
return 1; return 1;
}
if (a > b) {
return -1;
}
return 0;
} }
}
if (a > b) {
return -1;
}
return 0;
};
}

View File

@@ -1,11 +1,10 @@
export class TimeUtil { export class TimeUtil {
public static humanize = (seconds: number) => {
public static humanize = (seconds: number) => { const d = Math.floor(seconds / (3600 * 24));
const d = Math.floor(seconds / (3600 * 24)); const h = Math.floor((seconds % (3600 * 24)) / 3600);
const h = Math.floor(seconds % (3600 * 24) / 3600); const m = Math.floor((seconds % 3600) / 60);
const m = Math.floor(seconds % 3600 / 60); const s = Math.floor(seconds % 60);
const s = Math.floor(seconds % 60);
return `${d}d ${h}h ${m}m ${s}s`;
return `${d}d ${h}h ${m}m ${s}s`; };
} }
}

View File

@@ -1,9 +1,17 @@
import { Controller, Get, HostParam, HttpException, HttpStatus, Param, Req } from '@nestjs/common'; import {
Controller,
Get,
HostParam,
HttpException,
HttpStatus,
Param,
Req,
} from '@nestjs/common';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { DatabaseService } from './database/database.service'; import { DatabaseService } from './database/database.service';
import logger from './logger/Logger'; import logger from './logger/Logger';
import { TableEntry } from "./models/TableEntry"; import { TableEntry } from './models/TableEntry';
import { SinusBotService } from "./services/sinusbot.service"; import { SinusBotService } from './services/sinusbot.service';
import { UserStatsResponse } from './models/aliases'; import { UserStatsResponse } from './models/aliases';
@Controller() @Controller()
@@ -11,7 +19,7 @@ export class AppController {
constructor( constructor(
private readonly appService: AppService, private readonly appService: AppService,
private readonly sinusBotService: SinusBotService, private readonly sinusBotService: SinusBotService,
private readonly databaseService: DatabaseService private readonly databaseService: DatabaseService,
) {} ) {}
@Get() @Get()
@@ -26,13 +34,16 @@ export class AppController {
@Get('/stats/season/:id') @Get('/stats/season/:id')
async getStats(@Param('id') id: string): Promise<UserStatsResponse> { async getStats(@Param('id') id: string): Promise<UserStatsResponse> {
return this.databaseService.fetchStats(id) return this.databaseService
.then(value => { return value; }) .fetchStats(id)
.then(value => {
return value;
})
.catch(err => { .catch(err => {
logger.error(`Error occured when fetching stats.`, err); logger.error(`Error occured when fetching stats.`, err);
throw new HttpException( throw new HttpException(
'Error when fetching stats. Contact administrator or try again later!', 'Error when fetching stats. Contact administrator or try again later!',
HttpStatus.INTERNAL_SERVER_ERROR HttpStatus.INTERNAL_SERVER_ERROR,
); );
}); });
} }

View File

@@ -10,7 +10,13 @@ import { TimeTrackerPredicate } from './database/timetracking.predicate';
@Module({ @Module({
imports: [], imports: [],
controllers: [AppController], controllers: [AppController],
providers: [AppService, SinusBotService, DatabaseService, PrismaService, TimeTrackerPredicate], providers: [
AppService,
SinusBotService,
DatabaseService,
PrismaService,
TimeTrackerPredicate,
],
}) })
export class AppModule implements NestModule { export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): any { configure(consumer: MiddlewareConsumer): any {

View File

@@ -1,6 +1,10 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TimeTrackerPredicate } from './timetracking.predicate'; import { TimeTrackerPredicate } from './timetracking.predicate';
import { SeasonInfo, TimeTrackerStats, UserStatsResponse } from '../models/aliases'; import {
SeasonInfo,
TimeTrackerStats,
UserStatsResponse,
} from '../models/aliases';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
@Injectable() @Injectable()
@@ -16,8 +20,7 @@ export class DatabaseService {
constructor( constructor(
private readonly prismaClient: PrismaService, private readonly prismaClient: PrismaService,
private readonly timetrackerPredicate: TimeTrackerPredicate, private readonly timetrackerPredicate: TimeTrackerPredicate,
) { ) {}
}
public fetchSeasonInfos = async (seasonId: string): Promise<SeasonInfo> => { public fetchSeasonInfos = async (seasonId: string): Promise<SeasonInfo> => {
let seasons; let seasons;

View File

@@ -1,23 +1,26 @@
import { ranks, seasons, timetracker, user } from "@prisma/client"; import { ranks, seasons, timetracker, user } from '@prisma/client';
import { Predicate } from "src/api/predicate"; import { Predicate } from 'src/api/predicate';
import { Sort } from "src/api/sort"; import { Sort } from 'src/api/sort';
import { TimeUtil } from "src/api/timeutil"; import { TimeUtil } from 'src/api/timeutil';
import { TableEntry } from "src/models/TableEntry"; import { TableEntry } from 'src/models/TableEntry';
import { Injectable } from '@nestjs/common';
type UserStats = timetracker & { user: user, ranks: ranks; }; type UserStats = timetracker & { user: user; ranks: ranks };
export class TimeTrackerPredicate implements Predicate<UserStats[], TableEntry[]> { @Injectable()
export class TimeTrackerPredicate
process(input: UserStats[]): TableEntry[] { implements Predicate<UserStats[], TableEntry[]> {
return input.filter((userStats: UserStats) => userStats.time != null) process(input: UserStats[]): TableEntry[] {
.map((userStats: UserStats) => { return input
return { .filter((userStats: UserStats) => userStats.time != null)
name: userStats.user.name, .map((userStats: UserStats) => {
rawTime: userStats.time, return {
onlineTime: TimeUtil.humanize(userStats.time), name: userStats.user.name,
rank: userStats.ranks.rank_name rawTime: userStats.time,
} onlineTime: TimeUtil.humanize(userStats.time),
}) rank: userStats.ranks.rank_name,
.sort((lhs, rhs) => Sort.descending(lhs.rawTime, rhs.rawTime)); };
} })
} .sort((lhs, rhs) => Sort.descending(lhs.rawTime, rhs.rawTime));
}
}

View File

@@ -13,7 +13,7 @@ export class LoggerMiddleware implements NestMiddleware {
const contentLength = response.get('content-length'); const contentLength = response.get('content-length');
logger.info( logger.info(
`${method} ${url} ${statusCode} ${contentLength} - ${userAgent} ${ip}` `${method} ${url} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
); );
}); });

View File

@@ -1,11 +1,11 @@
import * as winston from 'winston'; import * as winston from 'winston';
const transports = { const transports = {
console: new winston.transports.Console() console: new winston.transports.Console(),
}; };
const logger = winston.createLogger({ const logger = winston.createLogger({
transports: [transports.console] transports: [transports.console],
}) });
export default logger; export default logger;

View File

@@ -4,7 +4,7 @@ interface RequestError extends Error {
} }
interface RequestErrorConstructor extends ErrorConstructor { interface RequestErrorConstructor extends ErrorConstructor {
new(message?: string): RequestError; new (message?: string): RequestError;
(message?: string): RequestError; (message?: string): RequestError;
readonly prototype: RequestError; readonly prototype: RequestError;
} }

View File

@@ -1,4 +1,5 @@
export interface TableEntry { export interface TableEntry {
name: string; name: string;
onlineTime: string; rank: string;
onlineTime: string;
} }

View File

@@ -1,6 +1,6 @@
import { ranks, seasons, timetracker, user } from '@prisma/client'; import { ranks, seasons, timetracker, user } from '@prisma/client';
import { TableEntry } from './TableEntry'; import { TableEntry } from './TableEntry';
export type TimeTrackerStats = timetracker & { user: user; ranks: ranks; } export type TimeTrackerStats = timetracker & { user: user; ranks: ranks };
export type SeasonInfo = seasons & { maxSeasonId: number } export type SeasonInfo = seasons & { maxSeasonId: number };
export type UserStatsResponse = SeasonInfo & TableEntry[] export type UserStatsResponse = SeasonInfo & TableEntry[];

View File

@@ -11,4 +11,4 @@ export class PrismaService extends PrismaClient
async onModuleDestroy() { async onModuleDestroy() {
await this.$disconnect(); await this.$disconnect();
} }
} }

View File

@@ -1,22 +1,22 @@
import { TableEntry } from "../models/TableEntry"; import { TableEntry } from '../models/TableEntry';
import { TunakillUser } from "../models/TunakillUser"; import { TunakillUser } from '../models/TunakillUser';
import {HttpException, HttpStatus, Injectable} from "@nestjs/common"; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {TunakillLogin} from "../models/TunakillLogin"; import { TunakillLogin } from '../models/TunakillLogin';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import logger from "../logger/Logger"; import logger from '../logger/Logger';
@Injectable() @Injectable()
export class SinusBotService { export class SinusBotService {
private host = process.env.HOST private host = process.env.HOST;
private credentials = { private credentials = {
username: process.env.SINUSBOT_USER, username: process.env.SINUSBOT_USER,
password: process.env.SINUSBOT_PASSWORD password: process.env.SINUSBOT_PASSWORD,
} };
private botInfo = { private botInfo = {
id: undefined, id: undefined,
instanceId: process.env.SINUSBOT_INSTANCEID instanceId: process.env.SINUSBOT_INSTANCEID,
} };
private botIdURL = `${this.host}/api/v1/botId`; private botIdURL = `${this.host}/api/v1/botId`;
private tunaKillURL = `${this.host}/api/v1/bot/i/${this.botInfo.instanceId}/event/tunakill_rank_all_user`; private tunaKillURL = `${this.host}/api/v1/bot/i/${this.botInfo.instanceId}/event/tunakill_rank_all_user`;
@@ -34,23 +34,30 @@ export class SinusBotService {
// if (this.bearer == null) { // if (this.bearer == null) {
logger.info(`Hey! I'm trying to get my Bearer token!`); logger.info(`Hey! I'm trying to get my Bearer token!`);
await this.login() await this.login()
.then(token => this.bearer = token) .then(token => (this.bearer = token))
.then(() => { .then(() => {
logger.debug( logger.debug(
`Seems like I have my Bearer token! `Seems like I have my Bearer token!
Looks like it's ${this.bearer == null ? 'undefined' : 'not undefined'}` Looks like it's ${
this.bearer == null ? 'undefined' : 'not undefined'
}`,
); );
}); });
// } // }
logger.info(`I try to fetch user data now! The URL is called ${this.tunaKillURL}`); logger.info(
`I try to fetch user data now! The URL is called ${this.tunaKillURL}`,
);
return await fetch(this.tunaKillURL, this.requestConfig(null, this.bearer)) return await fetch(this.tunaKillURL, this.requestConfig(null, this.bearer))
.then(res => this.checkStatus(res)) .then(res => this.checkStatus(res))
.then(res => res.json()) .then(res => res.json())
.then(data => this.consumeTunakillResponse(data)) .then(data => this.consumeTunakillResponse(data))
.catch(error => { .catch(error => {
logger.error(`I couldn't fetch user data.`, error); logger.error(`I couldn't fetch user data.`, error);
throw this.createHttpException(HttpStatus.INTERNAL_SERVER_ERROR, error.message); throw this.createHttpException(
HttpStatus.INTERNAL_SERVER_ERROR,
error.message,
);
}); });
} }
@@ -61,12 +68,15 @@ export class SinusBotService {
if (this.botInfo.id == null) { if (this.botInfo.id == null) {
logger.debug(`I have to fetch a bot ID before I can continue!`); logger.debug(`I have to fetch a bot ID before I can continue!`);
await this.fetchDefaultBotId() await this.fetchDefaultBotId()
.then(defaultBotId => this.botInfo.id = defaultBotId) .then(defaultBotId => (this.botInfo.id = defaultBotId))
.catch(error => { .catch(error => {
logger.warn(`I couldn't retrieve SinusBot bot information. Login is likely to fail!`, error); logger.warn(
`I couldn't retrieve SinusBot bot information. Login is likely to fail!`,
error,
);
throw this.createHttpException( throw this.createHttpException(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
`Could not fetch enough bot information for further requests.` `Could not fetch enough bot information for further requests.`,
); );
}); });
logger.info(`The bot ID now is ${this.botInfo.id}`); logger.info(`The bot ID now is ${this.botInfo.id}`);
@@ -75,20 +85,23 @@ export class SinusBotService {
const body: TunakillLogin = { const body: TunakillLogin = {
username: this.credentials.username, username: this.credentials.username,
password: this.credentials.password, password: this.credentials.password,
botId: this.botInfo.id botId: this.botInfo.id,
} };
logger.info(`Logging in for Bearer token!`) logger.info(`Logging in for Bearer token!`);
return await fetch(this.loginURL, this.requestConfig(JSON.stringify(body))) return await fetch(this.loginURL, this.requestConfig(JSON.stringify(body)))
.then(res => this.checkStatus(res)) .then(res => this.checkStatus(res))
.then(res => res.json()) .then(res => res.json())
.then(data => data.token) .then(data => data.token)
.catch(error => { .catch(error => {
logger.error(`Oh oh! Something went wrong while fetching Bearer token.`, error); logger.error(
`Oh oh! Something went wrong while fetching Bearer token.`,
error,
);
throw this.createHttpException( throw this.createHttpException(
HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED,
`Fetching Bearer token for Sinusbot failed. `Fetching Bearer token for Sinusbot failed.
Please refresh page or try again later!` Please refresh page or try again later!`,
); );
}); });
} }
@@ -110,39 +123,56 @@ export class SinusBotService {
throw Error('Response from SinusBot does not have any data to parse.'); throw Error('Response from SinusBot does not have any data to parse.');
} }
return response return (
// TODO: Remove hardcoded username filter for bots. response
.filter((user: TunakillUser) => user.name !== "Server Query Admin" && user.name !== "DJ Inshalla") // TODO: Remove hardcoded username filter for bots.
.filter(
(user: TunakillUser) =>
user.name !== 'Server Query Admin' && user.name !== 'DJ Inshalla',
)
.filter((user: TunakillUser) => user.time != null) .filter((user: TunakillUser) => user.time != null)
.map((user: TunakillUser) => { .map((user: TunakillUser) => {
return { return {
name: user.name, name: user.name,
rawTime: user.time, rawTime: user.time,
onlineTime: this.humanizeTime(user.time) onlineTime: this.humanizeTime(user.time),
}; };
}) })
.sort((a: any, b: any) => this.sortByDescendingTime(a.rawTime, b.rawTime)); .sort((a: any, b: any) =>
this.sortByDescendingTime(a.rawTime, b.rawTime),
)
);
} }
private requestConfig = (body?: string, bearerToken?: string, requestType: string = 'POST'): RequestInit => { private requestConfig = (
body?: string,
bearerToken?: string,
requestType: string = 'POST',
): RequestInit => {
return { return {
method: requestType, method: requestType,
body: body, body: body,
headers: { headers: {
'Accept': '*/*', Accept: '*/*',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'User-Agent': 'HumeniusTSRankingBackend/0.0.2', 'User-Agent': 'HumeniusTSRankingBackend/0.0.2',
'Authorization': `Bearer ${bearerToken}` Authorization: `Bearer ${bearerToken}`,
} },
}; };
} };
private createHttpException = (statusCode: HttpStatus, message?: string): HttpException => { private createHttpException = (
return new HttpException({ statusCode: HttpStatus,
status: statusCode, message?: string,
error: message ): HttpException => {
}, statusCode); return new HttpException(
} {
status: statusCode,
error: message,
},
statusCode,
);
};
private checkStatus = response => { private checkStatus = response => {
if (!response.ok) { if (!response.ok) {
@@ -151,7 +181,7 @@ export class SinusBotService {
throw err; throw err;
} }
return response; return response;
} };
private sortByDescendingTime = (a: number, b: number) => { private sortByDescendingTime = (a: number, b: number) => {
if (a < b) { if (a < b) {
@@ -163,16 +193,16 @@ export class SinusBotService {
} }
return 0; return 0;
} };
private humanizeTime = (seconds: number) => { private humanizeTime = (seconds: number) => {
const d = Math.floor(seconds / (3600 * 24)); const d = Math.floor(seconds / (3600 * 24));
const h = Math.floor(seconds % (3600 * 24) / 3600); const h = Math.floor((seconds % (3600 * 24)) / 3600);
const m = Math.floor(seconds % 3600 / 60); const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60); const s = Math.floor(seconds % 60);
return `${d}d ${h}h ${m}m ${s}s`; return `${d}d ${h}h ${m}m ${s}s`;
} };
private static logResponse(res: any) { private static logResponse(res: any) {
logger.debug(res); logger.debug(res);