From 75b1007ec0b9a041702c907cbcb32afe1eaa51cc Mon Sep 17 00:00:00 2001 From: Kalane Date: Thu, 16 Sep 2021 13:49:32 +0200 Subject: [PATCH] feat: refactor queries + add records query --- .../Controllers/Http/SummonersController.ts | 14 +++ server-v2/app/Models/MatchPlayer.ts | 9 ++ server-v2/app/Parsers/MatchParser.ts | 14 ++- server-v2/app/Repositories/MatchRepository.ts | 107 +++++++++++++----- .../app/Validators/SummonerRecordValidator.ts | 43 +++++++ .../migrations/1631392766690_match_players.ts | 4 + server-v2/start/routes.ts | 24 +--- 7 files changed, 160 insertions(+), 55 deletions(-) create mode 100644 server-v2/app/Validators/SummonerRecordValidator.ts diff --git a/server-v2/app/Controllers/Http/SummonersController.ts b/server-v2/app/Controllers/Http/SummonersController.ts index a8de7e3..1b65771 100644 --- a/server-v2/app/Controllers/Http/SummonersController.ts +++ b/server-v2/app/Controllers/Http/SummonersController.ts @@ -1,12 +1,14 @@ import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' import { getCurrentSeason } from 'App/helpers' import Summoner from 'App/Models/Summoner' +import MatchRepository from 'App/Repositories/MatchRepository' import Jax from 'App/Services/Jax' import MatchService from 'App/Services/MatchService' import StatsService from 'App/Services/StatsService' import SummonerService from 'App/Services/SummonerService' import SummonerBasicValidator from 'App/Validators/SummonerBasicValidator' import SummonerOverviewValidator from 'App/Validators/SummonerOverviewValidator' +import SummonerRecordValidator from 'App/Validators/SummonerRecordValidator' export default class SummonersController { public async basic({ request, response }: HttpContextContract) { @@ -83,4 +85,16 @@ export default class SummonersController { console.timeEnd('OVERVIEW_REQUEST') return response.json(finalJSON) } + + /** + * POST: get records view summoner data + * @param ctx + */ + public async records({ request, response }: HttpContextContract) { + console.time('recordsRequest') + const { puuid, season } = await request.validate(SummonerRecordValidator) + const records = await MatchRepository.records(puuid) + console.timeEnd('recordsRequest') + return response.json(records) + } } diff --git a/server-v2/app/Models/MatchPlayer.ts b/server-v2/app/Models/MatchPlayer.ts index c4ff31d..b7006b7 100644 --- a/server-v2/app/Models/MatchPlayer.ts +++ b/server-v2/app/Models/MatchPlayer.ts @@ -36,6 +36,15 @@ export default class MatchPlayer extends BaseModel { @column() public teamPosition: number + @column() + public win: number + + @column() + public loss: number + + @column() + public remake: number + @column() public kills: number diff --git a/server-v2/app/Parsers/MatchParser.ts b/server-v2/app/Parsers/MatchParser.ts index fabbb4f..0860b43 100644 --- a/server-v2/app/Parsers/MatchParser.ts +++ b/server-v2/app/Parsers/MatchParser.ts @@ -20,10 +20,12 @@ class MatchParser { gameDuration: Math.round(match.info.gameDuration / 1000), }) + const isRemake = match.info.gameDuration < 300 + // - 2x MatchTeam : Red and Blue for (const team of match.info.teams) { let result = team.win ? 'Win' : 'Fail' - if (match.info.gameDuration < 300) { + if (isRemake) { result = 'Remake' } await parsedMatch.related('teams').create({ @@ -47,10 +49,9 @@ class MatchParser { ? player.kills + player.assists : +(player.deaths === 0 ? 0 : (player.kills + player.assists) / player.deaths).toFixed(2) - const teamKills = - match.info.teams[0].teamId === player.teamId - ? match.info.teams[0].objectives.champion.kills - : match.info.teams[1].objectives.champion.kills + const team = + match.info.teams[0].teamId === player.teamId ? match.info.teams[0] : match.info.teams[1] + const teamKills = team.objectives.champion.kills const kp = teamKills === 0 ? 0 : +(((player.kills + player.assists) * 100) / teamKills).toFixed(1) @@ -87,6 +88,9 @@ class MatchParser { summoner_id: player.summonerId, summoner_puuid: player.puuid, summoner_name: player.summonerName, + win: team.win ? 1 : 0, + loss: team.win ? 0 : 1, + remake: isRemake ? 1 : 0, team: player.teamId, team_position: player.teamPosition.length && queuesWithRole.includes(match.info.queueId) diff --git a/server-v2/app/Repositories/MatchRepository.ts b/server-v2/app/Repositories/MatchRepository.ts index e6317f1..445a0b5 100644 --- a/server-v2/app/Repositories/MatchRepository.ts +++ b/server-v2/app/Repositories/MatchRepository.ts @@ -8,7 +8,7 @@ class MatchRepository { private readonly GLOBAL_FILTERS = ` match_players.summoner_puuid = :puuid - AND match_teams.result != 'Remake' + AND match_players.remake = 0 AND matches.gamemode NOT IN (800, 810, 820, 830, 840, 850, 2000, 2010, 2020) ` @@ -34,19 +34,19 @@ class MatchRepository { public async globalStats(puuid: string) { const query = ` SELECT - SUM(assists) as assists, - SUM(deaths) as deaths, - SUM(kills) as kills, - SUM(minions) as minions, + SUM(match_players.assists) as assists, + SUM(match_players.deaths) as deaths, + SUM(match_players.kills) as kills, + SUM(match_players.minions) as minions, SUM(matches.game_duration) as time, - SUM(vision_score) as vision, + SUM(match_players.vision_score) as vision, COUNT(match_players.id) as count, - AVG(kp) as kp, - COUNT(case when match_teams.result = 'Win' then 1 else null end) as wins, - COUNT(case when match_teams.result = 'Fail' then 1 else null end) as losses + AVG(match_players.kp) as kp, + SUM(match_players.win) as wins, + SUM(match_players.loss) as losses FROM match_players - ${this.JOIN_ALL} + ${this.JOIN_MATCHES} WHERE ${this.GLOBAL_FILTERS} LIMIT @@ -83,11 +83,11 @@ class MatchRepository { SELECT matches.gamemode as id, COUNT(match_players.id) as count, - COUNT(case when match_teams.result = 'Win' then 1 else null end) as wins, - COUNT(case when match_teams.result = 'Fail' then 1 else null end) as losses + SUM(match_players.win) as wins, + SUM(match_players.loss) as losses FROM match_players - ${this.JOIN_ALL} + ${this.JOIN_MATCHES} WHERE ${this.GLOBAL_FILTERS} GROUP BY @@ -104,11 +104,11 @@ class MatchRepository { SELECT match_players.team_position as role, COUNT(match_players.id) as count, - COUNT(case when match_teams.result = 'Win' then 1 else null end) as wins, - COUNT(case when match_teams.result = 'Fail' then 1 else null end) as losses + SUM(match_players.win) as wins, + SUM(match_players.loss) as losses FROM match_players - ${this.JOIN_ALL} + ${this.JOIN_MATCHES} WHERE ${this.GLOBAL_FILTERS} AND match_players.team_position != 0 @@ -123,15 +123,15 @@ class MatchRepository { const query = ` SELECT match_players.champion_id as id, - SUM(assists) as assists, - SUM(deaths) as deaths, - SUM(kills) as kills, + SUM(match_players.assists) as assists, + SUM(match_players.deaths) as deaths, + SUM(match_players.kills) as kills, COUNT(match_players.id) as count, - COUNT(case when match_teams.result = 'Win' then 1 else null end) as wins, - COUNT(case when match_teams.result = 'Fail' then 1 else null end) as losses + SUM(match_players.win) as wins, + SUM(match_players.loss) as losses FROM match_players - ${this.JOIN_ALL} + ${this.JOIN_MATCHES} WHERE ${this.GLOBAL_FILTERS} GROUP BY @@ -150,11 +150,11 @@ class MatchRepository { SELECT match_players.champion_role as id, COUNT(match_players.id) as count, - COUNT(case when match_teams.result = 'Win' then 1 else null end) as wins, - COUNT(case when match_teams.result = 'Fail' then 1 else null end) as losses + SUM(match_players.win) as wins, + SUM(match_players.loss) as losses FROM match_players - ${this.JOIN_ALL} + ${this.JOIN_MATCHES} WHERE ${this.GLOBAL_FILTERS} GROUP BY @@ -171,8 +171,8 @@ class MatchRepository { SELECT (array_agg(mates.summoner_name ORDER BY mates.match_id DESC))[1] as name, COUNT(match_players.id) as count, - COUNT(case when match_teams.result = 'Win' then 1 else null end) as wins, - COUNT(case when match_teams.result = 'Fail' then 1 else null end) as losses + SUM(match_players.win) as wins, + SUM(match_players.loss) as losses FROM match_players ${this.JOIN_ALL} @@ -191,6 +191,59 @@ class MatchRepository { // Remove the Summoner himself + unique game mates return rows.splice(1).filter((row) => row.count > 1) } + + public async records(puuid: string) { + const fields = [ + 'match_players.kills', + 'match_players.deaths', + 'match_players.assists', + 'match_players.gold', + 'matches.game_duration', + 'match_players.minions', + 'match_players.kda', + 'match_players.damage_taken', + 'match_players.damage_dealt_champions', + 'match_players.damage_dealt_objectives', + 'match_players.kp', + 'match_players.vision_score', + 'match_players.critical_strike', + 'match_players.time_spent_living', + 'match_players.heal', + 'match_players.turret_kills', + 'match_players.killing_spree', + 'match_players.double_kills', + 'match_players.triple_kills', + 'match_players.quadra_kills', + 'match_players.penta_kills', + ] + + const query = fields + .map((field) => { + return ` + (SELECT + '${field}' AS what, + ${field} AS amount, + match_players.win as result, + matches.id, + matches.date, + matches.gamemode, + match_players.champion_id + FROM + match_players + ${this.JOIN_MATCHES} + WHERE + ${this.GLOBAL_FILTERS} + ORDER BY + ${field} DESC, matches.id + LIMIT + 1) + ` + }) + .join('UNION ALL ') + + const { rows } = await Database.rawQuery(query, { puuid }) + return rows + } } export default new MatchRepository() diff --git a/server-v2/app/Validators/SummonerRecordValidator.ts b/server-v2/app/Validators/SummonerRecordValidator.ts new file mode 100644 index 0000000..97a6f35 --- /dev/null +++ b/server-v2/app/Validators/SummonerRecordValidator.ts @@ -0,0 +1,43 @@ +import { schema } from '@ioc:Adonis/Core/Validator' +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class SummonerRecordValidator { + constructor(protected ctx: HttpContextContract) {} + + /* + * Define schema to validate the "shape", "type", "formatting" and "integrity" of data. + * + * For example: + * 1. The username must be of data type string. But then also, it should + * not contain special characters or numbers. + * ``` + * schema.string({}, [ rules.alpha() ]) + * ``` + * + * 2. The email must be of data type string, formatted as a valid + * email. But also, not used by any other user. + * ``` + * schema.string({}, [ + * rules.email(), + * rules.unique({ table: 'users', column: 'email' }), + * ]) + * ``` + */ + public schema = schema.create({ + puuid: schema.string(), + season: schema.number.optional(), + }) + + /** + * Custom messages for validation failures. You can make use of dot notation `(.)` + * for targeting nested fields and array expressions `(*)` for targeting all + * children of an array. For example: + * + * { + * 'profile.username.required': 'Username is required', + * 'scores.*.number': 'Define scores as valid numbers' + * } + * + */ + public messages = {} +} diff --git a/server-v2/database/migrations/1631392766690_match_players.ts b/server-v2/database/migrations/1631392766690_match_players.ts index bbaff02..8814ed5 100644 --- a/server-v2/database/migrations/1631392766690_match_players.ts +++ b/server-v2/database/migrations/1631392766690_match_players.ts @@ -13,6 +13,10 @@ export default class MatchPlayers extends BaseSchema { table.string('summoner_puuid', 78).notNullable() table.string('summoner_name', 16).notNullable() + table.specificType('win', 'smallint').notNullable() + table.specificType('loss', 'smallint').notNullable() + table.specificType('remake', 'smallint').notNullable() + table.integer('team').notNullable() table.integer('team_position').notNullable() diff --git a/server-v2/start/routes.ts b/server-v2/start/routes.ts index e34e24c..8d83f97 100644 --- a/server-v2/start/routes.ts +++ b/server-v2/start/routes.ts @@ -20,8 +20,6 @@ import HealthCheck from '@ioc:Adonis/Core/HealthCheck' import Route from '@ioc:Adonis/Core/Route' -import MatchParser from 'App/Parsers/MatchParser' -import Jax from 'App/Services/Jax' Route.get('/', async () => ({ hi: 'Hello World from LeagueStats V2 API', @@ -34,7 +32,7 @@ Route.post('/summoner/basic', 'SummonersController.basic') Route.post('/summoner/overview', 'SummonersController.overview') // Route.post('/summoner/champions', 'SummonersController.champions') -// Route.post('/summoner/records', 'SummonersController.records') +Route.post('/summoner/records', 'SummonersController.records') // Route.post('/summoner/live', 'SummonersController.liveMatchDetails') Route.post('/match', 'MatchesController.index') @@ -42,23 +40,3 @@ Route.post('/match', 'MatchesController.index') // Route.post('/match/details/ranks', 'MatchesController.showRanks') Route.get('/cdragon/runes', 'CDragonController.runes') - -Route.get('/test', async () => { - const ids = [ - 'EUW1_5221171940', - 'EUW1_5220845489', - 'EUW1_5220852134', - 'EUW1_5220728352', - 'EUW1_5220656980', - 'EUW1_5215357679', - 'EUW1_5215311330', - 'EUW1_5215244329', - 'EUW1_5214301786', - 'EUW1_5212337578', - 'EUW1_5212353922', - 'EUW1_5212371343', - ] - const region = 'euw1' - const matches = await Promise.all(ids.map((id) => Jax.Match.get(id, region))) - await MatchParser.parse(matches) -})