diff --git a/server-new/app/Controllers/Http/SummonersController.ts b/server-new/app/Controllers/Http/SummonersController.ts index ee07cf7..a094ea6 100644 --- a/server-new/app/Controllers/Http/SummonersController.ts +++ b/server-new/app/Controllers/Http/SummonersController.ts @@ -3,11 +3,13 @@ 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 LiveMatchTransformer from 'App/Transformers/LiveMatchTransformer' import SummonerBasicValidator from 'App/Validators/SummonerBasicValidator' import SummonerChampionValidator from 'App/Validators/SummonerChampionValidator' import SummonerLiveValidator from 'App/Validators/SummonerLiveValidator' +import SummonerOverviewValidator from 'App/Validators/SummonerOverviewValidator' import SummonerRecordValidator from 'App/Validators/SummonerRecordValidator' export default class SummonersController { @@ -15,7 +17,7 @@ export default class SummonersController { * Get all played seasons for a summoner * @param puuid of the summoner */ - private async getSeasons (puuid: string) { + private async getSeasons (puuid: string): Promise { const seasons = await MatchRepository.seasons(puuid) return seasons.length ? seasons.map(s => s._id) : [10] } @@ -74,6 +76,43 @@ export default class SummonersController { return response.json(finalJSON) } + /** + * POST: get overview view summoner data + * @param ctx + */ + public async overview ({ request, response }: HttpContextContract) { + console.time('overview') + const { puuid, accountId, region, season } = await request.validate(SummonerOverviewValidator) + const finalJSON: any = {} + + // Summoner in DB + let summonerDB = await Summoner.findOne({ puuid: puuid }) + if (!summonerDB) { + summonerDB = await Summoner.create({ puuid: puuid }) + } + + // MATCHES BASIC + const gameIds = summonerDB.matchList!.slice(0) + .filter(m => { + return season ? m.seasonMatch === season : true + }) + .slice(0, 10) + .map(({ gameId }) => gameId) + finalJSON.matchesDetails = await MatchService.getMatches(puuid, accountId, region, gameIds, summonerDB) + + // STATS + console.time('STATS') + finalJSON.stats = await StatsService.getSummonerStats(puuid, season) + console.timeEnd('STATS') + + // SAVE IN DB + await summonerDB.save() + + console.timeEnd('overview') + console.log(finalJSON) + return response.json(finalJSON) + } + /** * POST: get champions view summoner data * @param ctx diff --git a/server-new/app/Models/Match.ts b/server-new/app/Models/Match.ts index a899e8d..85c2525 100644 --- a/server-new/app/Models/Match.ts +++ b/server-new/app/Models/Match.ts @@ -13,6 +13,7 @@ export interface MatchModel extends ParticipantDetails { region: string, season: number, time: number, + newMatch?: boolean, } export interface ParticipantDetails { diff --git a/server-new/app/Repositories/MatchRepository.ts b/server-new/app/Repositories/MatchRepository.ts index 41bece3..31e2a72 100644 --- a/server-new/app/Repositories/MatchRepository.ts +++ b/server-new/app/Repositories/MatchRepository.ts @@ -77,6 +77,36 @@ class MatchRepository { } } + /** + * Get Summoner's statistics for the N most played champions + * @param puuid of the summoner + * @param limit number of champions to fetch + * @param season + */ + public async championStats (puuid: string, limit = 5, season?: number) { + const groupParams = { + champion: { $first: '$champion' }, + kills: { $sum: '$stats.kills' }, + deaths: { $sum: '$stats.deaths' }, + assists: { $sum: '$stats.assists' }, + } + const finalSteps = [ + { $sort: { 'count': -1, 'champion.name': 1 } }, + { $limit: limit }, + ] + return this.aggregate(puuid, {}, [], '$champion.id', groupParams, finalSteps, season) + } + + /** + * Get Summoner's statistics for all played champion classes + * @param puuid of the summoner + * @param season + */ + public async championClassStats (puuid: string, season?: number) { + const groupId = { '$arrayElemAt': ['$champion.roles', 0] } + return this.aggregate(puuid, {}, [], groupId, {}, [], season) + } + /** * Get Summoner's complete statistics for the all played champs * @param puuid of the summoner @@ -105,6 +135,33 @@ class MatchRepository { return this.aggregate(puuid, matchParams, [], '$champion.id', groupParams, finalSteps, season) } + /** + * Get Summoner's statistics for all played modes + * @param puuid of the summoner + * @param season + */ + public async gamemodeStats (puuid: string, season?: number) { + return this.aggregate(puuid, {}, [], '$gamemode', {}, [], season) + } + + /** + * Get global Summoner's statistics + * @param puuid of the summoner + * @param season + */ + public async globalStats (puuid: string, season?: number) { + const groupParams = { + time: { $sum: '$time' }, + kills: { $sum: '$stats.kills' }, + deaths: { $sum: '$stats.deaths' }, + assists: { $sum: '$stats.assists' }, + minions: { $sum: '$stats.minions' }, + vision: { $sum: '$stats.vision' }, + kp: { $avg: '$stats.kp' }, + } + return this.aggregate(puuid, {}, [], null, groupParams, [], season) + } + /** * Get Summoner's all records * @param puuid of the summoner @@ -205,6 +262,28 @@ class MatchRepository { return records[0] } + /** + * Get Summoner's statistics for the 5 differnt roles + * @param puuid of the summoner + * @param season + */ + public async roleStats (puuid: string, season?: number) { + const matchParams = { + role: { $not: { $eq: 'NONE' } }, + } + const finalSteps = [ + { + $project: { + role: '$_id', + count: '$count', + wins: '$wins', + losses: '$losses', + }, + }, + ] + return this.aggregate(puuid, matchParams, [], '$role', {}, finalSteps, season) + } + /** * Get Summoner's played seasons * @param puuid of the summoner @@ -221,6 +300,39 @@ class MatchRepository { }, ]).toArray() } + + /** + * Get Summoner's mates list + * @param puuid of the summoner + * @param season + */ + public async mates (puuid: string, season?: number) { + const intermediateSteps = [ + { $sort: { 'gameId': -1 } }, + { $unwind: '$allyTeam' }, + ] + const groupParams = { + account_id: { $first: '$account_id' }, + name: { $first: '$allyTeam.name' }, + mateId: { $first: '$allyTeam.account_id' }, + } + const finalSteps = [ + { + '$addFields': { + 'idEq': { '$eq': ['$mateId', '$account_id'] }, + }, + }, + { + $match: { + 'idEq': false, + 'count': { $gte: 2 }, + }, + }, + { $sort: { 'count': -1, 'name': 1 } }, + { $limit: 15 }, + ] + return this.aggregate(puuid, {}, intermediateSteps, '$allyTeam.account_id', groupParams, finalSteps, season) + } } export default new MatchRepository() diff --git a/server-new/app/Services/MatchService.ts b/server-new/app/Services/MatchService.ts index 2b1aa8f..a4dc3fb 100644 --- a/server-new/app/Services/MatchService.ts +++ b/server-new/app/Services/MatchService.ts @@ -1,9 +1,12 @@ import Jax from './Jax' -// import Logger from '@ioc:Adonis/Core/Logger' +import Logger from '@ioc:Adonis/Core/Logger' import { getSeasonNumber } from 'App/helpers' import { MatchReferenceDto } from './Jax/src/Endpoints/MatchListEndpoint' import { SummonerDTO } from './Jax/src/Endpoints/SummonerEndpoint' import { SummonerModel } from 'App/Models/Summoner' +import Match, { MatchModel } from 'App/Models/Match' +import BasicMatchTransformer from 'App/Transformers/BasicMatchTransformer' +import mongodb from '@ioc:Mongodb/Database' class MatchService { /** @@ -84,53 +87,62 @@ class MatchService { /** * Fetch list of matches for a specific Summoner - * @param account of the summoner - * @param gameIds of the matches to fetch - * @param summonerDB summoner in the database + * @param puuid + * @param accountId + * @param region + * @param gameIds + * @param summonerDB */ - // public async getMatches (account, gameIds, summonerDB) { - // console.time('getMatches') + public async getMatches (puuid: string, accountId: string, region: string, gameIds: number[], summonerDB: SummonerModel) { + console.time('getMatches') - // let matchesDetails = [] - // const matchesToGetFromRiot = [] - // for (let i = 0; i < gameIds.length; ++i) { - // const matchSaved = await summonerDB.matches().where({ gameId: gameIds[i] }).first() - // if (matchSaved) { - // matchesDetails.push(matchSaved) - // } else { - // matchesToGetFromRiot.push(gameIds[i]) - // } - // } + let matchesDetails: MatchModel[] = [] + const matchesToGetFromRiot: number[] = [] + // TODO: replace it with Match Model once the package is fixed + const matchesCollection = await mongodb.connection().collection('matches') + for (let i = 0; i < gameIds.length; ++i) { + const matchSaved = await matchesCollection.findOne({ + summoner_puuid: puuid, + gameId: gameIds[i], + }) + if (matchSaved) { + console.log('match saved') + console.log(matchSaved) + matchesDetails.push(matchSaved) + } else { + matchesToGetFromRiot.push(gameIds[i]) + } + } - // const requests = matchesToGetFromRiot.map(gameId => Jax.Match.get(gameId, account.region)) - // let matchesFromApi = await Promise.all(requests) + const requests = matchesToGetFromRiot.map(gameId => Jax.Match.get(gameId, region)) + let matchesFromApi = await Promise.all(requests) - // /* If we have to store some matches in the db */ - // if (matchesFromApi.length !== 0) { - // // Try to see why matches are sometimes undefined - // matchesFromApi.filter(m => { - // if (m === undefined) { - // Logger.info(`Match undefined, summoner: ${summonerDB.puuid}`, m) - // } - // }) + /* If we have to store some matches in the db */ + if (matchesFromApi.length !== 0) { + // Try to see why matches are sometimes undefined + matchesFromApi.filter(m => { + if (m === undefined) { + Logger.info(`Match undefined, summoner: ${summonerDB.puuid}`, m) + } + }) - // // Transform raw matches data - // await BasicMatchTransformer.transform(matchesFromApi, { account }) + // Transform raw matches data + const transformedMatches = await BasicMatchTransformer.transform(matchesFromApi, { puuid, accountId }) - // /* Save all matches from Riot Api in db */ - // for (const match of matchesFromApi) { - // await summonerDB.matches().create(match) - // match.newMatch = true - // } - // matchesDetails = [...matchesDetails, ...matchesFromApi] - // } + /* Save all matches from Riot Api in db */ + for (const match of transformedMatches) { + await Match.create(match) + match.newMatch = true + } + matchesDetails = [...matchesDetails, ...transformedMatches] + } - // /* Sort matches */ - // matchesDetails.sort((a, b) => (a.date < b.date) ? 1 : -1) - // console.timeEnd('getMatches') + /* Sort matches */ + matchesDetails.sort((a, b) => (a.date < b.date) ? 1 : -1) + console.timeEnd('getMatches') - // return matchesDetails - // } + return matchesDetails + } } export default new MatchService() diff --git a/server-new/app/Services/StatsService.ts b/server-new/app/Services/StatsService.ts new file mode 100644 index 0000000..c22b5f1 --- /dev/null +++ b/server-new/app/Services/StatsService.ts @@ -0,0 +1,48 @@ +import MatchRepository from 'App/Repositories/MatchRepository' +import { sortTeamByRole } from 'App/helpers' + +class StatsService { + public async getSummonerStats (puuid: string, season?: number) { + console.time('GLOBAL') + const globalStats = await MatchRepository.globalStats(puuid, season) + console.timeEnd('GLOBAL') + console.time('GAMEMODE') + const gamemodeStats = await MatchRepository.gamemodeStats(puuid, season) + console.timeEnd('GAMEMODE') + console.time('ROLE') + const roleStats = await MatchRepository.roleStats(puuid, season) + // Check if all roles are in the array + const roles = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'SUPPORT'] + for (const role of roles) { + if (!roleStats.find(r => r.role === role)) { + roleStats.push({ + count: 0, + losses: 0, + role, + wins: 0, + }) + } + } + console.timeEnd('ROLE') + console.time('CHAMPION') + const championStats = await MatchRepository.championStats(puuid, 5, season) + console.timeEnd('CHAMPION') + console.time('CHAMPION-CLASS') + const championClassStats = await MatchRepository.championClassStats(puuid, season) + console.timeEnd('CHAMPION-CLASS') + console.time('MATES') + const mates = await MatchRepository.mates(puuid, season) + console.timeEnd('MATES') + + return { + global: globalStats[0], + league: gamemodeStats, + role: roleStats.sort(sortTeamByRole), + class: championClassStats, + mates, + champion: championStats, + } + } +} + +export default new StatsService() diff --git a/server-new/app/Transformers/BasicMatchTransformer.ts b/server-new/app/Transformers/BasicMatchTransformer.ts index 74e2d33..59d07b0 100644 --- a/server-new/app/Transformers/BasicMatchTransformer.ts +++ b/server-new/app/Transformers/BasicMatchTransformer.ts @@ -1,19 +1,19 @@ import { MatchModel, ParticipantBasic } from 'App/Models/Match' import { MatchDto } from 'App/Services/Jax/src/Endpoints/MatchEndpoint' -import { SummonerDTO } from 'App/Services/Jax/src/Endpoints/SummonerEndpoint' import MatchTransformer from 'App/Transformers/MatchTransformer' class BasicMatchTransformer extends MatchTransformer { /** * Transform raw data for 1 match * @param match - * @param account + * @param puuid + * @param accountId */ - private transformOneMatch (match: MatchDto, account: SummonerDTO) { + private transformOneMatch (match: MatchDto, puuid: string, accountId: string): MatchModel { // Global data about the match const globalInfos = super.getGameInfos(match) - const identity = match.participantIdentities.find((p) => p.player.currentAccountId === account.accountId) + const identity = match.participantIdentities.find((p) => p.player.currentAccountId === accountId) const player = match.participants[identity!.participantId - 1] let win = match.teams.find((t) => t.teamId === player.teamId)!.win @@ -50,7 +50,7 @@ class BasicMatchTransformer extends MatchTransformer { return { account_id: identity!.player.currentAccountId, - summoner_puuid: account.puuid, + summoner_puuid: puuid, gameId: match.gameId, result: win, allyTeam, @@ -62,21 +62,17 @@ class BasicMatchTransformer extends MatchTransformer { /** * Transform raw data from Riot API - * @param matches data from Riot API, Array of match or a single match + * @param matches data from Riot API, Array of matches * @param ctx context */ - public async transform (matches: MatchDto[] | MatchDto, { account }: { account: SummonerDTO }) { + public async transform (matches: MatchDto[], { puuid, accountId }: { puuid: string, accountId: string }) { await super.getContext() - if (Array.isArray(matches)) { - const finalMatches:MatchModel[] = [] - matches.forEach((match, index) => { - finalMatches[index] = this.transformOneMatch(match, account) - }) - return finalMatches - } else { - return this.transformOneMatch(matches, account) as MatchModel - } + const finalMatches:MatchModel[] = [] + matches.forEach((match, index) => { + finalMatches[index] = this.transformOneMatch(match, puuid, accountId) + }) + return finalMatches } } diff --git a/server-new/app/Validators/SummonerOverviewValidator.ts b/server-new/app/Validators/SummonerOverviewValidator.ts new file mode 100644 index 0000000..8320125 --- /dev/null +++ b/server-new/app/Validators/SummonerOverviewValidator.ts @@ -0,0 +1,55 @@ +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import { schema } from '@ioc:Adonis/Core/Validator' + +export default class SummonerOverviewValidator { + constructor (private ctx: HttpContextContract) { + } + + /** + * Defining a 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(), + accountId: schema.string(), + region: schema.string(), + season: schema.number.optional(), + }) + + /** + * The `schema` first gets compiled to a reusable function and then that compiled + * function validates the data at runtime. + * + * Since, compiling the schema is an expensive operation, you must always cache it by + * defining a unique cache key. The simplest way is to use the current request route + * key, which is a combination of the route pattern and HTTP method. + */ + public cacheKey = this.ctx.routeKey + + /** + * 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 = {} +}