feat: add summoner/overview endpoint

This commit is contained in:
Valentin Kaelin 2020-10-08 09:51:12 +02:00
parent 2dbf4ba73a
commit 58ade06bc0
7 changed files with 320 additions and 57 deletions

View file

@ -3,11 +3,13 @@ import Summoner from 'App/Models/Summoner'
import MatchRepository from 'App/Repositories/MatchRepository' import MatchRepository from 'App/Repositories/MatchRepository'
import Jax from 'App/Services/Jax' import Jax from 'App/Services/Jax'
import MatchService from 'App/Services/MatchService' import MatchService from 'App/Services/MatchService'
import StatsService from 'App/Services/StatsService'
import SummonerService from 'App/Services/SummonerService' import SummonerService from 'App/Services/SummonerService'
import LiveMatchTransformer from 'App/Transformers/LiveMatchTransformer' import LiveMatchTransformer from 'App/Transformers/LiveMatchTransformer'
import SummonerBasicValidator from 'App/Validators/SummonerBasicValidator' import SummonerBasicValidator from 'App/Validators/SummonerBasicValidator'
import SummonerChampionValidator from 'App/Validators/SummonerChampionValidator' import SummonerChampionValidator from 'App/Validators/SummonerChampionValidator'
import SummonerLiveValidator from 'App/Validators/SummonerLiveValidator' import SummonerLiveValidator from 'App/Validators/SummonerLiveValidator'
import SummonerOverviewValidator from 'App/Validators/SummonerOverviewValidator'
import SummonerRecordValidator from 'App/Validators/SummonerRecordValidator' import SummonerRecordValidator from 'App/Validators/SummonerRecordValidator'
export default class SummonersController { export default class SummonersController {
@ -15,7 +17,7 @@ export default class SummonersController {
* Get all played seasons for a summoner * Get all played seasons for a summoner
* @param puuid of the summoner * @param puuid of the summoner
*/ */
private async getSeasons (puuid: string) { private async getSeasons (puuid: string): Promise<number[]> {
const seasons = await MatchRepository.seasons(puuid) const seasons = await MatchRepository.seasons(puuid)
return seasons.length ? seasons.map(s => s._id) : [10] return seasons.length ? seasons.map(s => s._id) : [10]
} }
@ -74,6 +76,43 @@ export default class SummonersController {
return response.json(finalJSON) 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 * POST: get champions view summoner data
* @param ctx * @param ctx

View file

@ -13,6 +13,7 @@ export interface MatchModel extends ParticipantDetails {
region: string, region: string,
season: number, season: number,
time: number, time: number,
newMatch?: boolean,
} }
export interface ParticipantDetails { export interface ParticipantDetails {

View file

@ -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 * Get Summoner's complete statistics for the all played champs
* @param puuid of the summoner * @param puuid of the summoner
@ -105,6 +135,33 @@ class MatchRepository {
return this.aggregate(puuid, matchParams, [], '$champion.id', groupParams, finalSteps, season) 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 * Get Summoner's all records
* @param puuid of the summoner * @param puuid of the summoner
@ -205,6 +262,28 @@ class MatchRepository {
return records[0] 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 * Get Summoner's played seasons
* @param puuid of the summoner * @param puuid of the summoner
@ -221,6 +300,39 @@ class MatchRepository {
}, },
]).toArray() ]).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() export default new MatchRepository()

View file

@ -1,9 +1,12 @@
import Jax from './Jax' import Jax from './Jax'
// import Logger from '@ioc:Adonis/Core/Logger' import Logger from '@ioc:Adonis/Core/Logger'
import { getSeasonNumber } from 'App/helpers' import { getSeasonNumber } from 'App/helpers'
import { MatchReferenceDto } from './Jax/src/Endpoints/MatchListEndpoint' import { MatchReferenceDto } from './Jax/src/Endpoints/MatchListEndpoint'
import { SummonerDTO } from './Jax/src/Endpoints/SummonerEndpoint' import { SummonerDTO } from './Jax/src/Endpoints/SummonerEndpoint'
import { SummonerModel } from 'App/Models/Summoner' 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 { class MatchService {
/** /**
@ -84,53 +87,62 @@ class MatchService {
/** /**
* Fetch list of matches for a specific Summoner * Fetch list of matches for a specific Summoner
* @param account of the summoner * @param puuid
* @param gameIds of the matches to fetch * @param accountId
* @param summonerDB summoner in the database * @param region
* @param gameIds
* @param summonerDB
*/ */
// public async getMatches (account, gameIds, summonerDB) { public async getMatches (puuid: string, accountId: string, region: string, gameIds: number[], summonerDB: SummonerModel) {
// console.time('getMatches') console.time('getMatches')
// let matchesDetails = [] let matchesDetails: MatchModel[] = []
// const matchesToGetFromRiot = [] const matchesToGetFromRiot: number[] = []
// for (let i = 0; i < gameIds.length; ++i) { // TODO: replace it with Match Model once the package is fixed
// const matchSaved = await summonerDB.matches().where({ gameId: gameIds[i] }).first() const matchesCollection = await mongodb.connection().collection('matches')
// if (matchSaved) { for (let i = 0; i < gameIds.length; ++i) {
// matchesDetails.push(matchSaved) const matchSaved = await matchesCollection.findOne({
// } else { summoner_puuid: puuid,
// matchesToGetFromRiot.push(gameIds[i]) 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)) const requests = matchesToGetFromRiot.map(gameId => Jax.Match.get(gameId, region))
// let matchesFromApi = await Promise.all(requests) let matchesFromApi = await Promise.all(requests)
// /* If we have to store some matches in the db */ /* If we have to store some matches in the db */
// if (matchesFromApi.length !== 0) { if (matchesFromApi.length !== 0) {
// // Try to see why matches are sometimes undefined // Try to see why matches are sometimes undefined
// matchesFromApi.filter(m => { matchesFromApi.filter(m => {
// if (m === undefined) { if (m === undefined) {
// Logger.info(`Match undefined, summoner: ${summonerDB.puuid}`, m) Logger.info(`Match undefined, summoner: ${summonerDB.puuid}`, m)
// } }
// }) })
// // Transform raw matches data // Transform raw matches data
// await BasicMatchTransformer.transform(matchesFromApi, { account }) const transformedMatches = await BasicMatchTransformer.transform(matchesFromApi, { puuid, accountId })
// /* Save all matches from Riot Api in db */ /* Save all matches from Riot Api in db */
// for (const match of matchesFromApi) { for (const match of transformedMatches) {
// await summonerDB.matches().create(match) await Match.create(match)
// match.newMatch = true match.newMatch = true
// } }
// matchesDetails = [...matchesDetails, ...matchesFromApi] matchesDetails = [...matchesDetails, ...transformedMatches]
// } }
// /* Sort matches */ /* Sort matches */
// matchesDetails.sort((a, b) => (a.date < b.date) ? 1 : -1) matchesDetails.sort((a, b) => (a.date < b.date) ? 1 : -1)
// console.timeEnd('getMatches') console.timeEnd('getMatches')
// return matchesDetails return matchesDetails
// } }
} }
export default new MatchService() export default new MatchService()

View file

@ -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()

View file

@ -1,19 +1,19 @@
import { MatchModel, ParticipantBasic } from 'App/Models/Match' import { MatchModel, ParticipantBasic } from 'App/Models/Match'
import { MatchDto } from 'App/Services/Jax/src/Endpoints/MatchEndpoint' import { MatchDto } from 'App/Services/Jax/src/Endpoints/MatchEndpoint'
import { SummonerDTO } from 'App/Services/Jax/src/Endpoints/SummonerEndpoint'
import MatchTransformer from 'App/Transformers/MatchTransformer' import MatchTransformer from 'App/Transformers/MatchTransformer'
class BasicMatchTransformer extends MatchTransformer { class BasicMatchTransformer extends MatchTransformer {
/** /**
* Transform raw data for 1 match * Transform raw data for 1 match
* @param 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 // Global data about the match
const globalInfos = super.getGameInfos(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] const player = match.participants[identity!.participantId - 1]
let win = match.teams.find((t) => t.teamId === player.teamId)!.win let win = match.teams.find((t) => t.teamId === player.teamId)!.win
@ -50,7 +50,7 @@ class BasicMatchTransformer extends MatchTransformer {
return { return {
account_id: identity!.player.currentAccountId, account_id: identity!.player.currentAccountId,
summoner_puuid: account.puuid, summoner_puuid: puuid,
gameId: match.gameId, gameId: match.gameId,
result: win, result: win,
allyTeam, allyTeam,
@ -62,21 +62,17 @@ class BasicMatchTransformer extends MatchTransformer {
/** /**
* Transform raw data from Riot API * 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 * @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() await super.getContext()
if (Array.isArray(matches)) {
const finalMatches:MatchModel[] = [] const finalMatches:MatchModel[] = []
matches.forEach((match, index) => { matches.forEach((match, index) => {
finalMatches[index] = this.transformOneMatch(match, account) finalMatches[index] = this.transformOneMatch(match, puuid, accountId)
}) })
return finalMatches return finalMatches
} else {
return this.transformOneMatch(matches, account) as MatchModel
}
} }
} }

View file

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