mirror of
https://github.com/vkaelin/LeagueStats.git
synced 2026-03-25 12:57:28 +00:00
feat: add summoner/overview endpoint
This commit is contained in:
parent
2dbf4ba73a
commit
58ade06bc0
7 changed files with 320 additions and 57 deletions
|
|
@ -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<number[]> {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface MatchModel extends ParticipantDetails {
|
|||
region: string,
|
||||
season: number,
|
||||
time: number,
|
||||
newMatch?: boolean,
|
||||
}
|
||||
|
||||
export interface ParticipantDetails {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
48
server-new/app/Services/StatsService.ts
Normal file
48
server-new/app/Services/StatsService.ts
Normal 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()
|
||||
|
|
@ -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)
|
||||
finalMatches[index] = this.transformOneMatch(match, puuid, accountId)
|
||||
})
|
||||
return finalMatches
|
||||
} else {
|
||||
return this.transformOneMatch(matches, account) as MatchModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
55
server-new/app/Validators/SummonerOverviewValidator.ts
Normal file
55
server-new/app/Validators/SummonerOverviewValidator.ts
Normal 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 = {}
|
||||
}
|
||||
Loading…
Reference in a new issue