mirror of
https://github.com/amineo/t2-stat-parser.git
synced 2026-01-20 09:44:45 +00:00
Compare commits
112 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8165e35d10 | ||
|
|
912a7c676a | ||
|
|
f803963015 | ||
|
|
928bf1812c | ||
|
|
248f8a4687 | ||
|
|
eea261af1a | ||
|
|
a615d4524b | ||
|
|
86e13db165 | ||
|
|
4df3a1b594 | ||
|
|
7f1f34364e | ||
|
|
08010956a8 | ||
|
|
3114913c85 | ||
|
|
edd3076b54 | ||
|
|
79a8d4eeac | ||
|
|
01c2b8b37d | ||
|
|
c42b05e7de | ||
|
|
3bbdb99253 | ||
|
|
89fbe7cf85 | ||
|
|
70306791c3 | ||
|
|
321047c38e | ||
|
|
efdf44d7ba | ||
|
|
e55c6ed9da | ||
|
|
e01179fd72 | ||
|
|
2e8b8458a6 | ||
|
|
7585564018 | ||
|
|
9be3e79372 | ||
|
|
ab028e52fd | ||
|
|
1949494098 | ||
|
|
6f37071ba6 | ||
|
|
e7e064ae55 | ||
|
|
4cfeab4264 | ||
|
|
c8402074b4 | ||
|
|
59d0cc198d | ||
|
|
d0f963911f | ||
|
|
05fa275f85 | ||
|
|
4bb29a22ff | ||
|
|
53c66be747 | ||
|
|
7504d1497c | ||
|
|
1015e4b8d4 | ||
|
|
9466af400d | ||
|
|
1a56501db4 | ||
|
|
1adcdf1d0e | ||
|
|
ebb72ff01f | ||
|
|
0bbfa32ef6 | ||
|
|
fbff090ba5 | ||
|
|
ddf8c112f5 | ||
|
|
9222b8f0d6 | ||
|
|
a14fec7355 | ||
|
|
c1fcb61731 | ||
|
|
aed14743e6 | ||
|
|
2f3f61fd62 | ||
|
|
98affddbff | ||
|
|
c10cd44615 | ||
|
|
67eb8db0c8 | ||
|
|
ef3909b8ac | ||
|
|
fdb446714a | ||
|
|
cc1c71c769 | ||
|
|
f70b95ab5e | ||
|
|
1a51c95b82 | ||
|
|
9e40b300f5 | ||
|
|
8e5cf83d4d | ||
|
|
52f87b1fc3 | ||
|
|
72ac49e4f9 | ||
|
|
69f250871a | ||
|
|
80056903c6 | ||
|
|
87bfab307d | ||
|
|
b3925a8fc6 | ||
|
|
9176889086 | ||
|
|
ef32d90030 | ||
|
|
96e15be86d | ||
|
|
f240ccd306 | ||
|
|
86c531d61e | ||
|
|
d85896dcaa | ||
|
|
2c3c9a0532 | ||
|
|
a1b2f2cd78 | ||
|
|
75f6e64a1f | ||
|
|
38fb66da06 | ||
|
|
2fcfd8cb3b | ||
|
|
0db8a7f439 | ||
|
|
2abedc99f9 | ||
|
|
2d90f9dde8 | ||
|
|
3b5f41f933 | ||
|
|
1a211e261e | ||
|
|
f1e93e9d29 | ||
|
|
0b5aceca7a | ||
|
|
bb179b82f8 | ||
|
|
43c15e1649 | ||
|
|
b1bf08267e | ||
|
|
71c3cad722 | ||
|
|
15ac59d6e9 | ||
|
|
a484d94254 | ||
|
|
093636fc6d | ||
|
|
8fc23cc6e4 | ||
|
|
2cf1a508d5 | ||
|
|
56878c47d6 | ||
|
|
77880c9df2 | ||
|
|
74798d239b | ||
|
|
a57800e3a4 | ||
|
|
5394982541 | ||
|
|
063f6ba55b | ||
|
|
e601fe8e91 | ||
|
|
825159ea56 | ||
|
|
77a564125a | ||
|
|
15b96d197e | ||
|
|
31d927bbe2 | ||
|
|
47c6017ea1 | ||
|
|
354ea453e0 | ||
|
|
cdbcbe19e3 | ||
|
|
229ce10466 | ||
|
|
982d1c429c | ||
|
|
cfa645120d | ||
|
|
ade7f7e288 |
13
.env.example
13
.env.example
|
|
@ -6,4 +6,15 @@ POSTGRES_PASSWORD="dev"
|
|||
|
||||
FTP_HOST="127.0.0.1"
|
||||
FTP_USER="user"
|
||||
FTP_PW="pw"
|
||||
FTP_PW="pw"
|
||||
FTP_PATH="/path/to/stats"
|
||||
|
||||
# API DB Connection
|
||||
DATABASE_HOST="db"
|
||||
DATABASE_PORT="5432"
|
||||
DATABASE_USER="dev"
|
||||
DATABASE_PASSWORD="dev"
|
||||
DATABASE_NAME="t2_stats"
|
||||
|
||||
NODE_ENV="development"
|
||||
APP_NAME="T2StatsAPI"
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
|
|
@ -1,5 +1,15 @@
|
|||
.env
|
||||
node_modules
|
||||
|
||||
notes.md
|
||||
|
||||
app/t2-stat-parser/serverStats/stats
|
||||
docker-compose.deploy.yml
|
||||
app/t2-stat-parser/serverStats/mlData
|
||||
app/t2-stat-parser/serverStats/lData
|
||||
app/t2-stat-parser/serverStats/__notes
|
||||
|
||||
docker-compose.deploy.yml
|
||||
traefik.yml
|
||||
docker-secrets/adonis.appkey.*
|
||||
|
||||
*.dump
|
||||
72
app/api/.eslintrc.js
Normal file
72
app/api/.eslintrc.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: [ '@typescript-eslint/eslint-plugin' ],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
'prettier/@typescript-eslint'
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: 'default',
|
||||
filter: {
|
||||
regex: '^(created|updated)_at$',
|
||||
match: false
|
||||
},
|
||||
format: [ 'camelCase' ],
|
||||
leadingUnderscore: 'allow',
|
||||
trailingUnderscore: 'allow'
|
||||
},
|
||||
{
|
||||
selector: 'property',
|
||||
filter: {
|
||||
regex: '^(created|updated)_at$',
|
||||
match: false
|
||||
},
|
||||
format: [ 'camelCase', 'UPPER_CASE' ],
|
||||
leadingUnderscore: 'allow',
|
||||
trailingUnderscore: 'allow'
|
||||
},
|
||||
{
|
||||
selector: 'enumMember',
|
||||
format: [ 'UPPER_CASE' ],
|
||||
leadingUnderscore: 'allow',
|
||||
trailingUnderscore: 'allow'
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: [ 'camelCase', 'UPPER_CASE' ],
|
||||
types: [ 'boolean', 'string', 'number', 'array' ],
|
||||
leadingUnderscore: 'allow',
|
||||
trailingUnderscore: 'allow'
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: [ 'camelCase', 'PascalCase' ],
|
||||
types: [ 'function' ],
|
||||
leadingUnderscore: 'allow',
|
||||
trailingUnderscore: 'allow'
|
||||
},
|
||||
{
|
||||
selector: 'typeLike',
|
||||
format: [ 'PascalCase' ]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
37
app/api/.gitignore
vendored
Normal file
37
app/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# ENV
|
||||
*.env
|
||||
5
app/api/.prettierrc
Normal file
5
app/api/.prettierrc
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"useTabs": true
|
||||
}
|
||||
75
app/api/README.md
Normal file
75
app/api/README.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
|
||||
[travis-url]: https://travis-ci.org/nestjs/nest
|
||||
[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
|
||||
[linux-url]: https://travis-ci.org/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.io" target="blank">Angular</a>.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/dm/@nestjs/core.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://travis-ci.org/nestjs/nest"><img src="https://api.travis-ci.org/nestjs/nest.svg?branch=master" alt="Travis" /></a>
|
||||
<a href="https://travis-ci.org/nestjs/nest"><img src="https://img.shields.io/travis/nestjs/nest/master.svg?label=linux" alt="Linux" /></a>
|
||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#5" alt="Coverage" /></a>
|
||||
<a href="https://gitter.im/nestjs/nestjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge"><img src="https://badges.gitter.im/nestjs/nestjs.svg" alt="Gitter" /></a>
|
||||
<a href="https://opencollective.com/nest#backer"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec"><img src="https://img.shields.io/badge/Donate-PayPal-dc3d53.svg"/></a>
|
||||
<a href="https://twitter.com/nestframework"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Running the app
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](LICENSE).
|
||||
4
app/api/nest-cli.json
Normal file
4
app/api/nest-cli.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
||||
12695
app/api/package-lock.json
generated
Normal file
12695
app/api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
87
app/api/package.json
Normal file
87
app/api/package.json
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
{
|
||||
"name": "playt2-stats-api",
|
||||
"version": "0.0.1",
|
||||
"description": "Open-API powering stats.playt2.com",
|
||||
"authors": {
|
||||
"name": "Anthony Mineo",
|
||||
"email": "anthonymineo@gmail.com",
|
||||
"url": "https://anthonymineo.com"
|
||||
},
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^8.0.7",
|
||||
"@nestjs/config": "^1.0.1",
|
||||
"@nestjs/core": "^8.0.7",
|
||||
"@nestjs/mapped-types": "^1.0.0",
|
||||
"@nestjs/platform-express": "^8.0.7",
|
||||
"@nestjs/swagger": "^5.0.9",
|
||||
"@nestjs/typeorm": "^8.0.2",
|
||||
"cache-manager": "^3.4.4",
|
||||
"class-transformer": "^0.3.1",
|
||||
"class-validator": "^0.12.2",
|
||||
"joi": "^17.4.2",
|
||||
"pg": "^8.7.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.3.0",
|
||||
"swagger-ui-express": "^4.1.6",
|
||||
"typeorm": "^0.2.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^8.1.1",
|
||||
"@nestjs/schematics": "^8.0.3",
|
||||
"@nestjs/testing": "^8.0.7",
|
||||
"@types/cache-manager": "^3.4.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/hapi__joi": "^17.1.7",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/node": "^16.10.1",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^3.10.1",
|
||||
"@typescript-eslint/parser": "^3.10.1",
|
||||
"eslint": "^7.9.0",
|
||||
"eslint-config-prettier": "^6.10.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"jest": "^26.4.2",
|
||||
"prettier": "^2.1.2",
|
||||
"supertest": "^4.0.2",
|
||||
"ts-jest": "^26.3.0",
|
||||
"ts-loader": "^8.0.3",
|
||||
"ts-node": "^9.0.0",
|
||||
"tsconfig-paths": "^3.9.0",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"volta": {
|
||||
"node": "12.18"
|
||||
}
|
||||
}
|
||||
22
app/api/src/app.controller.spec.ts
Normal file
22
app/api/src/app.controller.spec.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [ AppController ],
|
||||
providers: [ AppService ]
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Healthy!"', () => {
|
||||
expect(appController.getHello()).toBe('Healthy!');
|
||||
});
|
||||
});
|
||||
});
|
||||
12
app/api/src/app.controller.ts
Normal file
12
app/api/src/app.controller.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
||||
39
app/api/src/app.module.ts
Normal file
39
app/api/src/app.module.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import * as Joi from 'joi';
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { GamesModule } from './games/games.module';
|
||||
import { GameModule } from './game/game.module';
|
||||
import { PlayersModule } from './players/players.module';
|
||||
import { PlayerModule } from './player/player.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
validationSchema: Joi.object({
|
||||
DATABASE_HOST: Joi.required(),
|
||||
DATABASE_PORT: Joi.number().default(5432)
|
||||
})
|
||||
}),
|
||||
TypeOrmModule.forRoot({
|
||||
type: 'postgres', // type of our database
|
||||
host: process.env.DATABASE_HOST,
|
||||
port: +process.env.DATABASE_PORT,
|
||||
username: process.env.DATABASE_USER,
|
||||
password: process.env.DATABASE_PASSWORD,
|
||||
database: process.env.DATABASE_NAME,
|
||||
autoLoadEntities: true // models will be loaded automatically (you don't have to explicitly specify the entities: [] array)
|
||||
}),
|
||||
GamesModule,
|
||||
GameModule,
|
||||
PlayersModule,
|
||||
PlayerModule
|
||||
],
|
||||
controllers: [ AppController ],
|
||||
providers: [ AppService ]
|
||||
})
|
||||
export class AppModule {}
|
||||
8
app/api/src/app.service.ts
Normal file
8
app/api/src/app.service.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Healthy!';
|
||||
}
|
||||
}
|
||||
11
app/api/src/common/dto/pagination-query.dto.ts
Normal file
11
app/api/src/common/dto/pagination-query.dto.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { IsOptional, IsPositive } from 'class-validator';
|
||||
|
||||
export class PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsPositive()
|
||||
limit: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsPositive()
|
||||
offset: number;
|
||||
}
|
||||
60
app/api/src/common/dto/top-players-query.dto.ts
Normal file
60
app/api/src/common/dto/top-players-query.dto.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { IsOptional, IsPositive, IsNotEmpty, IsIn, Max } from 'class-validator';
|
||||
|
||||
const filterableGameTypes = ['CTFGame', 'LakRabbitGame'] as const;
|
||||
|
||||
type FilterableGameType = typeof filterableGameTypes[number];
|
||||
|
||||
const hitStats = [
|
||||
'discHitsTG',
|
||||
'discDmgHitsTG',
|
||||
'discMATG',
|
||||
'laserHitsTG',
|
||||
'laserMATG',
|
||||
'cgHitsTG',
|
||||
'shockHitsTG',
|
||||
'grenadeHitsTG',
|
||||
'grenadeDmgHitsTG',
|
||||
'grenadeMATG',
|
||||
] as const;
|
||||
|
||||
type Stat = typeof hitStats[number];
|
||||
|
||||
export class TopAccuracyQueryDto {
|
||||
@IsNotEmpty()
|
||||
@IsIn(hitStats as any)
|
||||
stat: Stat;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(filterableGameTypes as any)
|
||||
gameType: FilterableGameType;
|
||||
|
||||
@IsOptional()
|
||||
@IsPositive()
|
||||
minGames: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsPositive()
|
||||
minShots: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsPositive()
|
||||
@Max(100)
|
||||
limit: number;
|
||||
|
||||
@IsOptional()
|
||||
timePeriod: string;
|
||||
}
|
||||
|
||||
export class TopWinsQueryDto {
|
||||
@IsOptional()
|
||||
@IsPositive()
|
||||
minGames: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsPositive()
|
||||
@Max(100)
|
||||
limit: number;
|
||||
|
||||
@IsOptional()
|
||||
timePeriod: string;
|
||||
}
|
||||
703
app/api/src/common/util/formatStats.ts
Normal file
703
app/api/src/common/util/formatStats.ts
Normal file
|
|
@ -0,0 +1,703 @@
|
|||
/* eslint-disable */
|
||||
|
||||
// function formatArrayOfNumbers(arr: string): number[] {
|
||||
// return arr ? arr.split(',').map(Number) : [];
|
||||
// };
|
||||
|
||||
function formatArrayOfNumbers(arr){
|
||||
return arr;
|
||||
};
|
||||
|
||||
// Fixup player stat line types
|
||||
function formatPlayerStats(statObj: any) {
|
||||
return {
|
||||
...statObj.stats,
|
||||
aaTurretDeathsTG: Number(statObj.stats.aaTurretDeathsTG),
|
||||
aaTurretKillsTG: Number(statObj.stats.aaTurretKillsTG),
|
||||
airTimeAvg: Number(statObj.stats.airTimeAvg),
|
||||
airTimeTG: Number(statObj.stats.airTimeTG),
|
||||
armorHDTG: Number(statObj.stats.armorHDTG),
|
||||
armorHHDTG: Number(statObj.stats.armorHHDTG),
|
||||
armorHHTG: Number(statObj.stats.armorHHTG),
|
||||
armorHLDTG: Number(statObj.stats.armorHLDTG),
|
||||
armorHLTG: Number(statObj.stats.armorHLTG),
|
||||
armorHMDTG: Number(statObj.stats.armorHMDTG),
|
||||
armorHMTG: Number(statObj.stats.armorHMTG),
|
||||
armorHTG: Number(statObj.stats.armorHTG),
|
||||
armorLDTG: Number(statObj.stats.armorLDTG),
|
||||
armorLHDTG: Number(statObj.stats.armorLHDTG),
|
||||
armorLHTG: Number(statObj.stats.armorLHTG),
|
||||
armorLLDTG: Number(statObj.stats.armorLLDTG),
|
||||
armorLLTG: Number(statObj.stats.armorLLTG),
|
||||
armorLMDTG: Number(statObj.stats.armorLMDTG),
|
||||
armorLMTG: Number(statObj.stats.armorLMTG),
|
||||
armorLTG: Number(statObj.stats.armorLTG),
|
||||
armorMDTG: Number(statObj.stats.armorMDTG),
|
||||
armorMHDTG: Number(statObj.stats.armorMHDTG),
|
||||
armorMHTG: Number(statObj.stats.armorMHTG),
|
||||
armorMLDTG: Number(statObj.stats.armorMLDTG),
|
||||
armorMLTG: Number(statObj.stats.armorMLTG),
|
||||
armorMMDTG: Number(statObj.stats.armorMMDTG),
|
||||
armorMMTG: Number(statObj.stats.armorMMTG),
|
||||
armorMTG: Number(statObj.stats.armorMTG),
|
||||
assaultRDTG: Number(statObj.stats.assaultRDTG),
|
||||
assaultRKTG: Number(statObj.stats.assaultRKTG),
|
||||
assistTG: Number(statObj.stats.assistTG),
|
||||
avgSpeedAvg: Number(statObj.stats.avgSpeedAvg),
|
||||
bellyTurretDeathsTG: Number(statObj.stats.bellyTurretDeathsTG),
|
||||
bellyTurretKillsTG: Number(statObj.stats.bellyTurretKillsTG),
|
||||
blasterACCAvg: Number(statObj.stats.blasterACCAvg),
|
||||
blasterComTG: Number(statObj.stats.blasterComTG),
|
||||
blasterDeathAirAirTG: Number(statObj.stats.blasterDeathAirAirTG),
|
||||
blasterDeathAirGroundTG: Number(statObj.stats.blasterDeathAirGroundTG),
|
||||
blasterDeathAirTG: Number(statObj.stats.blasterDeathAirTG),
|
||||
blasterDeathGroundAirTG: Number(statObj.stats.blasterDeathGroundAirTG),
|
||||
blasterDeathGroundGroundTG: Number(statObj.stats.blasterDeathGroundGroundTG),
|
||||
blasterDeathGroundTG: Number(statObj.stats.blasterDeathGroundTG),
|
||||
blasterDeathsTG: Number(statObj.stats.blasterDeathsTG),
|
||||
blasterDmgTG: Number(statObj.stats.blasterDmgTG),
|
||||
blasterHitDistMax: Number(statObj.stats.blasterHitDistMax),
|
||||
blasterHitsTG: Number(statObj.stats.blasterHitsTG),
|
||||
blasterHitSVMax: Number(statObj.stats.blasterHitSVMax),
|
||||
blasterHitVVMax: Number(statObj.stats.blasterHitVVMax),
|
||||
blasterKillAirAirTG: Number(statObj.stats.blasterKillAirAirTG),
|
||||
blasterKillAirGroundTG: Number(statObj.stats.blasterKillAirGroundTG),
|
||||
blasterKillAirTG: Number(statObj.stats.blasterKillAirTG),
|
||||
blasterKillDistMax: Number(statObj.stats.blasterKillDistMax),
|
||||
blasterKillGroundAirTG: Number(statObj.stats.blasterKillGroundAirTG),
|
||||
blasterKillGroundGroundTG: Number(statObj.stats.blasterKillGroundGroundTG),
|
||||
blasterKillGroundTG: Number(statObj.stats.blasterKillGroundTG),
|
||||
blasterKillsTG: Number(statObj.stats.blasterKillsTG),
|
||||
blasterKillSVMax: Number(statObj.stats.blasterKillSVMax),
|
||||
blasterKillVVMax: Number(statObj.stats.blasterKillVVMax),
|
||||
blasterMADistMax: Number(statObj.stats.blasterMADistMax),
|
||||
blasterMAHitDistMax: Number(statObj.stats.blasterMAHitDistMax),
|
||||
blasterMATG: Number(statObj.stats.blasterMATG),
|
||||
blasterReflectHitTG: Number(statObj.stats.blasterReflectHitTG),
|
||||
blasterReflectKillTG: Number(statObj.stats.blasterReflectKillTG),
|
||||
blasterScoreTG: Number(statObj.stats.blasterScoreTG),
|
||||
blasterShotsFiredTG: Number(statObj.stats.blasterShotsFiredTG),
|
||||
bomberBombsDeathsTG: Number(statObj.stats.bomberBombsDeathsTG),
|
||||
bomberBombsKillsTG: Number(statObj.stats.bomberBombsKillsTG),
|
||||
bomberFlyerRDTG: Number(statObj.stats.bomberFlyerRDTG),
|
||||
bomberFlyerRKTG: Number(statObj.stats.bomberFlyerRKTG),
|
||||
capEfficiencyAvg: Number(statObj.stats.capEfficiencyAvg),
|
||||
carrierKillsTG: Number(statObj.stats.carrierKillsTG),
|
||||
cgACCAvg: Number(statObj.stats.cgACCAvg),
|
||||
cgComTG: Number(statObj.stats.cgComTG),
|
||||
cgDeathAirAirTG: Number(statObj.stats.cgDeathAirAirTG),
|
||||
cgDeathAirGroundTG: Number(statObj.stats.cgDeathAirGroundTG),
|
||||
cgDeathAirTG: Number(statObj.stats.cgDeathAirTG),
|
||||
cgDeathGroundAirTG: Number(statObj.stats.cgDeathGroundAirTG),
|
||||
cgDeathGroundGroundTG: Number(statObj.stats.cgDeathGroundGroundTG),
|
||||
cgDeathGroundTG: Number(statObj.stats.cgDeathGroundTG),
|
||||
cgDeathsTG: Number(statObj.stats.cgDeathsTG),
|
||||
cgDmgTG: Number(statObj.stats.cgDmgTG),
|
||||
cgHitDistMax: Number(statObj.stats.cgHitDistMax),
|
||||
cgHitsTG: Number(statObj.stats.cgHitsTG),
|
||||
cgHitSVMax: Number(statObj.stats.cgHitSVMax),
|
||||
cgHitVVMax: Number(statObj.stats.cgHitVVMax),
|
||||
cgKillAirAirTG: Number(statObj.stats.cgKillAirAirTG),
|
||||
cgKillAirGroundTG: Number(statObj.stats.cgKillAirGroundTG),
|
||||
cgKillAirTG: Number(statObj.stats.cgKillAirTG),
|
||||
cgKillDistMax: Number(statObj.stats.cgKillDistMax),
|
||||
cgKillGroundAirTG: Number(statObj.stats.cgKillGroundAirTG),
|
||||
cgKillGroundGroundTG: Number(statObj.stats.cgKillGroundGroundTG),
|
||||
cgKillGroundTG: Number(statObj.stats.cgKillGroundTG),
|
||||
cgKillsTG: Number(statObj.stats.cgKillsTG),
|
||||
cgKillSVMax: Number(statObj.stats.cgKillSVMax),
|
||||
cgKillVVMax: Number(statObj.stats.cgKillVVMax),
|
||||
cgMADistMax: Number(statObj.stats.cgMADistMax),
|
||||
cgMAHitDistMax: Number(statObj.stats.cgMAHitDistMax),
|
||||
cgMATG: Number(statObj.stats.cgMATG),
|
||||
cgScoreTG: Number(statObj.stats.cgScoreTG),
|
||||
cgShotsFiredTG: Number(statObj.stats.cgShotsFiredTG),
|
||||
chainKillTG: Number(statObj.stats.chainKillTG),
|
||||
comboCountTG: Number(statObj.stats.comboCountTG),
|
||||
concussFlagTG: Number(statObj.stats.concussFlagTG),
|
||||
concussHitTG: Number(statObj.stats.concussHitTG),
|
||||
concussTakenTG: Number(statObj.stats.concussTakenTG),
|
||||
crashDeathsTG: Number(statObj.stats.crashDeathsTG),
|
||||
crashKillsTG: Number(statObj.stats.crashKillsTG),
|
||||
ctrlKKillsTG: Number(statObj.stats.ctrlKKillsTG),
|
||||
dayStamp: Number(statObj.stats.dayStamp),
|
||||
deadDistMax: Number(statObj.stats.deadDistMax),
|
||||
deathAirAirTG: Number(statObj.stats.deathAirAirTG),
|
||||
deathAirGroundTG: Number(statObj.stats.deathAirGroundTG),
|
||||
deathAirTG: Number(statObj.stats.deathAirTG),
|
||||
deathGroundAirTG: Number(statObj.stats.deathGroundAirTG),
|
||||
deathGroundGroundTG: Number(statObj.stats.deathGroundGroundTG),
|
||||
deathGroundTG: Number(statObj.stats.deathGroundTG),
|
||||
deathKillsTG: Number(statObj.stats.deathKillsTG),
|
||||
deathsTG: Number(statObj.stats.deathsTG),
|
||||
decupleChainKillTG: Number(statObj.stats.decupleChainKillTG),
|
||||
decupleKillTG: Number(statObj.stats.decupleKillTG),
|
||||
defenseScoreTG: Number(statObj.stats.defenseScoreTG),
|
||||
depInvRepairsTG: Number(statObj.stats.depInvRepairsTG),
|
||||
depInvyUseTG: Number(statObj.stats.depInvyUseTG),
|
||||
depSensorDestroysTG: Number(statObj.stats.depSensorDestroysTG),
|
||||
depSensorRepairsTG: Number(statObj.stats.depSensorRepairsTG),
|
||||
depStationDestroysTG: Number(statObj.stats.depStationDestroysTG),
|
||||
depTurretDestroysTG: Number(statObj.stats.depTurretDestroysTG),
|
||||
depTurretRepairsTG: Number(statObj.stats.depTurretRepairsTG),
|
||||
destructionTG: Number(statObj.stats.destructionTG),
|
||||
discACCAvg: Number(statObj.stats.discACCAvg),
|
||||
discAoeMATG: Number(statObj.stats.discAoeMATG),
|
||||
discComTG: Number(statObj.stats.discComTG),
|
||||
discDeathAirAirTG: Number(statObj.stats.discDeathAirAirTG),
|
||||
discDeathAirGroundTG: Number(statObj.stats.discDeathAirGroundTG),
|
||||
discDeathAirTG: Number(statObj.stats.discDeathAirTG),
|
||||
discDeathGroundAirTG: Number(statObj.stats.discDeathGroundAirTG),
|
||||
discDeathGroundGroundTG: Number(statObj.stats.discDeathGroundGroundTG),
|
||||
discDeathGroundTG: Number(statObj.stats.discDeathGroundTG),
|
||||
discDeathsTG: Number(statObj.stats.discDeathsTG),
|
||||
discDmgACCAvg: Number(statObj.stats.discDmgACCAvg),
|
||||
discDmgHitsTG: Number(statObj.stats.discDmgHitsTG),
|
||||
discDmgTG: Number(statObj.stats.discDmgTG),
|
||||
discHitDistMax: Number(statObj.stats.discHitDistMax),
|
||||
discHitsTG: Number(statObj.stats.discHitsTG),
|
||||
discHitSVMax: Number(statObj.stats.discHitSVMax),
|
||||
discHitVVMax: Number(statObj.stats.discHitVVMax),
|
||||
discJumpTG: Number(statObj.stats.discJumpTG),
|
||||
discKillAirAirTG: Number(statObj.stats.discKillAirAirTG),
|
||||
discKillAirGroundTG: Number(statObj.stats.discKillAirGroundTG),
|
||||
discKillAirTG: Number(statObj.stats.discKillAirTG),
|
||||
discKillDistMax: Number(statObj.stats.discKillDistMax),
|
||||
discKillGroundAirTG: Number(statObj.stats.discKillGroundAirTG),
|
||||
discKillGroundGroundTG: Number(statObj.stats.discKillGroundGroundTG),
|
||||
discKillGroundTG: Number(statObj.stats.discKillGroundTG),
|
||||
discKillsTG: Number(statObj.stats.discKillsTG),
|
||||
discKillSVMax: Number(statObj.stats.discKillSVMax),
|
||||
discKillVVMax: Number(statObj.stats.discKillVVMax),
|
||||
discMADistMax: Number(statObj.stats.discMADistMax),
|
||||
discMAHitDistMax: Number(statObj.stats.discMAHitDistMax),
|
||||
discMATG: Number(statObj.stats.discMATG),
|
||||
discReflectHitTG: Number(statObj.stats.discReflectHitTG),
|
||||
discReflectKillTG: Number(statObj.stats.discReflectKillTG),
|
||||
discScoreTG: Number(statObj.stats.discScoreTG),
|
||||
discShotsFiredTG: Number(statObj.stats.discShotsFiredTG),
|
||||
distMovTG: Number(statObj.stats.distMovTG),
|
||||
doubleChainKillTG: Number(statObj.stats.doubleChainKillTG),
|
||||
doubleKillTG: Number(statObj.stats.doubleKillTG),
|
||||
dtTeamGame: Number(statObj.stats.dtTeamGame),
|
||||
dtTurretKillsTG: Number(statObj.stats.dtTurretKillsTG),
|
||||
elfShotsFiredTG: Number(statObj.stats.elfShotsFiredTG),
|
||||
elfTurretDeathsTG: Number(statObj.stats.elfTurretDeathsTG),
|
||||
elfTurretKillsTG: Number(statObj.stats.elfTurretKillsTG),
|
||||
escortAssistsTG: Number(statObj.stats.escortAssistsTG),
|
||||
EVDeathsTG: Number(statObj.stats.EVDeathsTG),
|
||||
EVHitWepTG: Number(statObj.stats.EVHitWepTG),
|
||||
EVKillsTG: Number(statObj.stats.EVKillsTG),
|
||||
EVMAHitTG: Number(statObj.stats.EVMAHitTG),
|
||||
explosionDeathsTG: Number(statObj.stats.explosionDeathsTG),
|
||||
explosionKillsTG: Number(statObj.stats.explosionKillsTG),
|
||||
firstKillTG: Number(statObj.stats.firstKillTG),
|
||||
flagCapsTG: Number(statObj.stats.flagCapsTG),
|
||||
flagCatchSpeedMax: Number(statObj.stats.flagCatchSpeedMax),
|
||||
flagCatchTG: Number(statObj.stats.flagCatchTG),
|
||||
flagDefendsTG: Number(statObj.stats.flagDefendsTG),
|
||||
flagGrabsTG: Number(statObj.stats.flagGrabsTG),
|
||||
flagReturnsTG: Number(statObj.stats.flagReturnsTG),
|
||||
flagTimeMinTG: Number(statObj.stats.flagTimeMinTG),
|
||||
flagTossCatchTG: Number(statObj.stats.flagTossCatchTG),
|
||||
flagTossTG: Number(statObj.stats.flagTossTG),
|
||||
flareHitTG: Number(statObj.stats.flareHitTG),
|
||||
flareKillTG: Number(statObj.stats.flareKillTG),
|
||||
forceFieldPowerUpDeathsTG: Number(statObj.stats.forceFieldPowerUpDeathsTG),
|
||||
forceFieldPowerUpKillsTG: Number(statObj.stats.forceFieldPowerUpKillsTG),
|
||||
friendlyFireTG: Number(statObj.stats.friendlyFireTG),
|
||||
fullSet: Number(statObj.stats.fullSet),
|
||||
gamePCT: Number(statObj.stats.gamePCT),
|
||||
startPCTGame: Number(statObj.stats.startPCTGame),
|
||||
endPCTGame: Number(statObj.stats.endPCTGame),
|
||||
teamOneCapTimesGame: formatArrayOfNumbers(statObj.stats.teamOneCapTimesGame),
|
||||
teamTwoCapTimesGame: formatArrayOfNumbers(statObj.stats.teamTwoCapTimesGame),
|
||||
mapSkipGame: Number(statObj.stats.mapSkipGame),
|
||||
clientQuitGame: Number(statObj.stats.clientQuitGame),
|
||||
genDefendsTG: Number(statObj.stats.genDefendsTG),
|
||||
genDestroysTG: Number(statObj.stats.genDestroysTG),
|
||||
genRepairsTG: Number(statObj.stats.genRepairsTG),
|
||||
grabSpeedAvg: Number(statObj.stats.grabSpeedAvg),
|
||||
grabSpeedLowMax: Number(statObj.stats.grabSpeedLowMax),
|
||||
grabSpeedMax: Number(statObj.stats.grabSpeedMax),
|
||||
grenadeACCAvg: Number(statObj.stats.grenadeACCAvg),
|
||||
grenadeAoeMATG: Number(statObj.stats.grenadeAoeMATG),
|
||||
grenadeComTG: Number(statObj.stats.grenadeComTG),
|
||||
grenadeDeathAirAirTG: Number(statObj.stats.grenadeDeathAirAirTG),
|
||||
grenadeDeathAirGroundTG: Number(statObj.stats.grenadeDeathAirGroundTG),
|
||||
grenadeDeathAirTG: Number(statObj.stats.grenadeDeathAirTG),
|
||||
grenadeDeathGroundAirTG: Number(statObj.stats.grenadeDeathGroundAirTG),
|
||||
grenadeDeathGroundGroundTG: Number(statObj.stats.grenadeDeathGroundGroundTG),
|
||||
grenadeDeathGroundTG: Number(statObj.stats.grenadeDeathGroundTG),
|
||||
grenadeDeathsTG: Number(statObj.stats.grenadeDeathsTG),
|
||||
grenadeDmgACCAvg: Number(statObj.stats.grenadeDmgACCAvg),
|
||||
grenadeDmgHitsTG: Number(statObj.stats.grenadeDmgHitsTG),
|
||||
grenadeDmgTG: Number(statObj.stats.grenadeDmgTG),
|
||||
grenadeHitDistMax: Number(statObj.stats.grenadeHitDistMax),
|
||||
grenadeHitsTG: Number(statObj.stats.grenadeHitsTG),
|
||||
grenadeHitSVMax: Number(statObj.stats.grenadeHitSVMax),
|
||||
grenadeHitVVMax: Number(statObj.stats.grenadeHitVVMax),
|
||||
grenadeKillAirAirTG: Number(statObj.stats.grenadeKillAirAirTG),
|
||||
grenadeKillAirGroundTG: Number(statObj.stats.grenadeKillAirGroundTG),
|
||||
grenadeKillAirTG: Number(statObj.stats.grenadeKillAirTG),
|
||||
grenadeKillDistMax: Number(statObj.stats.grenadeKillDistMax),
|
||||
grenadeKillGroundAirTG: Number(statObj.stats.grenadeKillGroundAirTG),
|
||||
grenadeKillGroundGroundTG: Number(statObj.stats.grenadeKillGroundGroundTG),
|
||||
grenadeKillGroundTG: Number(statObj.stats.grenadeKillGroundTG),
|
||||
grenadeKillsTG: Number(statObj.stats.grenadeKillsTG),
|
||||
grenadeKillSVMax: Number(statObj.stats.grenadeKillSVMax),
|
||||
grenadeKillVVMax: Number(statObj.stats.grenadeKillVVMax),
|
||||
grenadeMADistMax: Number(statObj.stats.grenadeMADistMax),
|
||||
grenadeMAHitDistMax: Number(statObj.stats.grenadeMAHitDistMax),
|
||||
grenadeMATG: Number(statObj.stats.grenadeMATG),
|
||||
grenadeScoreTG: Number(statObj.stats.grenadeScoreTG),
|
||||
grenadeShotsFiredTG: Number(statObj.stats.grenadeShotsFiredTG),
|
||||
groundDeathsTG: Number(statObj.stats.groundDeathsTG),
|
||||
groundKillsTG: Number(statObj.stats.groundKillsTG),
|
||||
groundTimeAvg: Number(statObj.stats.groundTimeAvg),
|
||||
groundTimeTG: Number(statObj.stats.groundTimeTG),
|
||||
hapcFlyerRDTG: Number(statObj.stats.hapcFlyerRDTG),
|
||||
hapcFlyerRKTG: Number(statObj.stats.hapcFlyerRKTG),
|
||||
hArmorTimeTG: Number(statObj.stats.hArmorTimeTG),
|
||||
heldTimeSecAvgI: Number(statObj.stats.heldTimeSecAvgI),
|
||||
heldTimeSecLowMin: Number(statObj.stats.heldTimeSecLowMin),
|
||||
heldTimeSecMin: Number(statObj.stats.heldTimeSecMin),
|
||||
hGrenadeACCAvg: Number(statObj.stats.hGrenadeACCAvg),
|
||||
hGrenadeComTG: Number(statObj.stats.hGrenadeComTG),
|
||||
hGrenadeDeathAirAirTG: Number(statObj.stats.hGrenadeDeathAirAirTG),
|
||||
hGrenadeDeathAirGroundTG: Number(statObj.stats.hGrenadeDeathAirGroundTG),
|
||||
hGrenadeDeathAirTG: Number(statObj.stats.hGrenadeDeathAirTG),
|
||||
hGrenadeDeathGroundAirTG: Number(statObj.stats.hGrenadeDeathGroundAirTG),
|
||||
hGrenadeDeathGroundGroundTG: Number(statObj.stats.hGrenadeDeathGroundGroundTG),
|
||||
hGrenadeDeathGroundTG: Number(statObj.stats.hGrenadeDeathGroundTG),
|
||||
hGrenadeDeathsTG: Number(statObj.stats.hGrenadeDeathsTG),
|
||||
hGrenadeDmgTG: Number(statObj.stats.hGrenadeDmgTG),
|
||||
hGrenadeHitDistMax: Number(statObj.stats.hGrenadeHitDistMax),
|
||||
hGrenadeHitsTG: Number(statObj.stats.hGrenadeHitsTG),
|
||||
hGrenadeHitSVMax: Number(statObj.stats.hGrenadeHitSVMax),
|
||||
hGrenadeHitVVMax: Number(statObj.stats.hGrenadeHitVVMax),
|
||||
hGrenadeKillAirAirTG: Number(statObj.stats.hGrenadeKillAirAirTG),
|
||||
hGrenadeKillAirGroundTG: Number(statObj.stats.hGrenadeKillAirGroundTG),
|
||||
hGrenadeKillAirTG: Number(statObj.stats.hGrenadeKillAirTG),
|
||||
hGrenadeKillDistMax: Number(statObj.stats.hGrenadeKillDistMax),
|
||||
hGrenadeKillGroundAirTG: Number(statObj.stats.hGrenadeKillGroundAirTG),
|
||||
hGrenadeKillGroundGroundTG: Number(statObj.stats.hGrenadeKillGroundGroundTG),
|
||||
hGrenadeKillGroundTG: Number(statObj.stats.hGrenadeKillGroundTG),
|
||||
hGrenadeKillsTG: Number(statObj.stats.hGrenadeKillsTG),
|
||||
hGrenadeKillSVMax: Number(statObj.stats.hGrenadeKillSVMax),
|
||||
hGrenadeKillVVMax: Number(statObj.stats.hGrenadeKillVVMax),
|
||||
hGrenadeMADistMax: Number(statObj.stats.hGrenadeMADistMax),
|
||||
hGrenadeMAHitDistMax: Number(statObj.stats.hGrenadeMAHitDistMax),
|
||||
hGrenadeMATG: Number(statObj.stats.hGrenadeMATG),
|
||||
hGrenadeScoreTG: Number(statObj.stats.hGrenadeScoreTG),
|
||||
hGrenadeShotsFiredTG: Number(statObj.stats.hGrenadeShotsFiredTG),
|
||||
hitHeadBackTG: Number(statObj.stats.hitHeadBackTG),
|
||||
hitHeadFrontTG: Number(statObj.stats.hitHeadFrontTG),
|
||||
hitHeadLeftTG: Number(statObj.stats.hitHeadLeftTG),
|
||||
hitHeadRightTG: Number(statObj.stats.hitHeadRightTG),
|
||||
hitHeadTG: Number(statObj.stats.hitHeadTG),
|
||||
hitLegBackLTG: Number(statObj.stats.hitLegBackLTG),
|
||||
hitLegBackRTG: Number(statObj.stats.hitLegBackRTG),
|
||||
hitLegFrontLTG: Number(statObj.stats.hitLegFrontLTG),
|
||||
hitLegFrontRTG: Number(statObj.stats.hitLegFrontRTG),
|
||||
hitLegsTG: Number(statObj.stats.hitLegsTG),
|
||||
hitTakenHeadBackTG: Number(statObj.stats.hitTakenHeadBackTG),
|
||||
hitTakenHeadFrontTG: Number(statObj.stats.hitTakenHeadFrontTG),
|
||||
hitTakenHeadLeftTG: Number(statObj.stats.hitTakenHeadLeftTG),
|
||||
hitTakenHeadRightTG: Number(statObj.stats.hitTakenHeadRightTG),
|
||||
hitTakenHeadTG: Number(statObj.stats.hitTakenHeadTG),
|
||||
hitTakenLegBackLTG: Number(statObj.stats.hitTakenLegBackLTG),
|
||||
hitTakenLegBackRTG: Number(statObj.stats.hitTakenLegBackRTG),
|
||||
hitTakenLegFrontLTG: Number(statObj.stats.hitTakenLegFrontLTG),
|
||||
hitTakenLegFrontRTG: Number(statObj.stats.hitTakenLegFrontRTG),
|
||||
hitTakenLegsTG: Number(statObj.stats.hitTakenLegsTG),
|
||||
hitTakenTorsoBackLTG: Number(statObj.stats.hitTakenTorsoBackLTG),
|
||||
hitTakenTorsoBackRTG: Number(statObj.stats.hitTakenTorsoBackRTG),
|
||||
hitTakenTorsoFrontLTG: Number(statObj.stats.hitTakenTorsoFrontLTG),
|
||||
hitTakenTorsoFrontRTG: Number(statObj.stats.hitTakenTorsoFrontRTG),
|
||||
hitTakenTorsoTG: Number(statObj.stats.hitTakenTorsoTG),
|
||||
hitTorsoBackLTG: Number(statObj.stats.hitTorsoBackLTG),
|
||||
hitTorsoBackRTG: Number(statObj.stats.hitTorsoBackRTG),
|
||||
hitTorsoFrontLTG: Number(statObj.stats.hitTorsoFrontLTG),
|
||||
hitTorsoFrontRTG: Number(statObj.stats.hitTorsoFrontRTG),
|
||||
hitTorsoTG: Number(statObj.stats.hitTorsoTG),
|
||||
idleTimeAvg: Number(statObj.stats.idleTimeAvg),
|
||||
idleTimeTG: Number(statObj.stats.idleTimeTG),
|
||||
impactDeathsTG: Number(statObj.stats.impactDeathsTG),
|
||||
impactKillsTG: Number(statObj.stats.impactKillsTG),
|
||||
indoorDepTurretDeathsTG: Number(statObj.stats.indoorDepTurretDeathsTG),
|
||||
indoorDepTurretKillsTG: Number(statObj.stats.indoorDepTurretKillsTG),
|
||||
interceptedFlagTG: Number(statObj.stats.interceptedFlagTG),
|
||||
interceptFlagSpeedMax: Number(statObj.stats.interceptFlagSpeedMax),
|
||||
interceptSpeedMax: Number(statObj.stats.interceptSpeedMax),
|
||||
inventoryDeathsTG: Number(statObj.stats.inventoryDeathsTG),
|
||||
InventoryDepTG: Number(statObj.stats.InventoryDepTG),
|
||||
inventoryKillsTG: Number(statObj.stats.inventoryKillsTG),
|
||||
iStationDestroysTG: Number(statObj.stats.iStationDestroysTG),
|
||||
kdrAvg: Number(statObj.stats.kdrAvg),
|
||||
killAirAirTG: Number(statObj.stats.killAirAirTG),
|
||||
killAirGroundTG: Number(statObj.stats.killAirGroundTG),
|
||||
killAirTG: Number(statObj.stats.killAirTG),
|
||||
killerDiscJumpTG: Number(statObj.stats.killerDiscJumpTG),
|
||||
killGroundAirTG: Number(statObj.stats.killGroundAirTG),
|
||||
killGroundGroundTG: Number(statObj.stats.killGroundGroundTG),
|
||||
killGroundTG: Number(statObj.stats.killGroundTG),
|
||||
killsTG: Number(statObj.stats.killsTG),
|
||||
killStreakMax: Number(statObj.stats.killStreakMax),
|
||||
killStreakTG: Number(statObj.stats.killStreakTG),
|
||||
lArmorTimeTG: Number(statObj.stats.lArmorTimeTG),
|
||||
laserACCAvg: Number(statObj.stats.laserACCAvg),
|
||||
laserComTG: Number(statObj.stats.laserComTG),
|
||||
laserDeathAirAirTG: Number(statObj.stats.laserDeathAirAirTG),
|
||||
laserDeathAirGroundTG: Number(statObj.stats.laserDeathAirGroundTG),
|
||||
laserDeathAirTG: Number(statObj.stats.laserDeathAirTG),
|
||||
laserDeathGroundAirTG: Number(statObj.stats.laserDeathGroundAirTG),
|
||||
laserDeathGroundGroundTG: Number(statObj.stats.laserDeathGroundGroundTG),
|
||||
laserDeathGroundTG: Number(statObj.stats.laserDeathGroundTG),
|
||||
laserDeathsTG: Number(statObj.stats.laserDeathsTG),
|
||||
laserDmgTG: Number(statObj.stats.laserDmgTG),
|
||||
laserHeadShotTG: Number(statObj.stats.laserHeadShotTG),
|
||||
laserHitDistMax: Number(statObj.stats.laserHitDistMax),
|
||||
laserHitsTG: Number(statObj.stats.laserHitsTG),
|
||||
laserHitSVMax: Number(statObj.stats.laserHitSVMax),
|
||||
laserHitVVMax: Number(statObj.stats.laserHitVVMax),
|
||||
laserKillAirAirTG: Number(statObj.stats.laserKillAirAirTG),
|
||||
laserKillAirGroundTG: Number(statObj.stats.laserKillAirGroundTG),
|
||||
laserKillAirTG: Number(statObj.stats.laserKillAirTG),
|
||||
laserKillDistMax: Number(statObj.stats.laserKillDistMax),
|
||||
laserKillGroundAirTG: Number(statObj.stats.laserKillGroundAirTG),
|
||||
laserKillGroundGroundTG: Number(statObj.stats.laserKillGroundGroundTG),
|
||||
laserKillGroundTG: Number(statObj.stats.laserKillGroundTG),
|
||||
laserKillsTG: Number(statObj.stats.laserKillsTG),
|
||||
laserKillSVMax: Number(statObj.stats.laserKillSVMax),
|
||||
laserKillVVMax: Number(statObj.stats.laserKillVVMax),
|
||||
laserMADistMax: Number(statObj.stats.laserMADistMax),
|
||||
laserMAHitDistMax: Number(statObj.stats.laserMAHitDistMax),
|
||||
laserMATG: Number(statObj.stats.laserMATG),
|
||||
laserScoreTG: Number(statObj.stats.laserScoreTG),
|
||||
laserShotsFiredTG: Number(statObj.stats.laserShotsFiredTG),
|
||||
lastKillTG: Number(statObj.stats.lastKillTG),
|
||||
lavaDeathsTG: Number(statObj.stats.lavaDeathsTG),
|
||||
lavaKillsTG: Number(statObj.stats.lavaKillsTG),
|
||||
lightningDeathsTG: Number(statObj.stats.lightningDeathsTG),
|
||||
lightningKillsTG: Number(statObj.stats.lightningKillsTG),
|
||||
lightningMAEVHitsTG: Number(statObj.stats.lightningMAEVHitsTG),
|
||||
lightningMAEVKillsTG: Number(statObj.stats.lightningMAEVKillsTG),
|
||||
lightningMAHitsTG: Number(statObj.stats.lightningMAHitsTG),
|
||||
lightningMAkillsTG: Number(statObj.stats.lightningMAkillsTG),
|
||||
lossCountTG: Number(statObj.stats.lossCountTG),
|
||||
maFlagCatchSpeedMax: Number(statObj.stats.maFlagCatchSpeedMax),
|
||||
maFlagCatchTG: Number(statObj.stats.maFlagCatchTG),
|
||||
maHitDistMax: Number(statObj.stats.maHitDistMax),
|
||||
maHitHeightMax: Number(statObj.stats.maHitHeightMax),
|
||||
maHitSVMax: Number(statObj.stats.maHitSVMax),
|
||||
maInterceptedFlagTG: Number(statObj.stats.maInterceptedFlagTG),
|
||||
mannedTurretKillsTG: Number(statObj.stats.mannedTurretKillsTG),
|
||||
mapGameID: Number(statObj.stats.mapGameID),
|
||||
mArmorTimeTG: Number(statObj.stats.mArmorTimeTG),
|
||||
masTG: Number(statObj.stats.masTG),
|
||||
maxSpeedMax: Number(statObj.stats.maxSpeedMax),
|
||||
MidairflagGrabPointsTG: Number(statObj.stats.MidairflagGrabPointsTG),
|
||||
MidairflagGrabsTG: Number(statObj.stats.MidairflagGrabsTG),
|
||||
mineACCAvg: Number(statObj.stats.mineACCAvg),
|
||||
mineComTG: Number(statObj.stats.mineComTG),
|
||||
mineDeathAirAirTG: Number(statObj.stats.mineDeathAirAirTG),
|
||||
mineDeathAirGroundTG: Number(statObj.stats.mineDeathAirGroundTG),
|
||||
mineDeathAirTG: Number(statObj.stats.mineDeathAirTG),
|
||||
mineDeathGroundAirTG: Number(statObj.stats.mineDeathGroundAirTG),
|
||||
mineDeathGroundGroundTG: Number(statObj.stats.mineDeathGroundGroundTG),
|
||||
mineDeathGroundTG: Number(statObj.stats.mineDeathGroundTG),
|
||||
mineDeathsTG: Number(statObj.stats.mineDeathsTG),
|
||||
mineDiscAccAvg: Number(statObj.stats.mineDiscAccAvg),
|
||||
mineDiscAccMPAvg: Number(statObj.stats.mineDiscAccMPAvg),
|
||||
mineDiscHitTG: Number(statObj.stats.mineDiscHitTG),
|
||||
mineDiscPctAvg: Number(statObj.stats.mineDiscPctAvg),
|
||||
mineDiscShotsTG: Number(statObj.stats.mineDiscShotsTG),
|
||||
mineDmgTG: Number(statObj.stats.mineDmgTG),
|
||||
mineHitDistMax: Number(statObj.stats.mineHitDistMax),
|
||||
mineHitsTG: Number(statObj.stats.mineHitsTG),
|
||||
mineHitVVMax: Number(statObj.stats.mineHitVVMax),
|
||||
mineKillAGroundAirTG: Number(statObj.stats.mineKillAGroundAirTG),
|
||||
mineKillAGroundGroundTG: Number(statObj.stats.mineKillAGroundGroundTG),
|
||||
mineKillAirAirTG: Number(statObj.stats.mineKillAirAirTG),
|
||||
mineKillAirGroundTG: Number(statObj.stats.mineKillAirGroundTG),
|
||||
mineKillAirTG: Number(statObj.stats.mineKillAirTG),
|
||||
mineKillDistMax: Number(statObj.stats.mineKillDistMax),
|
||||
mineKillGroundAirTG: Number(statObj.stats.mineKillGroundAirTG),
|
||||
mineKillGroundGroundTG: Number(statObj.stats.mineKillGroundGroundTG),
|
||||
mineKillGroundTG: Number(statObj.stats.mineKillGroundTG),
|
||||
mineKillsTG: Number(statObj.stats.mineKillsTG),
|
||||
mineKillVVMax: Number(statObj.stats.mineKillVVMax),
|
||||
mineMADistMax: Number(statObj.stats.mineMADistMax),
|
||||
mineMAHitDistMax: Number(statObj.stats.mineMAHitDistMax),
|
||||
mineMATG: Number(statObj.stats.mineMATG),
|
||||
minePlusDiscKillTG: Number(statObj.stats.minePlusDiscKillTG),
|
||||
minePlusDiscTG: Number(statObj.stats.minePlusDiscTG),
|
||||
mineScoreTG: Number(statObj.stats.mineScoreTG),
|
||||
mineShotsFiredTG: Number(statObj.stats.mineShotsFiredTG),
|
||||
missileACCAvg: Number(statObj.stats.missileACCAvg),
|
||||
missileComTG: Number(statObj.stats.missileComTG),
|
||||
missileDeathAirAirTG: Number(statObj.stats.missileDeathAirAirTG),
|
||||
missileDeathAirGroundTG: Number(statObj.stats.missileDeathAirGroundTG),
|
||||
missileDeathAirTG: Number(statObj.stats.missileDeathAirTG),
|
||||
missileDeathGroundAirTG: Number(statObj.stats.missileDeathGroundAirTG),
|
||||
missileDeathGroundGroundTG: Number(statObj.stats.missileDeathGroundGroundTG),
|
||||
missileDeathGroundTG: Number(statObj.stats.missileDeathGroundTG),
|
||||
missileDeathsTG: Number(statObj.stats.missileDeathsTG),
|
||||
missileDmgTG: Number(statObj.stats.missileDmgTG),
|
||||
missileHitDistMax: Number(statObj.stats.missileHitDistMax),
|
||||
missileHitsTG: Number(statObj.stats.missileHitsTG),
|
||||
missileHitSVMax: Number(statObj.stats.missileHitSVMax),
|
||||
missileHitVVMax: Number(statObj.stats.missileHitVVMax),
|
||||
missileKillAirAirTG: Number(statObj.stats.missileKillAirAirTG),
|
||||
missileKillAirGroundTG: Number(statObj.stats.missileKillAirGroundTG),
|
||||
missileKillAirTG: Number(statObj.stats.missileKillAirTG),
|
||||
missileKillDistMax: Number(statObj.stats.missileKillDistMax),
|
||||
missileKillGroundAirTG: Number(statObj.stats.missileKillGroundAirTG),
|
||||
missileKillGroundGroundTG: Number(statObj.stats.missileKillGroundGroundTG),
|
||||
missileKillGroundTG: Number(statObj.stats.missileKillGroundTG),
|
||||
missileKillsTG: Number(statObj.stats.missileKillsTG),
|
||||
missileKillSVMax: Number(statObj.stats.missileKillSVMax),
|
||||
missileKillVVMax: Number(statObj.stats.missileKillVVMax),
|
||||
missileMADistMax: Number(statObj.stats.missileMADistMax),
|
||||
missileMAHitDistMax: Number(statObj.stats.missileMAHitDistMax),
|
||||
missileMATG: Number(statObj.stats.missileMATG),
|
||||
missileScoreTG: Number(statObj.stats.missileScoreTG),
|
||||
missileShotsFiredTG: Number(statObj.stats.missileShotsFiredTG),
|
||||
missileTKTG: Number(statObj.stats.missileTKTG),
|
||||
missileTurretDeathsTG: Number(statObj.stats.missileTurretDeathsTG),
|
||||
missileTurretKillsTG: Number(statObj.stats.missileTurretKillsTG),
|
||||
mobileBaseRDTG: Number(statObj.stats.mobileBaseRDTG),
|
||||
mobileBaseRKTG: Number(statObj.stats.mobileBaseRKTG),
|
||||
monthStamp: Number(statObj.stats.monthStamp),
|
||||
morepointsTG: Number(statObj.stats.morepointsTG),
|
||||
mortarACCAvg: Number(statObj.stats.mortarACCAvg),
|
||||
mortarAoeMATG: Number(statObj.stats.mortarAoeMATG),
|
||||
mortarComTG: Number(statObj.stats.mortarComTG),
|
||||
mortarDeathAirAirTG: Number(statObj.stats.mortarDeathAirAirTG),
|
||||
mortarDeathAirGroundTG: Number(statObj.stats.mortarDeathAirGroundTG),
|
||||
mortarDeathAirTG: Number(statObj.stats.mortarDeathAirTG),
|
||||
mortarDeathGroundAirTG: Number(statObj.stats.mortarDeathGroundAirTG),
|
||||
mortarDeathGroundGroundTG: Number(statObj.stats.mortarDeathGroundGroundTG),
|
||||
mortarDeathGroundTG: Number(statObj.stats.mortarDeathGroundTG),
|
||||
mortarDeathsTG: Number(statObj.stats.mortarDeathsTG),
|
||||
mortarDmgACCAvg: Number(statObj.stats.mortarDmgACCAvg),
|
||||
mortarDmgHitsTG: Number(statObj.stats.mortarDmgHitsTG),
|
||||
mortarDmgTG: Number(statObj.stats.mortarDmgTG),
|
||||
mortarHitDistMax: Number(statObj.stats.mortarHitDistMax),
|
||||
mortarHitsTG: Number(statObj.stats.mortarHitsTG),
|
||||
mortarHitSVMax: Number(statObj.stats.mortarHitSVMax),
|
||||
mortarHitVVMax: Number(statObj.stats.mortarHitVVMax),
|
||||
mortarKillAirAirTG: Number(statObj.stats.mortarKillAirAirTG),
|
||||
mortarKillAirGroundTG: Number(statObj.stats.mortarKillAirGroundTG),
|
||||
mortarKillAirTG: Number(statObj.stats.mortarKillAirTG),
|
||||
mortarKillDistMax: Number(statObj.stats.mortarKillDistMax),
|
||||
mortarKillGroundAirTG: Number(statObj.stats.mortarKillGroundAirTG),
|
||||
mortarKillGroundGroundTG: Number(statObj.stats.mortarKillGroundGroundTG),
|
||||
mortarKillGroundTG: Number(statObj.stats.mortarKillGroundTG),
|
||||
mortarKillsTG: Number(statObj.stats.mortarKillsTG),
|
||||
mortarKillSVMax: Number(statObj.stats.mortarKillSVMax),
|
||||
mortarKillVVMax: Number(statObj.stats.mortarKillVVMax),
|
||||
mortarMADistMax: Number(statObj.stats.mortarMADistMax),
|
||||
mortarMAHitDistMax: Number(statObj.stats.mortarMAHitDistMax),
|
||||
mortarMATG: Number(statObj.stats.mortarMATG),
|
||||
mortarScoreTG: Number(statObj.stats.mortarScoreTG),
|
||||
mortarShotsFiredTG: Number(statObj.stats.mortarShotsFiredTG),
|
||||
mortarTurretDeathsTG: Number(statObj.stats.mortarTurretDeathsTG),
|
||||
mortarTurretKillsTG: Number(statObj.stats.mortarTurretKillsTG),
|
||||
MotionSensorDepTG: Number(statObj.stats.MotionSensorDepTG),
|
||||
mpbGlitchTG: Number(statObj.stats.mpbGlitchTG),
|
||||
mpbtstationDestroysTG: Number(statObj.stats.mpbtstationDestroysTG),
|
||||
mpbtstationRepairsTG: Number(statObj.stats.mpbtstationRepairsTG),
|
||||
multiKillTG: Number(statObj.stats.multiKillTG),
|
||||
nexusCampingDeathsTG: Number(statObj.stats.nexusCampingDeathsTG),
|
||||
nexusCampingKillsTG: Number(statObj.stats.nexusCampingKillsTG),
|
||||
nonupleChainKillTG: Number(statObj.stats.nonupleChainKillTG),
|
||||
nonupleKillTG: Number(statObj.stats.nonupleKillTG),
|
||||
nuclearKillTG: Number(statObj.stats.nuclearKillTG),
|
||||
nullTG: Number(statObj.stats.nullTG),
|
||||
octupleChainKillTG: Number(statObj.stats.octupleChainKillTG),
|
||||
octupleKillTG: Number(statObj.stats.octupleKillTG),
|
||||
offenseScoreTG: Number(statObj.stats.offenseScoreTG),
|
||||
onFireAvg: Number(statObj.stats.onFireAvg),
|
||||
onFireTG: Number(statObj.stats.onFireTG),
|
||||
onInputAvg: Number(statObj.stats.onInputAvg),
|
||||
onInputTG: Number(statObj.stats.onInputTG),
|
||||
onTargetAccAvg: Number(statObj.stats.onTargetAccAvg),
|
||||
onTargetHitTG: Number(statObj.stats.onTargetHitTG),
|
||||
onTargetHMRAvg: Number(statObj.stats.onTargetHMRAvg),
|
||||
onTargetMisTG: Number(statObj.stats.onTargetMisTG),
|
||||
outdoorDepTurretDeathsTG: Number(statObj.stats.outdoorDepTurretDeathsTG),
|
||||
outdoorDepTurretKillsTG: Number(statObj.stats.outdoorDepTurretKillsTG),
|
||||
outOfBoundDeathsTG: Number(statObj.stats.outOfBoundDeathsTG),
|
||||
outOfBoundKillsTG: Number(statObj.stats.outOfBoundKillsTG),
|
||||
plasmaACCAvg: Number(statObj.stats.plasmaACCAvg),
|
||||
plasmaAoeMATG: Number(statObj.stats.plasmaAoeMATG),
|
||||
plasmaComTG: Number(statObj.stats.plasmaComTG),
|
||||
plasmaDeathAirAirTG: Number(statObj.stats.plasmaDeathAirAirTG),
|
||||
plasmaDeathAirGroundTG: Number(statObj.stats.plasmaDeathAirGroundTG),
|
||||
plasmaDeathAirTG: Number(statObj.stats.plasmaDeathAirTG),
|
||||
plasmaDeathGroundAirTG: Number(statObj.stats.plasmaDeathGroundAirTG),
|
||||
plasmaDeathGroundGroundTG: Number(statObj.stats.plasmaDeathGroundGroundTG),
|
||||
plasmaDeathGroundTG: Number(statObj.stats.plasmaDeathGroundTG),
|
||||
plasmaDeathsTG: Number(statObj.stats.plasmaDeathsTG),
|
||||
plasmaDmgACCAvg: Number(statObj.stats.plasmaDmgACCAvg),
|
||||
plasmaDmgHitsTG: Number(statObj.stats.plasmaDmgHitsTG),
|
||||
plasmaDmgTG: Number(statObj.stats.plasmaDmgTG),
|
||||
plasmaHitDistMax: Number(statObj.stats.plasmaHitDistMax),
|
||||
plasmaHitsTG: Number(statObj.stats.plasmaHitsTG),
|
||||
plasmaHitSVMax: Number(statObj.stats.plasmaHitSVMax),
|
||||
plasmaHitVVMax: Number(statObj.stats.plasmaHitVVMax),
|
||||
plasmaKillAirAirTG: Number(statObj.stats.plasmaKillAirAirTG),
|
||||
plasmaKillAirGroundTG: Number(statObj.stats.plasmaKillAirGroundTG),
|
||||
plasmaKillAirTG: Number(statObj.stats.plasmaKillAirTG),
|
||||
plasmaKillDistMax: Number(statObj.stats.plasmaKillDistMax),
|
||||
plasmaKillGroundAirTG: Number(statObj.stats.plasmaKillGroundAirTG),
|
||||
plasmaKillGroundGroundTG: Number(statObj.stats.plasmaKillGroundGroundTG),
|
||||
plasmaKillGroundTG: Number(statObj.stats.plasmaKillGroundTG),
|
||||
plasmaKillsTG: Number(statObj.stats.plasmaKillsTG),
|
||||
plasmaKillSVMax: Number(statObj.stats.plasmaKillSVMax),
|
||||
plasmaKillVVMax: Number(statObj.stats.plasmaKillVVMax),
|
||||
plasmaMADistMax: Number(statObj.stats.plasmaMADistMax),
|
||||
plasmaMAHitDistMax: Number(statObj.stats.plasmaMAHitDistMax),
|
||||
plasmaMATG: Number(statObj.stats.plasmaMATG),
|
||||
plasmaScoreTG: Number(statObj.stats.plasmaScoreTG),
|
||||
plasmaShotsFiredTG: Number(statObj.stats.plasmaShotsFiredTG),
|
||||
plasmaTurretDeathsTG: Number(statObj.stats.plasmaTurretDeathsTG),
|
||||
plasmaTurretKillsTG: Number(statObj.stats.plasmaTurretKillsTG),
|
||||
playerName: Number(statObj.stats.playerName),
|
||||
PulseSensorDepTG: Number(statObj.stats.PulseSensorDepTG),
|
||||
quadrupleChainKillTG: Number(statObj.stats.quadrupleChainKillTG),
|
||||
quadrupleKillTG: Number(statObj.stats.quadrupleKillTG),
|
||||
quarterStamp: Number(statObj.stats.quarterStamp),
|
||||
quintupleChainKillTG: Number(statObj.stats.quintupleChainKillTG),
|
||||
quintupleKillTG: Number(statObj.stats.quintupleKillTG),
|
||||
repairEnemyTG: Number(statObj.stats.repairEnemyTG),
|
||||
repairsTG: Number(statObj.stats.repairsTG),
|
||||
roadDeathsTG: Number(statObj.stats.roadDeathsTG),
|
||||
roadKillsTG: Number(statObj.stats.roadKillsTG),
|
||||
satchelACCAvg: Number(statObj.stats.satchelACCAvg),
|
||||
satchelComTG: Number(statObj.stats.satchelComTG),
|
||||
satchelDeathAirAirTG: Number(statObj.stats.satchelDeathAirAirTG),
|
||||
satchelDeathAirGroundTG: Number(statObj.stats.satchelDeathAirGroundTG),
|
||||
satchelDeathAirTG: Number(statObj.stats.satchelDeathAirTG),
|
||||
satchelDeathGroundAirTG: Number(statObj.stats.satchelDeathGroundAirTG),
|
||||
satchelDeathGroundGroundTG: Number(statObj.stats.satchelDeathGroundGroundTG),
|
||||
satchelDeathsTG: Number(statObj.stats.satchelDeathsTG),
|
||||
satchelDmgTG: Number(statObj.stats.satchelDmgTG),
|
||||
satchelHitDistMax: Number(statObj.stats.satchelHitDistMax),
|
||||
satchelHitsTG: Number(statObj.stats.satchelHitsTG),
|
||||
satchelHitVVMax: Number(statObj.stats.satchelHitVVMax),
|
||||
satchelKillAGroundAirTG: Number(statObj.stats.satchelKillAGroundAirTG),
|
||||
satchelKillAGroundGroundTG: Number(statObj.stats.satchelKillAGroundGroundTG),
|
||||
satchelKillAirAirTG: Number(statObj.stats.satchelKillAirAirTG),
|
||||
satchelKillAirGroundTG: Number(statObj.stats.satchelKillAirGroundTG),
|
||||
satchelKillAirTG: Number(statObj.stats.satchelKillAirTG),
|
||||
satchelKillDistMax: Number(statObj.stats.satchelKillDistMax),
|
||||
satchelKillGroundAirTG: Number(statObj.stats.satchelKillGroundAirTG),
|
||||
satchelKillGroundGroundTG: Number(statObj.stats.satchelKillGroundGroundTG),
|
||||
satchelKillGroundTG: Number(statObj.stats.satchelKillGroundTG),
|
||||
satchelKillsTG: Number(statObj.stats.satchelKillsTG),
|
||||
satchelKillVVMax: Number(statObj.stats.satchelKillVVMax),
|
||||
satchelMATG: Number(statObj.stats.satchelMATG),
|
||||
satchelScoreTG: Number(statObj.stats.satchelScoreTG),
|
||||
satchelShotsFiredTG: Number(statObj.stats.satchelShotsFiredTG),
|
||||
scoreAvg: Number(statObj.stats.scoreAvg),
|
||||
scoreHeadshotTG: Number(statObj.stats.scoreHeadshotTG),
|
||||
scoreMax: Number(statObj.stats.scoreMax),
|
||||
scoreMidAirTG: Number(statObj.stats.scoreMidAirTG),
|
||||
scoreRearshotTG: Number(statObj.stats.scoreRearshotTG),
|
||||
scoreTG: Number(statObj.stats.scoreTG),
|
||||
scoutFlyerRDTG: Number(statObj.stats.scoutFlyerRDTG),
|
||||
scoutFlyerRKTG: Number(statObj.stats.scoutFlyerRKTG),
|
||||
sensorDestroysTG: Number(statObj.stats.sensorDestroysTG),
|
||||
SensorRepairsTG: Number(statObj.stats.SensorRepairsTG),
|
||||
sentryDestroysTG: Number(statObj.stats.sentryDestroysTG),
|
||||
sentryRepairsTG: Number(statObj.stats.sentryRepairsTG),
|
||||
sentryTurretDeathsTG: Number(statObj.stats.sentryTurretDeathsTG),
|
||||
sentryTurretKillsTG: Number(statObj.stats.sentryTurretKillsTG),
|
||||
septupleChainKillTG: Number(statObj.stats.septupleChainKillTG),
|
||||
septupleKillTG: Number(statObj.stats.septupleKillTG),
|
||||
sextupleChainKillTG: Number(statObj.stats.sextupleChainKillTG),
|
||||
sextupleKillTG: Number(statObj.stats.sextupleKillTG),
|
||||
shockACCAvg: Number(statObj.stats.shockACCAvg),
|
||||
shockComTG: Number(statObj.stats.shockComTG),
|
||||
shockDeathAirAirTG: Number(statObj.stats.shockDeathAirAirTG),
|
||||
shockDeathAirGroundTG: Number(statObj.stats.shockDeathAirGroundTG),
|
||||
shockDeathAirTG: Number(statObj.stats.shockDeathAirTG),
|
||||
shockDeathGroundAirTG: Number(statObj.stats.shockDeathGroundAirTG),
|
||||
shockDeathGroundGroundTG: Number(statObj.stats.shockDeathGroundGroundTG),
|
||||
shockDeathGroundTG: Number(statObj.stats.shockDeathGroundTG),
|
||||
shockDeathsTG: Number(statObj.stats.shockDeathsTG),
|
||||
shockDmgTG: Number(statObj.stats.shockDmgTG),
|
||||
shockHitDistMax: Number(statObj.stats.shockHitDistMax),
|
||||
shockHitsTG: Number(statObj.stats.shockHitsTG),
|
||||
shockHitSVMax: Number(statObj.stats.shockHitSVMax),
|
||||
shockHitVVMax: Number(statObj.stats.shockHitVVMax),
|
||||
shockKillAirAirTG: Number(statObj.stats.shockKillAirAirTG),
|
||||
shockKillAirGroundTG: Number(statObj.stats.shockKillAirGroundTG),
|
||||
shockKillAirTG: Number(statObj.stats.shockKillAirTG),
|
||||
shockKillDistMax: Number(statObj.stats.shockKillDistMax),
|
||||
shockKillGroundAirTG: Number(statObj.stats.shockKillGroundAirTG),
|
||||
shockKillGroundGroundTG: Number(statObj.stats.shockKillGroundGroundTG),
|
||||
shockKillGroundTG: Number(statObj.stats.shockKillGroundTG),
|
||||
shockKillsTG: Number(statObj.stats.shockKillsTG),
|
||||
shockKillSVMax: Number(statObj.stats.shockKillSVMax),
|
||||
shockKillVVMax: Number(statObj.stats.shockKillVVMax),
|
||||
shockMADistMax: Number(statObj.stats.shockMADistMax),
|
||||
shockMAHitDistMax: Number(statObj.stats.shockMAHitDistMax),
|
||||
shockMATG: Number(statObj.stats.shockMATG),
|
||||
shockRearShotTG: Number(statObj.stats.shockRearShotTG),
|
||||
shockScoreTG: Number(statObj.stats.shockScoreTG),
|
||||
shockShotsFiredTG: Number(statObj.stats.shockShotsFiredTG),
|
||||
shotsFiredTG: Number(statObj.stats.shotsFiredTG),
|
||||
shrikeBlasterDeathsTG: Number(statObj.stats.shrikeBlasterDeathsTG),
|
||||
shrikeBlasterKillsTG: Number(statObj.stats.shrikeBlasterKillsTG),
|
||||
solarDestroysTG: Number(statObj.stats.solarDestroysTG),
|
||||
solarRepairsTG: Number(statObj.stats.solarRepairsTG),
|
||||
StationRepairsTG: Number(statObj.stats.StationRepairsTG),
|
||||
statsOverWrite: Number(statObj.stats.statsOverWrite),
|
||||
suicidesTG: Number(statObj.stats.suicidesTG),
|
||||
tankChaingunDeathsTG: Number(statObj.stats.tankChaingunDeathsTG),
|
||||
tankChaingunKillsTG: Number(statObj.stats.tankChaingunKillsTG),
|
||||
tankMortarDeathsTG: Number(statObj.stats.tankMortarDeathsTG),
|
||||
tankMortarKillsTG: Number(statObj.stats.tankMortarKillsTG),
|
||||
teamKillsTG: Number(statObj.stats.teamKillsTG),
|
||||
teamScoreGame: Number(statObj.stats.teamScoreGame),
|
||||
timeTLAvg: Number(statObj.stats.timeTLAvg),
|
||||
timeTLTG: Number(statObj.stats.timeTLTG),
|
||||
tkDestroysTG: Number(statObj.stats.tkDestroysTG),
|
||||
totalGames: Number(statObj.stats.totalGames),
|
||||
totalMATG: Number(statObj.stats.totalMATG),
|
||||
totalTimeTG: Number(statObj.stats.totalTimeTG),
|
||||
timeOnTeamZeroTG: Number(statObj.stats.timeOnTeamZeroTG),
|
||||
timeOnTeamOneTG: Number(statObj.stats.timeOnTeamOneTG),
|
||||
timeOnTeamTwoTG: Number(statObj.stats.timeOnTeamTwoTG),
|
||||
matchRunTimeTG: Number(statObj.stats.matchRunTimeTG),
|
||||
totalWepDmgTG: Number(statObj.stats.totalWepDmgTG),
|
||||
tripleChainKillTG: Number(statObj.stats.tripleChainKillTG),
|
||||
tripleKillTG: Number(statObj.stats.tripleKillTG),
|
||||
turretDestroysTG: Number(statObj.stats.turretDestroysTG),
|
||||
TurretIndoorDepTG: Number(statObj.stats.TurretIndoorDepTG),
|
||||
turretKillsTG: Number(statObj.stats.turretKillsTG),
|
||||
TurretOutdoorDepTG: Number(statObj.stats.TurretOutdoorDepTG),
|
||||
TurretRepairsTG: Number(statObj.stats.TurretRepairsTG),
|
||||
vehicleBonusTG: Number(statObj.stats.vehicleBonusTG),
|
||||
vehicleScoreTG: Number(statObj.stats.vehicleScoreTG),
|
||||
vehicleSpawnDeathsTG: Number(statObj.stats.vehicleSpawnDeathsTG),
|
||||
vehicleSpawnKillsTG: Number(statObj.stats.vehicleSpawnKillsTG),
|
||||
versionNum: Number(statObj.stats.versionNum),
|
||||
vstationDestroysTG: Number(statObj.stats.vstationDestroysTG),
|
||||
VStationRepairsTG: Number(statObj.stats.VStationRepairsTG),
|
||||
weaponHitDistMax: Number(statObj.stats.weaponHitDistMax),
|
||||
weaponScoreTG: Number(statObj.stats.weaponScoreTG),
|
||||
weekStamp: Number(statObj.stats.weekStamp),
|
||||
wildRDTG: Number(statObj.stats.wildRDTG),
|
||||
wildRKTG: Number(statObj.stats.wildRKTG),
|
||||
winCountTG: Number(statObj.stats.winCountTG),
|
||||
winLostPctAvg: Number(statObj.stats.winLostPctAvg),
|
||||
yearStamp: Number(statObj.stats.yearStamp),
|
||||
};
|
||||
}
|
||||
|
||||
export default formatPlayerStats;
|
||||
47
app/api/src/game/entities/GameDetail.ts
Normal file
47
app/api/src/game/entities/GameDetail.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
import { Games } from '../../games/entities/Games';
|
||||
import { Players } from '../../players/entities/Players';
|
||||
|
||||
@Index('games_pk', [ 'id' ], { unique: true })
|
||||
@Index('game_detail_uuid_key', [ 'uuid' ], { unique: true })
|
||||
@Entity('game_detail', { schema: 'public' })
|
||||
export class GameDetail {
|
||||
@PrimaryGeneratedColumn({ type: 'integer', name: 'id' })
|
||||
id: number;
|
||||
|
||||
@Column('text', { name: 'player_name' })
|
||||
playerName: string;
|
||||
|
||||
@Column('numeric', { name: 'stat_overwrite', select: false })
|
||||
statOverwrite: string;
|
||||
|
||||
@Column('text', { name: 'map' })
|
||||
map: string;
|
||||
|
||||
@Column('jsonb', { name: 'stats' })
|
||||
stats: any;
|
||||
|
||||
@Column('timestamp without time zone', { name: 'datestamp' })
|
||||
datestamp: Date;
|
||||
|
||||
@Column('text', { name: 'uuid', unique: true })
|
||||
uuid: string;
|
||||
|
||||
@Column('text', { name: 'gametype' })
|
||||
gametype: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
name: 'created_at',
|
||||
default: () => 'now()'
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@ManyToOne(() => Games, (games) => games.gameDetails)
|
||||
@JoinColumn([ { name: 'game_id', referencedColumnName: 'gameId' } ])
|
||||
game: Games;
|
||||
|
||||
@ManyToOne(() => Players, (players) => players.gameDetails)
|
||||
@JoinColumn([ { name: 'player_guid', referencedColumnName: 'playerGuid' } ])
|
||||
playerGuid: Players;
|
||||
}
|
||||
18
app/api/src/game/game.controller.spec.ts
Normal file
18
app/api/src/game/game.controller.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { GameController } from './game.controller';
|
||||
|
||||
describe('Game Controller', () => {
|
||||
let controller: GameController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [GameController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<GameController>(GameController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
16
app/api/src/game/game.controller.ts
Normal file
16
app/api/src/game/game.controller.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiOperation } from '@nestjs/swagger';
|
||||
|
||||
import { GameService } from './game.service';
|
||||
|
||||
@Controller('game')
|
||||
export class GameController {
|
||||
constructor(private readonly gameService: GameService) {}
|
||||
|
||||
// /games/:gameId
|
||||
@Get(':gameId')
|
||||
@ApiOperation({ tags: [ 'Game' ], summary: 'Find game by Id' })
|
||||
findOne(@Param('gameId') gameId: string) {
|
||||
return this.gameService.findOne(gameId);
|
||||
}
|
||||
}
|
||||
19
app/api/src/game/game.module.ts
Normal file
19
app/api/src/game/game.module.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { GameController } from './game.controller';
|
||||
import { GameService } from './game.service';
|
||||
|
||||
import { GameDetail } from './entities/GameDetail';
|
||||
import { Games } from '../games/entities/Games';
|
||||
|
||||
import { Players } from '../players/entities/Players';
|
||||
|
||||
@Module({
|
||||
imports: [ TypeOrmModule.forFeature([ Games, GameDetail, Players ]), ConfigModule ],
|
||||
controllers: [ GameController ],
|
||||
providers: [ GameService ],
|
||||
exports: [ GameService ]
|
||||
})
|
||||
export class GameModule {}
|
||||
18
app/api/src/game/game.service.spec.ts
Normal file
18
app/api/src/game/game.service.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { GameService } from './game.service';
|
||||
|
||||
describe('GameService', () => {
|
||||
let service: GameService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [GameService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<GameService>(GameService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
188
app/api/src/game/game.service.ts
Normal file
188
app/api/src/game/game.service.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Connection, Repository } from 'typeorm';
|
||||
|
||||
import { Games } from '../games/entities/Games';
|
||||
import { GameDetail } from './entities/GameDetail';
|
||||
|
||||
import formatPlayerStats from '../common/util/formatStats';
|
||||
|
||||
@Injectable()
|
||||
export class GameService {
|
||||
constructor(
|
||||
private readonly connection: Connection,
|
||||
private readonly configService: ConfigService,
|
||||
@InjectRepository(Games)
|
||||
private readonly gamesRepository: Repository<Games>,
|
||||
@InjectRepository(GameDetail)
|
||||
private readonly gameRepository: Repository<GameDetail>,
|
||||
) {}
|
||||
|
||||
async findOne(gameId: string) {
|
||||
const query = await this.gameRepository.find({
|
||||
relations: ['game', 'playerGuid'],
|
||||
where: [{ game: { gameId: gameId } }],
|
||||
});
|
||||
|
||||
if (!query.length) {
|
||||
throw new NotFoundException(`Game ID: ${gameId} not found`);
|
||||
}
|
||||
|
||||
const game: any = {
|
||||
...query[0].game,
|
||||
};
|
||||
|
||||
// Need to set return based off gameType
|
||||
// Modify game object if not a CTF type game and return early
|
||||
if (query[0].gametype !== 'CTFGame' && query[0].gametype !== 'SCtFGame') {
|
||||
game['players'] = [];
|
||||
for (const player of query) {
|
||||
const { playerName } = player;
|
||||
const stats = formatPlayerStats(player);
|
||||
|
||||
const p = {
|
||||
playerGuid: player.playerGuid.playerGuid,
|
||||
playerName,
|
||||
stats,
|
||||
};
|
||||
|
||||
game.players.push(p);
|
||||
}
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
// Team Based game stats (CTF/SCtF)
|
||||
game['teams'] = {
|
||||
obs: { score: 0, players: [] },
|
||||
storm: { score: 0, players: [] },
|
||||
inferno: { score: 0, players: [] },
|
||||
};
|
||||
|
||||
const teamZero = [],
|
||||
teamOne = [],
|
||||
teamTwo = [];
|
||||
|
||||
for (const player of query) {
|
||||
const { playerName } = player;
|
||||
const stats = formatPlayerStats(player);
|
||||
|
||||
const p = {
|
||||
playerGuid: player.playerGuid.playerGuid,
|
||||
playerName,
|
||||
stats,
|
||||
};
|
||||
|
||||
if (isNaN(player.stats.teamScoreGame)) {
|
||||
// legacy calculations for game totals (not using the new teamScoreGame attribute)
|
||||
const flagGrabsTG = parseInt(player.stats.flagGrabsTG[0]);
|
||||
const flagCapsTG = parseInt(player.stats.flagCapsTG[0]) * 100;
|
||||
const totalFlagScore = flagGrabsTG + flagCapsTG;
|
||||
|
||||
if (player.stats.dtTeamGame[0] === '1') {
|
||||
// Storm
|
||||
game.teams.storm.score += totalFlagScore;
|
||||
teamOne.push(p);
|
||||
} else if (player.stats.dtTeamGame[0] === '2') {
|
||||
// Inferno
|
||||
game.teams.inferno.score += totalFlagScore;
|
||||
teamTwo.push(p);
|
||||
} else {
|
||||
// OBS
|
||||
game.teams.obs.score += totalFlagScore;
|
||||
teamZero.push(p);
|
||||
}
|
||||
} else {
|
||||
// Use new player.stats.teamScoreGame key
|
||||
if (player.stats.dtTeamGame[0] === '1') {
|
||||
// Storm
|
||||
game.teams.storm.score = Number(player.stats.teamScoreGame);
|
||||
teamOne.push(p);
|
||||
} else if (player.stats.dtTeamGame[0] === '2') {
|
||||
// Inferno
|
||||
game.teams.inferno.score = Number(player.stats.teamScoreGame);
|
||||
teamTwo.push(p);
|
||||
} else {
|
||||
// OBS
|
||||
game.teams.obs.score = Number(player.stats.teamScoreGame);
|
||||
teamZero.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
game['teams']['obs']['players'] = teamZero.sort(
|
||||
(a, b) => b.stats.scoreTG - a.stats.scoreTG,
|
||||
);
|
||||
game['teams']['storm']['players'] = teamOne.sort(
|
||||
(a, b) => b.stats.scoreTG - a.stats.scoreTG,
|
||||
);
|
||||
game['teams']['inferno']['players'] = teamTwo.sort(
|
||||
(a, b) => b.stats.scoreTG - a.stats.scoreTG,
|
||||
);
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
async findOneAbvSummary(gameId: string) {
|
||||
const query = await this.gameRepository.find({
|
||||
relations: ['game', 'playerGuid'],
|
||||
where: [{ game: { gameId: gameId } }],
|
||||
});
|
||||
|
||||
if (!query.length) {
|
||||
throw new NotFoundException(`Game ID: ${gameId} not found`);
|
||||
}
|
||||
|
||||
const game: any = {
|
||||
...query[0].game,
|
||||
};
|
||||
|
||||
// Need to set return based off gameType
|
||||
// Modify game object if not a CTF type game and return early
|
||||
if (query[0].gametype !== 'CTFGame' && query[0].gametype !== 'SCtFGame') {
|
||||
game['totalScore'] = 0;
|
||||
|
||||
for (const player of query) {
|
||||
const stats = formatPlayerStats(player);
|
||||
|
||||
game.totalScore += stats.scoreTG;
|
||||
}
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
// Team Based game stats (CTF/SCtF)
|
||||
game['teams'] = {
|
||||
obs: { score: 0, playerCount: 0 },
|
||||
storm: { score: 0, playerCount: 0 },
|
||||
inferno: { score: 0, playerCount: 0 },
|
||||
};
|
||||
|
||||
// console.log(query);
|
||||
|
||||
for (const player of query) {
|
||||
const flagGrabsTG = parseInt(player.stats.flagGrabsTG[0]);
|
||||
const flagCapsTG = parseInt(player.stats.flagCapsTG[0]) * 100;
|
||||
const totalFlagScore = flagGrabsTG + flagCapsTG;
|
||||
|
||||
if (player.stats.dtTeamGame[0] === '1') {
|
||||
// Storm
|
||||
game.teams.storm.score += totalFlagScore;
|
||||
game.teams.storm.playerCount += 1;
|
||||
} else if (player.stats.dtTeamGame[0] === '2') {
|
||||
// Inferno
|
||||
game.teams.inferno.score += totalFlagScore;
|
||||
game.teams.inferno.playerCount += 1;
|
||||
} else {
|
||||
// OBS
|
||||
game.teams.obs.score += totalFlagScore;
|
||||
game.teams.obs.playerCount += 1;
|
||||
}
|
||||
}
|
||||
game['totalScore'] =
|
||||
game.teams.storm.score + game.teams.inferno.score + game.teams.obs.score;
|
||||
|
||||
return game;
|
||||
}
|
||||
}
|
||||
27
app/api/src/games/entities/Games.ts
Normal file
27
app/api/src/games/entities/Games.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Column, Entity, Index, OneToMany } from 'typeorm';
|
||||
import { GameDetail } from '../../game/entities/GameDetail';
|
||||
|
||||
@Index('game_pk', [ 'gameId' ], { unique: true })
|
||||
@Entity('games', { schema: 'public' })
|
||||
export class Games {
|
||||
@Column('numeric', { primary: true, name: 'game_id' })
|
||||
gameId: string;
|
||||
|
||||
@Column('text', { name: 'map' })
|
||||
map: string;
|
||||
|
||||
@Column('timestamp without time zone', { name: 'datestamp' })
|
||||
datestamp: Date;
|
||||
|
||||
@Column('text', { name: 'gametype' })
|
||||
gametype: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
name: 'created_at',
|
||||
default: () => 'now()'
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@OneToMany(() => GameDetail, (gameDetail) => gameDetail.game)
|
||||
gameDetails: GameDetail[];
|
||||
}
|
||||
18
app/api/src/games/games.controller.spec.ts
Normal file
18
app/api/src/games/games.controller.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { GamesController } from './games.controller';
|
||||
|
||||
describe('Games Controller', () => {
|
||||
let controller: GamesController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [GamesController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<GamesController>(GamesController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
44
app/api/src/games/games.controller.ts
Normal file
44
app/api/src/games/games.controller.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { ApiOperation } from '@nestjs/swagger';
|
||||
|
||||
import { GamesService } from './games.service';
|
||||
import { PaginationQueryDto } from '../common/dto/pagination-query.dto';
|
||||
|
||||
@Controller('games')
|
||||
export class GamesController {
|
||||
constructor(private readonly gamesService: GamesService) {}
|
||||
|
||||
// /games
|
||||
@Get()
|
||||
@ApiOperation({ tags: [ 'Game' ], summary: 'Return the latest games' })
|
||||
findAll(@Query() paginationQuery: PaginationQueryDto) {
|
||||
const { limit = 10, offset = 0 } = paginationQuery;
|
||||
return this.gamesService.findAll({ limit, offset });
|
||||
}
|
||||
|
||||
// /games/summary
|
||||
@Get('summary')
|
||||
@ApiOperation({ tags: [ 'Game' ], summary: 'Return the latest games with summary' })
|
||||
findAllWithSummary(@Query() paginationQuery: PaginationQueryDto) {
|
||||
const { limit = 10, offset = 0 } = paginationQuery;
|
||||
|
||||
return this.gamesService.findAllWithSummary({ limit, offset });
|
||||
}
|
||||
|
||||
// /games/gametype/:gametype
|
||||
@Get('gametype/:gametype')
|
||||
@ApiOperation({ tags: [ 'Game' ], summary: 'Return the latest games by game type' })
|
||||
findByType(@Param('gametype') gametype: string, @Query() paginationQuery: PaginationQueryDto) {
|
||||
const { limit = 10, offset = 0 } = paginationQuery;
|
||||
return this.gamesService.findByType(gametype, { limit, offset });
|
||||
}
|
||||
|
||||
// /games/gametype/CTFGame/summary
|
||||
@Get('gametype/:gametype/summary')
|
||||
@ApiOperation({ tags: [ 'Game' ], summary: 'Return the latest games by game type with game summaries' })
|
||||
findByTypeWithSummary(@Param('gametype') gametype: string, @Query() paginationQuery: PaginationQueryDto) {
|
||||
const { limit = 10, offset = 0 } = paginationQuery;
|
||||
|
||||
return this.gamesService.findByTypeWithSummary(gametype, { limit, offset });
|
||||
}
|
||||
}
|
||||
17
app/api/src/games/games.module.ts
Normal file
17
app/api/src/games/games.module.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { GamesController } from './games.controller';
|
||||
import { GamesService } from './games.service';
|
||||
|
||||
import { Games } from './entities/Games';
|
||||
import { GameDetail } from '../game/entities/GameDetail';
|
||||
import { GameModule } from '../game/game.module';
|
||||
|
||||
@Module({
|
||||
imports: [ TypeOrmModule.forFeature([ Games, GameDetail ]), ConfigModule, GameModule ],
|
||||
controllers: [ GamesController ],
|
||||
providers: [ GamesService ]
|
||||
})
|
||||
export class GamesModule {}
|
||||
18
app/api/src/games/games.service.spec.ts
Normal file
18
app/api/src/games/games.service.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { GamesService } from './games.service';
|
||||
|
||||
describe('GamesService', () => {
|
||||
let service: GamesService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [GamesService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<GamesService>(GamesService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
114
app/api/src/games/games.service.ts
Normal file
114
app/api/src/games/games.service.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Connection, Repository } from 'typeorm';
|
||||
|
||||
import { Games } from './entities/Games';
|
||||
import { GameService } from '../game/game.service';
|
||||
import { PaginationQueryDto } from '../common/dto/pagination-query.dto';
|
||||
|
||||
@Injectable()
|
||||
export class GamesService {
|
||||
constructor(
|
||||
private readonly connection: Connection,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly gameService: GameService,
|
||||
@InjectRepository(Games) private readonly gamesRepository: Repository<Games>
|
||||
) {}
|
||||
|
||||
async findAll(paginationQuery: PaginationQueryDto) {
|
||||
const { limit, offset } = paginationQuery;
|
||||
const returnMaxLimit = Math.min(300, Math.max(0, limit));
|
||||
|
||||
const games = await this.gamesRepository.find({
|
||||
skip: offset,
|
||||
take: returnMaxLimit,
|
||||
order: {
|
||||
datestamp: 'DESC'
|
||||
}
|
||||
});
|
||||
|
||||
const abvSummary = [];
|
||||
for (const game of games) {
|
||||
const summary = await this.gameService.findOneAbvSummary(game.gameId);
|
||||
abvSummary.push(summary);
|
||||
}
|
||||
|
||||
// Only return games when the score is greater than 1
|
||||
return abvSummary.filter((g) => g.totalScore > 1);
|
||||
}
|
||||
|
||||
async findAllWithSummary(paginationQuery: PaginationQueryDto) {
|
||||
const { limit, offset } = paginationQuery;
|
||||
|
||||
const returnMaxLimit = Math.min(100, Math.max(0, limit));
|
||||
|
||||
const games = await this.gamesRepository.find({
|
||||
skip: offset,
|
||||
take: returnMaxLimit,
|
||||
order: {
|
||||
datestamp: 'DESC'
|
||||
}
|
||||
});
|
||||
|
||||
const withSummary = [];
|
||||
for (const game of games) {
|
||||
const summary = await this.gameService.findOne(game.gameId);
|
||||
withSummary.push(summary);
|
||||
}
|
||||
|
||||
return withSummary;
|
||||
}
|
||||
|
||||
async findByType(gametype: string, paginationQuery: PaginationQueryDto) {
|
||||
const { limit, offset } = paginationQuery;
|
||||
const returnMaxLimit = Math.min(100, Math.max(0, limit));
|
||||
|
||||
const games = await this.gamesRepository.find({
|
||||
where: { gametype: gametype },
|
||||
skip: offset,
|
||||
take: returnMaxLimit,
|
||||
order: {
|
||||
datestamp: 'DESC'
|
||||
}
|
||||
});
|
||||
if (!games.length) {
|
||||
throw new NotFoundException(`Game Type: ${gametype} not found`);
|
||||
}
|
||||
|
||||
const abvSummary = [];
|
||||
for (const game of games) {
|
||||
const summary = await this.gameService.findOneAbvSummary(game.gameId);
|
||||
abvSummary.push(summary);
|
||||
}
|
||||
|
||||
// Only return games when the score is greater than 1
|
||||
return abvSummary.filter((g) => g.totalScore > 1);
|
||||
}
|
||||
|
||||
async findByTypeWithSummary(gametype: string, paginationQuery: PaginationQueryDto) {
|
||||
const { limit, offset } = paginationQuery;
|
||||
|
||||
const returnMaxLimit = Math.min(100, Math.max(0, limit));
|
||||
|
||||
const games = await this.gamesRepository.find({
|
||||
where: { gametype: gametype },
|
||||
skip: offset,
|
||||
take: returnMaxLimit,
|
||||
order: {
|
||||
datestamp: 'DESC'
|
||||
}
|
||||
});
|
||||
if (!games.length) {
|
||||
throw new NotFoundException(`Game Type: ${gametype} not found`);
|
||||
}
|
||||
|
||||
const withSummary = [];
|
||||
for (const game of games) {
|
||||
const summary = await this.gameService.findOne(game.gameId);
|
||||
withSummary.push(summary);
|
||||
}
|
||||
|
||||
return withSummary;
|
||||
}
|
||||
}
|
||||
45
app/api/src/main.ts
Normal file
45
app/api/src/main.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import * as fs from 'fs';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
// Use local gen'd cert in container
|
||||
const httpsOptions = {
|
||||
key: fs.readFileSync('/localcert/key.pem', 'utf8'),
|
||||
cert: fs.readFileSync('/localcert/cert.pem', 'utf8')
|
||||
};
|
||||
|
||||
const app = await NestFactory.create(AppModule, { httpsOptions });
|
||||
|
||||
app.enableCors({
|
||||
credentials: true
|
||||
});
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const swaggerOptions = new DocumentBuilder()
|
||||
.setTitle('Tribes 2 Stats API')
|
||||
.setDescription('Powering stats.playt2.com')
|
||||
.setVersion('1.0')
|
||||
.addTag('Game')
|
||||
.addTag('Player')
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, swaggerOptions);
|
||||
SwaggerModule.setup('docs', app, document);
|
||||
|
||||
await app.listen(8443);
|
||||
}
|
||||
bootstrap();
|
||||
18
app/api/src/player/player.controller.spec.ts
Normal file
18
app/api/src/player/player.controller.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PlayerController } from './player.controller';
|
||||
|
||||
describe('PlayerController', () => {
|
||||
let controller: PlayerController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PlayerController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<PlayerController>(PlayerController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
16
app/api/src/player/player.controller.ts
Normal file
16
app/api/src/player/player.controller.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiOperation } from '@nestjs/swagger';
|
||||
|
||||
import { PlayerService } from './player.service';
|
||||
|
||||
@Controller('player')
|
||||
export class PlayerController {
|
||||
constructor(private readonly playerService: PlayerService) {}
|
||||
|
||||
// /player/:playerGuid
|
||||
@Get(':playerGuid')
|
||||
@ApiOperation({ tags: [ 'Player' ], summary: 'Return player stats by guid' })
|
||||
findOne(@Param('playerGuid') playerGuid: string) {
|
||||
return this.playerService.findOne(playerGuid);
|
||||
}
|
||||
}
|
||||
16
app/api/src/player/player.module.ts
Normal file
16
app/api/src/player/player.module.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { PlayerController } from './player.controller';
|
||||
import { PlayerService } from './player.service';
|
||||
|
||||
import { Players } from '../players/entities/Players';
|
||||
import { GameDetail } from '../game/entities/GameDetail';
|
||||
|
||||
@Module({
|
||||
imports: [ TypeOrmModule.forFeature([ Players, GameDetail ]), ConfigModule ],
|
||||
controllers: [ PlayerController ],
|
||||
providers: [ PlayerService ]
|
||||
})
|
||||
export class PlayerModule {}
|
||||
18
app/api/src/player/player.service.spec.ts
Normal file
18
app/api/src/player/player.service.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PlayerService } from './player.service';
|
||||
|
||||
describe('PlayerService', () => {
|
||||
let service: PlayerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PlayerService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PlayerService>(PlayerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
92
app/api/src/player/player.service.ts
Normal file
92
app/api/src/player/player.service.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Connection, Repository } from 'typeorm';
|
||||
|
||||
import { Players } from '../players/entities/Players';
|
||||
import { GameDetail } from '../game/entities/GameDetail';
|
||||
|
||||
import formatPlayerStats from '../common/util/formatStats';
|
||||
|
||||
@Injectable()
|
||||
export class PlayerService {
|
||||
constructor(
|
||||
private readonly connection: Connection,
|
||||
private readonly configService: ConfigService,
|
||||
@InjectRepository(Players) private readonly playersRepository: Repository<Players>,
|
||||
@InjectRepository(GameDetail) private readonly gameRepository: Repository<GameDetail>
|
||||
) {}
|
||||
|
||||
async findOne(playerGuid: string) {
|
||||
const player = await this.playersRepository.findOne({
|
||||
relations: [ 'gameDetails' ],
|
||||
where: [ { playerGuid: playerGuid } ]
|
||||
});
|
||||
|
||||
if (!player) {
|
||||
throw new NotFoundException(`Player GUID: ${playerGuid} not found`);
|
||||
}
|
||||
|
||||
const gameDetails = [];
|
||||
|
||||
for (const game in player.gameDetails) {
|
||||
const g = player.gameDetails[game];
|
||||
|
||||
const stats = formatPlayerStats(g);
|
||||
|
||||
gameDetails.push({ ...g, stats });
|
||||
}
|
||||
|
||||
/* stat sum */
|
||||
// Dynamically generate and sum the statTotals object
|
||||
// This is dirty and should be cleaned up but will do for now :)
|
||||
const playerStatTotals = {},
|
||||
statKeys = Object.keys(gameDetails[0].stats);
|
||||
|
||||
for (let i = 0; i < statKeys.length; i++) {
|
||||
if (
|
||||
statKeys[i] === 'map' ||
|
||||
statKeys[i] === 'dateStamp' ||
|
||||
statKeys[i] === 'timeDayMonth' ||
|
||||
statKeys[i] === 'gameID' ||
|
||||
statKeys[i] === 'mapID'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
playerStatTotals[statKeys[i]] = 0;
|
||||
}
|
||||
|
||||
gameDetails.map((statLine) => {
|
||||
// look through each object in playerStatsData array
|
||||
for (const [ key, value ] of Object.entries(statLine.stats)) {
|
||||
// console.log(`${key}: ${value}`);
|
||||
// If the stat item exists, add it -- if not create a new key in playerStatTotals
|
||||
if (playerStatTotals.hasOwnProperty(key) === true) {
|
||||
playerStatTotals[key] = playerStatTotals[key] + Number(value);
|
||||
} else {
|
||||
playerStatTotals[key] = Number(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
/* end stat sum */
|
||||
|
||||
const formattedStats = {
|
||||
...player,
|
||||
totalGamesCtfgame: Number(player.totalGamesCtfgame),
|
||||
totalGamesDmgame: Number(player.totalGamesDmgame),
|
||||
totalGamesSctfgame: Number(player.totalGamesSctfgame),
|
||||
totalGamesLakrabbitgame: Number(player.totalGamesLakrabbitgame),
|
||||
totalGames:
|
||||
Number(player.totalGamesCtfgame) +
|
||||
Number(player.totalGamesDmgame) +
|
||||
Number(player.totalGamesSctfgame) +
|
||||
Number(player.totalGamesLakrabbitgame),
|
||||
gameDetails: gameDetails
|
||||
.filter((g) => g.stats.scoreTG > 0)
|
||||
.sort((a, b) => Number(b.stats.gameID) - Number(a.stats.gameID)),
|
||||
statTotals: playerStatTotals
|
||||
};
|
||||
|
||||
return formattedStats;
|
||||
}
|
||||
}
|
||||
63
app/api/src/players/entities/Players.ts
Normal file
63
app/api/src/players/entities/Players.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { GameDetail } from '../../game/entities/GameDetail';
|
||||
|
||||
@Index('players_pk', [ 'playerGuid' ], { unique: true })
|
||||
@Index('players_player_name_key', [ 'playerName' ], { unique: true })
|
||||
@Index('players_uuid_key', [ 'uuid' ], { unique: true })
|
||||
@Entity('players', { schema: 'public' })
|
||||
export class Players {
|
||||
// @PrimaryGeneratedColumn({ type: 'integer', name: 'id' })
|
||||
// id: number;
|
||||
|
||||
@Column('numeric', { primary: true, name: 'player_guid' })
|
||||
playerGuid: string;
|
||||
|
||||
@Column('text', { name: 'player_name', unique: true })
|
||||
playerName: string;
|
||||
|
||||
@Column('numeric', { name: 'total_games_ctfgame', default: () => '0' })
|
||||
totalGamesCtfgame: string;
|
||||
|
||||
@Column('numeric', { name: 'total_games_dmgame', default: () => '0' })
|
||||
totalGamesDmgame: string;
|
||||
|
||||
@Column('numeric', { name: 'total_games_lakrabbitgame', default: () => '0' })
|
||||
totalGamesLakrabbitgame: string;
|
||||
|
||||
@Column('numeric', { name: 'total_games_sctfgame', default: () => '0' })
|
||||
totalGamesSctfgame: string;
|
||||
|
||||
@Column('numeric', { name: 'stat_overwrite_ctfgame', select: false, default: () => '0' })
|
||||
statOverwriteCtfgame: string;
|
||||
|
||||
@Column('numeric', { name: 'stat_overwrite_dmgame', select: false, default: () => '0' })
|
||||
statOverwriteDmgame: string;
|
||||
|
||||
@Column('numeric', {
|
||||
name: 'stat_overwrite_lakrabbitgame',
|
||||
select: false,
|
||||
default: () => '0'
|
||||
})
|
||||
statOverwriteLakrabbitgame: string;
|
||||
|
||||
@Column('numeric', { name: 'stat_overwrite_sctfgame', select: false, default: () => '0' })
|
||||
statOverwriteSctfgame: string;
|
||||
|
||||
@Column('text', { name: 'uuid', unique: true })
|
||||
uuid: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
name: 'created_at',
|
||||
default: () => 'now()'
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
name: 'updated_at',
|
||||
default: () => 'now()'
|
||||
})
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToMany(() => GameDetail, (gameDetail) => gameDetail.playerGuid)
|
||||
gameDetails: GameDetail[];
|
||||
}
|
||||
18
app/api/src/players/players.controller.spec.ts
Normal file
18
app/api/src/players/players.controller.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PlayersController } from './players.controller';
|
||||
|
||||
describe('PlayersController', () => {
|
||||
let controller: PlayersController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PlayersController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<PlayersController>(PlayersController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
92
app/api/src/players/players.controller.ts
Normal file
92
app/api/src/players/players.controller.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { Controller, Get, Query, Param, Inject, CACHE_MANAGER } from '@nestjs/common';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { ApiOperation } from '@nestjs/swagger';
|
||||
|
||||
import { PlayersService } from './players.service';
|
||||
import { PaginationQueryDto } from '../common/dto/pagination-query.dto';
|
||||
import {
|
||||
TopAccuracyQueryDto,
|
||||
TopWinsQueryDto,
|
||||
} from '../common/dto/top-players-query.dto';
|
||||
import { cache } from 'joi';
|
||||
|
||||
@Controller('players')
|
||||
export class PlayersController {
|
||||
constructor(
|
||||
private readonly playerService: PlayersService,
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache
|
||||
) {}
|
||||
|
||||
// /players
|
||||
@Get()
|
||||
@ApiOperation({ tags: ['Player'], summary: 'Return a list of players' })
|
||||
findAll(@Query() paginationQuery: PaginationQueryDto) {
|
||||
const { limit = 10, offset = 0 } = paginationQuery;
|
||||
return this.playerService.findAll({ limit, offset });
|
||||
}
|
||||
|
||||
@Get('top/accuracy')
|
||||
@ApiOperation({
|
||||
tags: ['Player', 'Leaderboard'],
|
||||
summary: 'Return a leaderboard of players for a specific accuracy stat',
|
||||
})
|
||||
async findTopAccuracy(@Query() topPlayersQuery: TopAccuracyQueryDto) {
|
||||
const {
|
||||
stat,
|
||||
gameType,
|
||||
minGames = 10,
|
||||
minShots = 100,
|
||||
limit = 10,
|
||||
timePeriod,
|
||||
} = topPlayersQuery;
|
||||
|
||||
const cacheKey =`topacc_${stat}_${gameType}_${minGames}_${minShots}_${limit}_${timePeriod}`;
|
||||
const queryCache = await this.cacheManager.get(cacheKey);
|
||||
|
||||
if(!queryCache){
|
||||
const results = await this.playerService.findTopAccuracy({
|
||||
stat,
|
||||
gameType,
|
||||
minGames,
|
||||
minShots,
|
||||
limit,
|
||||
timePeriod,
|
||||
});
|
||||
|
||||
await this.cacheManager.set(cacheKey, results, { ttl: 3600 * 2 }); // 2 hours
|
||||
return results
|
||||
}
|
||||
|
||||
return queryCache
|
||||
}
|
||||
|
||||
|
||||
@Get('top/wins')
|
||||
@ApiOperation({
|
||||
tags: ['Player', 'Leaderboard'],
|
||||
summary: 'Return a leaderboard of players for win percentage',
|
||||
})
|
||||
async findTopWins(@Query() topPlayersQuery: TopWinsQueryDto) {
|
||||
const { minGames = 100, limit = 10, timePeriod } = topPlayersQuery;
|
||||
|
||||
const cacheKey =`topwins_${minGames}_${limit}_${timePeriod}`;
|
||||
const queryCache = await this.cacheManager.get(cacheKey);
|
||||
|
||||
/*
|
||||
If we don't have a cache ready, lets make one so the next hit is super fast
|
||||
Cache ttl is in seconds
|
||||
*/
|
||||
if(!queryCache){
|
||||
const results = await this.playerService.findTopWins({
|
||||
minGames,
|
||||
limit,
|
||||
timePeriod,
|
||||
});
|
||||
|
||||
await this.cacheManager.set(cacheKey, results, { ttl: 3600 * 2 }); // 2 hours
|
||||
return results
|
||||
}
|
||||
|
||||
return queryCache
|
||||
}
|
||||
}
|
||||
20
app/api/src/players/players.module.ts
Normal file
20
app/api/src/players/players.module.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Module, CacheModule } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { PlayersService } from './players.service';
|
||||
import { PlayersController } from './players.controller';
|
||||
|
||||
import { Players } from './entities/Players';
|
||||
import { GameDetail } from '../game/entities/GameDetail';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CacheModule.register(),
|
||||
TypeOrmModule.forFeature([Players, GameDetail]),
|
||||
ConfigModule
|
||||
],
|
||||
providers: [PlayersService],
|
||||
controllers: [PlayersController],
|
||||
})
|
||||
export class PlayersModule {}
|
||||
18
app/api/src/players/players.service.spec.ts
Normal file
18
app/api/src/players/players.service.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PlayersService } from './players.service';
|
||||
|
||||
describe('PlayersService', () => {
|
||||
let service: PlayersService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PlayersService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PlayersService>(PlayersService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
378
app/api/src/players/players.service.ts
Normal file
378
app/api/src/players/players.service.ts
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Connection, Repository } from 'typeorm';
|
||||
|
||||
import { Players } from './entities/Players';
|
||||
import { GameDetail } from '../game/entities/GameDetail';
|
||||
import { PaginationQueryDto } from '../common/dto/pagination-query.dto';
|
||||
import {
|
||||
TopAccuracyQueryDto,
|
||||
TopWinsQueryDto,
|
||||
} from '../common/dto/top-players-query.dto';
|
||||
|
||||
@Injectable()
|
||||
export class PlayersService {
|
||||
constructor(
|
||||
private readonly connection: Connection,
|
||||
private readonly configService: ConfigService,
|
||||
@InjectRepository(Players)
|
||||
private readonly playersRepository: Repository<Players>,
|
||||
@InjectRepository(GameDetail)
|
||||
private readonly gameRepository: Repository<GameDetail>,
|
||||
) {}
|
||||
|
||||
async findAll(paginationQuery: PaginationQueryDto) {
|
||||
const { limit, offset } = paginationQuery;
|
||||
|
||||
const returnMaxLimit = Math.min(500, Math.max(0, limit));
|
||||
|
||||
const players = await this.playersRepository.find({
|
||||
skip: offset,
|
||||
take: returnMaxLimit,
|
||||
order: {
|
||||
updatedAt: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
return players;
|
||||
}
|
||||
|
||||
async findOne(playerGuid: string) {
|
||||
const player = await this.playersRepository.findOne({
|
||||
relations: ['gameDetails'],
|
||||
where: [{ playerGuid: playerGuid }],
|
||||
});
|
||||
if (!player) {
|
||||
throw new NotFoundException(`Player GUID: ${playerGuid} not found`);
|
||||
}
|
||||
return player;
|
||||
}
|
||||
|
||||
async findTopAccuracy(topAccuracyQuery: TopAccuracyQueryDto) {
|
||||
const {
|
||||
stat: hitsStat,
|
||||
gameType,
|
||||
minGames,
|
||||
minShots,
|
||||
limit,
|
||||
timePeriod = null,
|
||||
} = topAccuracyQuery;
|
||||
|
||||
const shotsStat = {
|
||||
discDmgHitsTG: 'discShotsFiredTG',
|
||||
discHitsTG: 'discShotsFiredTG',
|
||||
discMATG: 'discShotsFiredTG',
|
||||
laserHitsTG: 'laserShotsFiredTG',
|
||||
laserMATG: 'laserShotsFiredTG',
|
||||
cgHitsTG: 'cgShotsFiredTG',
|
||||
shockHitsTG: 'shockShotsFiredTG',
|
||||
grenadeDmgHitsTG: 'grenadeShotsFiredTG',
|
||||
grenadeHitsTG: 'grenadeShotsFiredTG',
|
||||
grenadeMATG: 'grenadeShotsFiredTG',
|
||||
}[hitsStat];
|
||||
|
||||
// Possibly make this a query param at some point?
|
||||
const excludeDiscJumps = shotsStat === 'discShotsFiredTG';
|
||||
|
||||
const hitsValue = '(game.stats->:hitsStat->>0)::integer';
|
||||
const shotsValue = '(game.stats->:shotsStat->>0)::integer';
|
||||
const discJumpsValue =
|
||||
"COALESCE((game.stats->'discJumpTG'->>0)::integer, 0)";
|
||||
const killerDiscJumpsValue =
|
||||
"COALESCE((game.stats->'killerDiscJumpTG'->>0)::integer, 0)";
|
||||
|
||||
// pgSQL doesn't let you reference aliased selections you've made in the
|
||||
// same select statement, so unfortunately these computed JSON values are
|
||||
// repeated in a couple places. Using template strings here to easily repeat
|
||||
// the same expression instead of having multiple nested subqueries. Rest
|
||||
// assured, no user-supplied values are ever interpolated, those are all
|
||||
// parameterized instead.
|
||||
const aggregatedHits = `SUM(${hitsValue})::integer`;
|
||||
// Add sums of `discJumpTG` and `killerDiscJumpTG`.
|
||||
const aggregatedDiscJumps = `(SUM(${discJumpsValue})::integer + SUM(${killerDiscJumpsValue})::integer)`;
|
||||
|
||||
let aggregatedShots = `SUM(${shotsValue})::integer`;
|
||||
if (excludeDiscJumps) {
|
||||
// Since subtracting disc jumps could theoretically drop the shots count
|
||||
// to 0, clamp it to at least the number of hits or 1, otherwise it'd be
|
||||
// possible to divide by zero.
|
||||
aggregatedShots = `GREATEST(1, ${aggregatedHits}, ${aggregatedShots} - ${aggregatedDiscJumps})`;
|
||||
}
|
||||
|
||||
// Cast to float to avoid integer division truncating the result.
|
||||
const aggregatedAccuracy = `(${aggregatedHits}::float / ${aggregatedShots}::float)`;
|
||||
|
||||
const sinceDate = '(now() - (:timePeriod)::interval)';
|
||||
|
||||
// TODO: This whole query could probably be turned into a `ViewEntity` at
|
||||
// some point, but I couldn't get that to work.
|
||||
|
||||
let playersQuery = this.playersRepository
|
||||
.createQueryBuilder('player')
|
||||
.setParameters({
|
||||
hitsStat,
|
||||
shotsStat,
|
||||
minGames,
|
||||
minShots,
|
||||
timePeriod,
|
||||
})
|
||||
.select([
|
||||
'player.player_guid',
|
||||
'player.player_name',
|
||||
'stats.game_count',
|
||||
'stats.hits',
|
||||
'stats.shots',
|
||||
'stats.accuracy',
|
||||
])
|
||||
.addSelect(
|
||||
timePeriod ? sinceDate : 'NULL',
|
||||
'since_date'
|
||||
)
|
||||
.innerJoin(
|
||||
(subQuery) => {
|
||||
let statsQuery = subQuery
|
||||
.select(['game.player_guid'])
|
||||
.from(GameDetail, 'game')
|
||||
.addSelect('COUNT(game.id)::integer', 'game_count')
|
||||
.addSelect(aggregatedHits, 'hits')
|
||||
.addSelect(aggregatedShots, 'shots')
|
||||
.addSelect(aggregatedAccuracy, 'accuracy')
|
||||
.where(`${shotsValue} > 0`)
|
||||
.andWhere(timePeriod ? `game.datestamp >= ${sinceDate}` : 'TRUE')
|
||||
.groupBy('game.player_guid');
|
||||
|
||||
if (excludeDiscJumps) {
|
||||
statsQuery = statsQuery.addSelect(
|
||||
aggregatedDiscJumps,
|
||||
'disc_jumps',
|
||||
);
|
||||
}
|
||||
|
||||
if (gameType) {
|
||||
statsQuery = statsQuery.andWhere('game.gametype = :gameType', {
|
||||
gameType,
|
||||
});
|
||||
}
|
||||
|
||||
return statsQuery;
|
||||
},
|
||||
'stats',
|
||||
'stats.player_guid = player.player_guid',
|
||||
)
|
||||
.where('stats.game_count >= :minGames')
|
||||
.andWhere('stats.shots >= :minShots')
|
||||
.orderBy('stats.accuracy', 'DESC')
|
||||
.limit(limit);
|
||||
|
||||
if (excludeDiscJumps) {
|
||||
playersQuery = playersQuery.addSelect('stats.disc_jumps');
|
||||
}
|
||||
|
||||
// Uncomment to debug:
|
||||
// console.log(query.getQueryAndParameters());
|
||||
|
||||
// typeorm doesn't let you select computed columns since they're not part
|
||||
// of the entity definition. There are workarounds, but I'm not a fan of any
|
||||
// and would rather use `getRawMany()` for now, which does include them.
|
||||
// See: https://github.com/typeorm/typeorm/issues/296
|
||||
const rows = await playersQuery.getRawMany();
|
||||
|
||||
// `getRawMany` was used, so manually snake_case -> camelCase.
|
||||
const players = rows.map((row) => ({
|
||||
playerGuid: row.player_guid,
|
||||
playerName: row.player_name,
|
||||
gameCount: row.game_count,
|
||||
hits: row.hits,
|
||||
shots: row.shots,
|
||||
discJumps: row.disc_jumps,
|
||||
accuracy: row.accuracy,
|
||||
}));
|
||||
|
||||
return {
|
||||
// Even though some of these parameters might have been supplied as input,
|
||||
// it's still useful to know what values were actually used, in case
|
||||
// defaults were used instead, values were clamped to min/max, etc.
|
||||
hitsStat,
|
||||
shotsStat,
|
||||
excludeDiscJumps,
|
||||
minGames,
|
||||
minShots,
|
||||
limit,
|
||||
timePeriod,
|
||||
sinceDate: rows.length ? rows[0].since_date : null,
|
||||
players,
|
||||
};
|
||||
}
|
||||
|
||||
async findTopWins(topWinsQuery: TopWinsQueryDto) {
|
||||
const { minGames, limit, timePeriod = null } = topWinsQuery;
|
||||
|
||||
const sinceDate = '(now() - (:timePeriod)::interval)';
|
||||
|
||||
const query = this.playersRepository
|
||||
.createQueryBuilder('player')
|
||||
.setParameters({ minGames, timePeriod })
|
||||
.select(['stats.player_name', 'stats.player_guid'])
|
||||
.addSelect('COUNT(stats.game_id)::integer', 'game_count')
|
||||
.addSelect(
|
||||
"COUNT(stats.player_match_result = 'win' OR NULL)::integer",
|
||||
'win_count',
|
||||
)
|
||||
.addSelect(
|
||||
"COUNT(stats.player_match_result = 'loss' OR NULL)::integer",
|
||||
'loss_count',
|
||||
)
|
||||
.addSelect(
|
||||
"COUNT(stats.player_match_result = 'draw' OR NULL)::integer",
|
||||
'draw_count',
|
||||
)
|
||||
.addSelect(
|
||||
"(COUNT(stats.player_match_result = 'win' OR NULL)::float + COUNT(stats.player_match_result = 'draw' OR NULL)::float / 2.0) / COUNT(stats.game_id)::float",
|
||||
'win_percent',
|
||||
)
|
||||
.addSelect(
|
||||
timePeriod ? sinceDate : 'NULL',
|
||||
'since_date'
|
||||
)
|
||||
.innerJoin(
|
||||
(qb) => {
|
||||
return (
|
||||
qb
|
||||
.select([
|
||||
'game.player_name',
|
||||
'game.player_guid',
|
||||
'game.map',
|
||||
'game.datestamp',
|
||||
'join_g.*',
|
||||
])
|
||||
// Determine whether they spent at least 67% of the total match time on
|
||||
// one team, and then determine whether that means they won or lost.
|
||||
// Note that this team may be different from `dtTeamGame`.
|
||||
.addSelect(
|
||||
`CASE
|
||||
WHEN
|
||||
((game.stats->'timeOnTeamOneTG'->>0)::float / (game.stats->'matchRunTimeTG'->>0)::float) >= 0.67
|
||||
THEN CASE
|
||||
WHEN
|
||||
join_g.score_storm > join_g.score_inferno
|
||||
THEN 'win'
|
||||
WHEN
|
||||
join_g.score_storm < join_g.score_inferno
|
||||
THEN 'loss'
|
||||
WHEN
|
||||
join_g.score_storm = join_g.score_inferno
|
||||
THEN 'draw'
|
||||
END
|
||||
WHEN
|
||||
((game.stats->'timeOnTeamTwoTG'->>0)::float / (game.stats->'matchRunTimeTG'->>0)::float) >= 0.67
|
||||
THEN CASE
|
||||
WHEN
|
||||
join_g.score_inferno > join_g.score_storm
|
||||
THEN 'win'
|
||||
WHEN
|
||||
join_g.score_inferno < join_g.score_storm
|
||||
THEN 'loss'
|
||||
WHEN
|
||||
join_g.score_inferno = join_g.score_storm
|
||||
THEN 'draw'
|
||||
END
|
||||
ELSE 'none'
|
||||
END`.replace(/\s+/g, ' '),
|
||||
'player_match_result',
|
||||
)
|
||||
.from(GameDetail, 'game')
|
||||
.innerJoin(
|
||||
(qb) => {
|
||||
return (
|
||||
qb
|
||||
.select(['g.game_id'])
|
||||
.addSelect(
|
||||
"COUNT(CASE WHEN (g.stats->'dtTeamGame'->>0)::integer = 1 AND (g.stats->'timeOnTeamOneTG'->>0)::float > 0 THEN 1 ELSE NULL END)::integer",
|
||||
'team_size_storm',
|
||||
)
|
||||
.addSelect(
|
||||
"COUNT(CASE WHEN (g.stats->'dtTeamGame'->>0)::integer = 2 AND (g.stats->'timeOnTeamTwoTG'->>0)::float > 0 THEN 1 ELSE NULL END)::integer",
|
||||
'team_size_inferno',
|
||||
)
|
||||
// `teamScoreGame` can get screwed up: players on one team
|
||||
// can be assigned the score from the other team (most
|
||||
// likely due to team switching). So to determine each
|
||||
// team's score, we take the most common `teamScoreGame`
|
||||
// from all players on that team who don't have a `timeOnTeam`
|
||||
// value of 0.
|
||||
.addSelect(
|
||||
"(mode() WITHIN GROUP (ORDER BY CASE WHEN ((g.stats->'dtTeamGame'->>0)::integer = 1 AND (g.stats->'timeOnTeamOneTG'->>0)::float > 0) THEN (g.stats->'teamScoreGame'->>0)::integer ELSE NULL END)) / 100",
|
||||
'score_storm',
|
||||
)
|
||||
.addSelect(
|
||||
"(mode() WITHIN GROUP (ORDER BY CASE WHEN ((g.stats->'dtTeamGame'->>0)::integer = 2 AND (g.stats->'timeOnTeamTwoTG'->>0)::float > 0) THEN (g.stats->'teamScoreGame'->>0)::integer ELSE NULL END)) / 100",
|
||||
'score_inferno',
|
||||
)
|
||||
.from(GameDetail, 'g')
|
||||
.groupBy('g.game_id')
|
||||
);
|
||||
},
|
||||
'join_g',
|
||||
'join_g.game_id = game.game_id',
|
||||
)
|
||||
.where("(game.gametype = 'CTFGame' OR game.gametype = 'SCtFGame')")
|
||||
// Only count if the player's `gamePCT` was at least 67%. This is
|
||||
// effectively how much of the match they were present for.
|
||||
.andWhere("(game.stats->'gamePCT'->>0)::float >= 67")
|
||||
// As an extra precaution against prematurely ended matches, only count
|
||||
// games that lasted at least 10 minutes.
|
||||
.andWhere("(game.stats->'matchRunTimeTG'->>0)::float >= 10")
|
||||
// Each team must have at least 2 players.
|
||||
.andWhere('join_g.team_size_storm >= 2')
|
||||
.andWhere('join_g.team_size_inferno >= 2')
|
||||
// Must fall within the specified time period.
|
||||
.andWhere(timePeriod ? `game.datestamp >= ${sinceDate}` : 'TRUE')
|
||||
);
|
||||
},
|
||||
'stats',
|
||||
'stats.player_guid = player.player_guid',
|
||||
)
|
||||
.where("stats.player_match_result != 'none'")
|
||||
.having('COUNT(stats.game_id)::integer >= :minGames')
|
||||
.groupBy('stats.player_guid')
|
||||
.addGroupBy('stats.player_name')
|
||||
.orderBy(
|
||||
"(COUNT(stats.player_match_result = 'win' OR NULL)::float + COUNT(stats.player_match_result = 'draw' OR NULL)::float / 2.0) / COUNT(stats.game_id)::float",
|
||||
'DESC',
|
||||
)
|
||||
.limit(limit);
|
||||
|
||||
// Uncomment to debug:
|
||||
// console.log(query.getQueryAndParameters());
|
||||
|
||||
// typeorm doesn't let you select computed columns since they're not part
|
||||
// of the entity definition. There are workarounds, but I'm not a fan of any
|
||||
// and would rather use `getRawMany()` for now, which does include them.
|
||||
// See: https://github.com/typeorm/typeorm/issues/296
|
||||
const rows = await query.getRawMany();
|
||||
|
||||
// `getRawMany` was used, so manually snake_case -> camelCase.
|
||||
const players = rows.map((row) => ({
|
||||
playerGuid: row.player_guid,
|
||||
playerName: row.player_name,
|
||||
gameCount: row.game_count,
|
||||
winCount: row.win_count,
|
||||
lossCount: row.loss_count,
|
||||
drawCount: row.draw_count,
|
||||
winPercent: row.win_percent,
|
||||
}));
|
||||
|
||||
return {
|
||||
// Even though some of these parameters might have been supplied as input,
|
||||
// it's still useful to know what values were actually used, in case
|
||||
// defaults were used instead, values were clamped to min/max, etc.
|
||||
minGames,
|
||||
gameType: ['CTFGame', 'SCtFGame'],
|
||||
limit,
|
||||
timePeriod,
|
||||
sinceDate: rows.length ? rows[0].since_date : null,
|
||||
players,
|
||||
};
|
||||
}
|
||||
}
|
||||
21
app/api/test/app.e2e-spec.ts
Normal file
21
app/api/test/app.e2e-spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [ AppModule ]
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer()).get('/').expect(200).expect('Healthy!');
|
||||
});
|
||||
});
|
||||
9
app/api/test/jest-e2e.json
Normal file
9
app/api/test/jest-e2e.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
9
app/api/tsconfig.build.json
Normal file
9
app/api/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"test",
|
||||
"dist",
|
||||
"**/*spec.ts"
|
||||
]
|
||||
}
|
||||
15
app/api/tsconfig.json
Normal file
15
app/api/tsconfig.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//ShellToUse is...
|
||||
|
|
@ -24,12 +25,22 @@ func Shellout(command string) (error, string, string) {
|
|||
func initFTP() {
|
||||
|
||||
ftpHOST := os.Getenv("FTP_HOST")
|
||||
ftpPath := os.Getenv("FTP_PATH")
|
||||
ftpUSER := os.Getenv("FTP_USER")
|
||||
ftpPW := os.Getenv("FTP_PW")
|
||||
ftpPW := strings.Replace(os.Getenv("FTP_PW"), `\\`, `\`, -1)
|
||||
|
||||
fmt.Println("Downloading stat files from", ftpHOST + ftpPath)
|
||||
|
||||
// wget is a bit buggy with FTP in v1.20.x
|
||||
// cmd := "wget --recursive --no-passive-ftp -nH --cut-dirs=4 --ftp-user=" + ftpUSER + " --no-parent --ftp-password="+ ftpPW +" -P /app/serverStats/stats/ ftp://" + ftpHOST + ftpPath
|
||||
|
||||
cmd := "lftp -c 'open "+ftpHOST+"; user "+ftpUSER+" "+ftpPW+"; mirror -e "+ftpPath+" /app/serverStats/stats/; quit'"
|
||||
|
||||
|
||||
|
||||
err, out, errout := Shellout(cmd)
|
||||
|
||||
fmt.Println("Downloading stat files from", ftpHOST)
|
||||
|
||||
err, out, errout := Shellout("wget --recursive -nH --cut-dirs=4 --user=" + ftpUSER + " --no-parent --password=" + ftpPW + " -P /app/serverStats/stats/ ftp://" + ftpHOST + "/" + ftpHOST + "_port_28000/classic/serverStats/stats/")
|
||||
if err != nil {
|
||||
log.Printf("error: %v\n", err)
|
||||
}
|
||||
|
|
@ -39,3 +50,5 @@ func initFTP() {
|
|||
fmt.Println(errout)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("Starting FTP stat file download")
|
||||
// fmt.Println("Starting FTP stat file download")
|
||||
initFTP()
|
||||
fmt.Println("Stat files downloaded!")
|
||||
// fmt.Println("Stat files downloaded!")
|
||||
|
||||
fmt.Println("Starting stat parser")
|
||||
initParser()
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ echo "T2 Stat Parser Running...";
|
|||
# Run this initially, then we'll execute this on a schedule
|
||||
./start.sh
|
||||
|
||||
echo "T2 Stat Parser Done!";
|
||||
# Keep container running with the cron schedule
|
||||
crond -l 2 -f
|
||||
#crond -l 2 -f
|
||||
|
|
@ -32,8 +32,8 @@ import (
|
|||
var (
|
||||
connectionString = flag.String("conn", getenvWithDefault("DATABASE_URL", ""), "PostgreSQL connection string")
|
||||
db *sqlx.DB
|
||||
maxStatOverwrite int = 100
|
||||
debugLevel int = 1 // 0 off,min | 1 Basic output checks | 2 Output all the things
|
||||
maxStatOverwrite int = 98 // there are 99 entries, remember arr counts 0
|
||||
debugLevel int = 1 // 0 off,min | 1 Basic output checks | 2 Output all the things
|
||||
)
|
||||
|
||||
func getenvWithDefault(name, defaultValue string) string {
|
||||
|
|
@ -59,12 +59,12 @@ type Game struct {
|
|||
dbStatOverWrite int `db.players:"stat_overwrite"`
|
||||
statOverWrite int
|
||||
|
||||
gameMap string `db.games:"map"`
|
||||
gameID int `db.games:"game_id"`
|
||||
gameType string `db.games:"gametype"`
|
||||
dateStamp string `db.games:"datestamp"`
|
||||
stats string `db.games:"stats"`
|
||||
uuid string `db.games:"uuid"`
|
||||
gameMap string `db.game_detail:"map"`
|
||||
gameID int `db.game_detail:"game_id"`
|
||||
gameType string `db.game_detail:"gametype"`
|
||||
dateStamp string `db.game_detail:"datestamp"`
|
||||
stats string `db.game_detail:"stats"`
|
||||
uuid string `db.game_detail:"uuid"`
|
||||
}
|
||||
|
||||
func initParser() {
|
||||
|
|
@ -164,6 +164,9 @@ func parseGameTypeStats(gt string) {
|
|||
break
|
||||
}
|
||||
|
||||
// Base the maxGamesLength off how many elements are present in the map length since this should always be there
|
||||
statArrayMaxLength := len(mStatLine["map"])
|
||||
|
||||
checkPlayer(g)
|
||||
g.dbStatOverWrite = getDBStatOverWrite(g.playerGUID, strings.ToLower(gt))
|
||||
|
||||
|
|
@ -176,32 +179,39 @@ func parseGameTypeStats(gt string) {
|
|||
fmt.Println("Stat Overwrite", g.statOverWrite)
|
||||
fmt.Println("maxStatOverwrite", maxStatOverwrite)
|
||||
|
||||
fmt.Println("statArrayMaxLength", statArrayMaxLength)
|
||||
|
||||
fmt.Println("g.dbStatOverWrite", g.dbStatOverWrite)
|
||||
}
|
||||
|
||||
statCron := 0
|
||||
if g.statOverWrite < g.dbStatOverWrite {
|
||||
// 100 -
|
||||
statCron = (maxStatOverwrite - g.statOverWrite) + g.dbStatOverWrite
|
||||
} else {
|
||||
statCron = g.statOverWrite - g.dbStatOverWrite
|
||||
}
|
||||
// statEntryDiff := 0
|
||||
// if g.statOverWrite < g.dbStatOverWrite {
|
||||
// // 100 -
|
||||
// statEntryDiff = (maxStatOverwrite - g.statOverWrite) + g.dbStatOverWrite
|
||||
// } else {
|
||||
// statEntryDiff = g.statOverWrite - g.dbStatOverWrite
|
||||
// }
|
||||
|
||||
// Reset statCron if it flows over maxStatOverwrite
|
||||
if statCron > maxStatOverwrite {
|
||||
statCron = 0
|
||||
}
|
||||
// // Reset statEntryDiff if it flows over maxStatOverwrite
|
||||
// if statEntryDiff > maxStatOverwrite {
|
||||
// statEntryDiff = 0
|
||||
// }
|
||||
|
||||
if debugLevel >= 1 {
|
||||
fmt.Println("statCron", statCron)
|
||||
}
|
||||
// if debugLevel >= 1 {
|
||||
// fmt.Println("statEntryDiff", statEntryDiff)
|
||||
// }
|
||||
|
||||
for i := 0; i <= statCron; i++ {
|
||||
// arrPosition := i - 1
|
||||
// fmt.Println(arrPosition)
|
||||
for i := 0; i < statArrayMaxLength; i++ {
|
||||
fmt.Println("index", i)
|
||||
parseStatOverWriteLine(g, mStatLine, i, strings.ToLower(gt))
|
||||
//g.dbStatOverWrite = getDBStatOverWrite(g.playerGUID, strings.ToLower(gt))
|
||||
}
|
||||
|
||||
// account for new players and new statlines
|
||||
// if g.statOverWrite == 0 && g.dbStatOverWrite == 0 {
|
||||
// parseStatOverWriteLine(g, mStatLine, 0, strings.ToLower(gt))
|
||||
// }
|
||||
|
||||
fmt.Println("---")
|
||||
}
|
||||
|
||||
|
|
@ -220,8 +230,13 @@ func parseStatOverWriteLine(g Game, mStatLine map[string][]string, arrPosition i
|
|||
|
||||
cleanStatLine := make(map[string][]string)
|
||||
for index, element := range mStatLine {
|
||||
if len(element) > 1 {
|
||||
//fmt.Println("index", index, " - arrPosition", arrPosition, " - element Length", len(element))
|
||||
if len(element) > 1 && arrPosition < len(element) {
|
||||
//fmt.Println("index", index, " - arrPosition", arrPosition, " - element Length", len(element))
|
||||
cleanStatLine[index] = append(cleanStatLine[index], element[arrPosition])
|
||||
} else {
|
||||
//fmt.Println(index, "prefilling this index since there isnt any data in this position...")
|
||||
cleanStatLine[index] = append(cleanStatLine[index], "0")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -250,8 +265,15 @@ func parseStatOverWriteLine(g Game, mStatLine map[string][]string, arrPosition i
|
|||
fmt.Println(g)
|
||||
}
|
||||
|
||||
// insert game stat
|
||||
addPlayerGameStat(g, strings.ToLower(gt))
|
||||
// Check if we need to create a new game record
|
||||
checkGameRecord(g)
|
||||
|
||||
if checkGameEntryForPlayer(g) == false && g.gameID != 0 {
|
||||
fmt.Println("does not exist, add")
|
||||
// insert game stat
|
||||
addPlayerGameStat(g, strings.ToLower(gt))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func rowExists(query string, args ...interface{}) bool {
|
||||
|
|
@ -264,6 +286,28 @@ func rowExists(query string, args ...interface{}) bool {
|
|||
return exists
|
||||
}
|
||||
|
||||
func checkGameRecord(g Game) {
|
||||
check := rowExists("select game_id from games where game_id = $1 and map = $2", g.gameID, g.gameMap)
|
||||
if !check {
|
||||
createGame(g)
|
||||
} else {
|
||||
fmt.Println("Game Record ", g.gameID, g.gameMap, " already exists")
|
||||
}
|
||||
}
|
||||
func createGame(g Game) {
|
||||
fmt.Println("Creating new Game ", g.gameMap, g.gameID, g.dateStamp, g.gameType)
|
||||
|
||||
if g.gameID != 0 {
|
||||
sqlInsert := `insert into games(map, game_id, datestamp, gametype) values($1,$2,$3,$4)`
|
||||
_, err := db.Exec(sqlInsert, g.gameMap, g.gameID, g.dateStamp, g.gameType)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to create new game record (Possible Dupe ID): %v\n", err)
|
||||
// Don't exit - just skip this insert
|
||||
// os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkPlayer(g Game) {
|
||||
check := rowExists("select player_guid from players where player_guid = $1", g.playerGUID)
|
||||
if !check {
|
||||
|
|
@ -273,6 +317,16 @@ func checkPlayer(g Game) {
|
|||
}
|
||||
}
|
||||
|
||||
func checkGameEntryForPlayer(g Game) bool {
|
||||
check := rowExists("select player_guid from game_detail where player_guid = $1 and game_id = $2 and map = $3", g.playerGUID, g.gameID, g.gameMap)
|
||||
if !check {
|
||||
return false
|
||||
} else {
|
||||
fmt.Println("Game Entry ", g.gameID, g.gameMap, " already exists for ", g.playerName)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func createPlayer(uuid string, g Game) {
|
||||
fmt.Println("Creating new player", g.playerName)
|
||||
_, err := db.Exec("insert into players(uuid, player_guid, player_name) values($1,$2,$3)", uuid, g.playerGUID, g.playerName)
|
||||
|
|
@ -293,16 +347,26 @@ func getDBStatOverWrite(playerGUID int, gt string) int {
|
|||
return dbStatOverWrite
|
||||
}
|
||||
|
||||
func resetDBStatOverWrite(playerGUID int, gt string) {
|
||||
sqlUpdate := `UPDATE players SET stat_overwrite_` + gt + `= 1 WHERE player_guid = $1;`
|
||||
_, err := db.Exec(sqlUpdate, playerGUID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("Reset player", playerGUID, "for GameType", gt)
|
||||
}
|
||||
|
||||
func addPlayerGameStat(g Game, gt string) {
|
||||
|
||||
if debugLevel == 1 {
|
||||
fmt.Println("g.dbStatOverWrite", g.dbStatOverWrite, "g.statOverWrite", g.statOverWrite)
|
||||
fmt.Println("Checking line: ", g.playerGUID, g.playerName, g.statOverWrite, g.gameMap, g.gameID, g.dateStamp, g.gameType)
|
||||
}
|
||||
|
||||
if g.dbStatOverWrite != g.statOverWrite && g.dateStamp != "0" {
|
||||
if g.dateStamp != "0" {
|
||||
// Insert new stat line
|
||||
fmt.Println("New stat line!", g.playerName, g.dateStamp)
|
||||
sqlInsert := `insert into games(player_guid, player_name, stat_overwrite, map, game_id, stats, datestamp, uuid, gametype) values($1,$2,$3,$4,$5,$6,$7,$8,$9)`
|
||||
sqlInsert := `insert into game_detail(player_guid, player_name, stat_overwrite, map, game_id, stats, datestamp, uuid, gametype) values($1,$2,$3,$4,$5,$6,$7,$8,$9)`
|
||||
_, err := db.Exec(sqlInsert, g.playerGUID, g.playerName, g.statOverWrite, g.gameMap, g.gameID, g.stats, g.dateStamp, g.uuid, g.gameType)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to add player's game stat: %v\n", err)
|
||||
|
|
@ -316,6 +380,9 @@ func addPlayerGameStat(g Game, gt string) {
|
|||
panic(err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("This stat line already exists")
|
||||
fmt.Println("The dateStamp looks malformed")
|
||||
}
|
||||
// else {
|
||||
// fmt.Println("This stat line already exists", g.playerGUID, g.playerName, g.statOverWrite, g.gameMap, g.gameID, g.dateStamp, g.gameType)
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
7
app/t2-stat-parser/serverStats/dir.txt
Normal file
7
app/t2-stat-parser/serverStats/dir.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
./lData
|
||||
./mlData
|
||||
|
||||
./stats/CTFGame
|
||||
./stats/DMGame
|
||||
./stats/LakRabbitGame
|
||||
./stats/SCTF/Game
|
||||
2
build/api/.dockerignore
Normal file
2
build/api/.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.git
|
||||
node_modules
|
||||
71
build/api/Dockerfile
Normal file
71
build/api/Dockerfile
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# [ Stage 1 ] - install node modules
|
||||
FROM node:12.18-alpine as builder
|
||||
|
||||
WORKDIR /build/app
|
||||
|
||||
RUN npm i -g @nestjs/cli@7.4.1
|
||||
|
||||
COPY ./app/api .
|
||||
RUN npm install
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN nest build
|
||||
|
||||
|
||||
|
||||
# ============================
|
||||
# ============================
|
||||
|
||||
# [ Stage 2 ]
|
||||
FROM node:12.18-alpine
|
||||
LABEL maintainer="Anthony Mineo <anthonymineo@gmail.com>"
|
||||
|
||||
RUN apk update && apk add --no-cache openssl bash curl
|
||||
|
||||
# Default envs as prod
|
||||
ENV HOST=0.0.0.0 \
|
||||
PORT=8443 \
|
||||
NODE_ENV=production \
|
||||
APP_NAME=NestJS
|
||||
|
||||
ENV APP_URL=https://${HOST}:${PORT}
|
||||
|
||||
|
||||
# Setup pm2 as our node process manager
|
||||
# https://pm2.keymetrics.io/docs/usage/docker-pm2-nodejs/
|
||||
RUN npm i -g pm2 @nestjs/cli@7.4.1
|
||||
|
||||
RUN mkdir /opt/node_app && chown node:node /opt/node_app && mkdir /localcert && chown node:node /localcert
|
||||
RUN mkdir -p /opt/node_app/app/node_modules/ && chown node:node /opt/node_app/app/node_modules/
|
||||
|
||||
WORKDIR /opt/node_app
|
||||
|
||||
#USER node
|
||||
|
||||
# Generate a localhost cert
|
||||
RUN openssl req -newkey rsa:2048 -new -nodes -x509 -days 360 -keyout /localcert/key.pem -out /localcert/cert.pem -subj "/C=US/ST=New Jersey/L=Warren/O=localhost/OU=IT/CN=127.0.0.1"
|
||||
|
||||
|
||||
ENV PATH /opt/node_app/node_modules/.bin:$PATH
|
||||
|
||||
# Our App
|
||||
WORKDIR /opt/node_app/app
|
||||
|
||||
# Set node modules outside our app to keep it clean
|
||||
COPY --from=builder /build/app/node_modules/ /opt/node_app/node_modules/
|
||||
|
||||
COPY --from=builder /build/app/dist/ /opt/node_app/app/dist/
|
||||
|
||||
COPY ./app/api/package.json ./app/api/package-lock.json* ./
|
||||
|
||||
|
||||
# Start script and config
|
||||
COPY ./build/api/ecosystem._PROD_.config.js /opt/node_app/ecosystem._PROD_.config.js
|
||||
COPY ./build/api/entrypoint.sh /entrypoint.sh
|
||||
|
||||
EXPOSE 8080 8443
|
||||
|
||||
HEALTHCHECK --interval=20s --timeout=30s --start-period=5s --retries=5 \
|
||||
CMD curl -f -k https://localhost:8443/ || exit 1
|
||||
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
14
build/api/ecosystem._DEV_.config.js
Normal file
14
build/api/ecosystem._DEV_.config.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: process.env.APP_NAME,
|
||||
cwd: '/opt/node_app/app/',
|
||||
script: 'npm run start:dev --interpreter bash',
|
||||
// Options reference: https://pm2.keymetrics.io/docs/usage/application-declaration/
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
ignore_watch: [ 'node_modules', 'dist' ]
|
||||
}
|
||||
]
|
||||
};
|
||||
10
build/api/ecosystem._PROD_.config.js
Normal file
10
build/api/ecosystem._PROD_.config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
cwd: '/opt/node_app/app/',
|
||||
script: 'node dist/main --interpreter bash',
|
||||
autorestart: true,
|
||||
instances: 1
|
||||
}
|
||||
]
|
||||
};
|
||||
41
build/api/entrypoint.sh
Executable file
41
build/api/entrypoint.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "Node ENV: $NODE_ENV"
|
||||
|
||||
file_env() {
|
||||
local var="$1"
|
||||
local fileVar="${var}_FILE"
|
||||
local def="${2:-}"
|
||||
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
|
||||
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
|
||||
exit 1
|
||||
fi
|
||||
local val="$def"
|
||||
if [ "${!var:-}" ]; then
|
||||
val="${!var}"
|
||||
elif [ "${!fileVar:-}" ]; then
|
||||
val="$(< "${!fileVar}")"
|
||||
fi
|
||||
export "$var"="$val"
|
||||
unset "$fileVar"
|
||||
}
|
||||
|
||||
# file_env 'APP_KEY'
|
||||
|
||||
|
||||
# # Exit if APP_KEY is missing, this key is important!
|
||||
# if [ -z "$APP_KEY" ]
|
||||
# then
|
||||
# echo >&2 "[ERROR] No ENV for APP_KEY or APP_KEY_FILE found!"
|
||||
# echo >&2 "Run the ./generate-adonis-app-key.sh script or provide one at runtime"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
|
||||
|
||||
if [[ "$NODE_ENV" == "production" ]]; then
|
||||
pm2-runtime start /opt/node_app/ecosystem._PROD_.config.js
|
||||
else
|
||||
npm install && \
|
||||
pm2-runtime start /opt/node_app/ecosystem._DEV_.config.js
|
||||
fi
|
||||
|
|
@ -24,13 +24,13 @@ RUN go build -o main .
|
|||
FROM golang:1.14-alpine
|
||||
LABEL maintainer="Anthony Mineo <anthonymineo@gmail.com>"
|
||||
|
||||
RUN apk update && apk add --no-cache wget tzdata
|
||||
RUN apk update && apk add --no-cache wget tzdata lftp
|
||||
|
||||
#Set TimeZone
|
||||
ENV TZ=America/New_York
|
||||
|
||||
#Set cron schedule (every day at 9:30am est)
|
||||
RUN echo '30 9 * * * /app/start.sh' > /etc/crontabs/root
|
||||
# #Set cron schedule (every day at 11:30am est)
|
||||
# RUN echo '30 11 * * * /app/start.sh' > /etc/crontabs/root
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
@ -38,4 +38,5 @@ COPY --from=builder /app/main /app/main
|
|||
COPY ./app/t2-stat-parser/start.sh /app/start.sh
|
||||
COPY ./app/t2-stat-parser/parser-daemon.sh /app/parser-daemon.sh
|
||||
|
||||
ENTRYPOINT ./parser-daemon.sh
|
||||
#ENTRYPOINT ./parser-daemon.sh
|
||||
ENTRYPOINT ./start.sh
|
||||
|
|
@ -31,27 +31,54 @@ CREATE TABLE "public"."players" (
|
|||
);
|
||||
|
||||
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS "public"."games";
|
||||
CREATE SEQUENCE IF NOT EXISTS games_id_seq;
|
||||
|
||||
-- Table Definition
|
||||
CREATE TABLE "public"."games" (
|
||||
"game_id" numeric NOT NULL UNIQUE,
|
||||
"map" text NOT NULL,
|
||||
"datestamp" timestamp NOT NULL,
|
||||
"gametype" text NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT game_pk PRIMARY KEY (game_id)
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS "public"."game_detail";
|
||||
CREATE SEQUENCE IF NOT EXISTS games_id_seq;
|
||||
|
||||
-- Table Definition
|
||||
CREATE TABLE "public"."game_detail" (
|
||||
"id" int4 NOT NULL DEFAULT nextval('games_id_seq'::regclass),
|
||||
"player_guid" numeric NOT NULL,
|
||||
"player_name" text NOT NULL,
|
||||
"stat_overwrite" numeric NOT NULL,
|
||||
"map" text NOT NULL,
|
||||
"game_id" numeric NOT NULL DEFAULT 0,
|
||||
"game_id" numeric NOT NULL,
|
||||
"stats" jsonb NOT NULL,
|
||||
"datestamp" timestamp NOT NULL,
|
||||
"uuid" text NOT NULL UNIQUE,
|
||||
"gametype" text NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT games_pk PRIMARY KEY (id),
|
||||
FOREIGN KEY (game_id) REFERENCES games (game_id),
|
||||
FOREIGN KEY (player_guid) REFERENCES players (player_guid)
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
|
|
|
|||
693
build/postgres/postgresql.conf
Normal file
693
build/postgres/postgresql.conf
Normal file
|
|
@ -0,0 +1,693 @@
|
|||
# -----------------------------
|
||||
# PostgreSQL configuration file
|
||||
# -----------------------------
|
||||
#
|
||||
# This file consists of lines of the form:
|
||||
#
|
||||
# name = value
|
||||
#
|
||||
# (The "=" is optional.) Whitespace may be used. Comments are introduced with
|
||||
# "#" anywhere on a line. The complete list of parameter names and allowed
|
||||
# values can be found in the PostgreSQL documentation.
|
||||
#
|
||||
# The commented-out settings shown in this file represent the default values.
|
||||
# Re-commenting a setting is NOT sufficient to revert it to the default value;
|
||||
# you need to reload the server.
|
||||
#
|
||||
# This file is read on server startup and when the server receives a SIGHUP
|
||||
# signal. If you edit the file on a running system, you have to SIGHUP the
|
||||
# server for the changes to take effect, run "pg_ctl reload", or execute
|
||||
# "SELECT pg_reload_conf()". Some parameters, which are marked below,
|
||||
# require a server shutdown and restart to take effect.
|
||||
#
|
||||
# Any parameter can also be given as a command-line option to the server, e.g.,
|
||||
# "postgres -c log_connections=on". Some parameters can be changed at run time
|
||||
# with the "SET" SQL command.
|
||||
#
|
||||
# Memory units: kB = kilobytes Time units: ms = milliseconds
|
||||
# MB = megabytes s = seconds
|
||||
# GB = gigabytes min = minutes
|
||||
# TB = terabytes h = hours
|
||||
# d = days
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# FILE LOCATIONS
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# The default values of these variables are driven from the -D command-line
|
||||
# option or PGDATA environment variable, represented here as ConfigDir.
|
||||
|
||||
#data_directory = 'ConfigDir' # use data in another directory
|
||||
# (change requires restart)
|
||||
#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file
|
||||
# (change requires restart)
|
||||
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
|
||||
# (change requires restart)
|
||||
|
||||
# If external_pid_file is not explicitly set, no extra PID file is written.
|
||||
#external_pid_file = '' # write an extra PID file
|
||||
# (change requires restart)
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# CONNECTIONS AND AUTHENTICATION
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# - Connection Settings -
|
||||
|
||||
listen_addresses = '*'
|
||||
# comma-separated list of addresses;
|
||||
# defaults to 'localhost'; use '*' for all
|
||||
# (change requires restart)
|
||||
#port = 5432 # (change requires restart)
|
||||
max_connections = 200 # (change requires restart)
|
||||
#superuser_reserved_connections = 3 # (change requires restart)
|
||||
#unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories
|
||||
# (change requires restart)
|
||||
#unix_socket_group = '' # (change requires restart)
|
||||
#unix_socket_permissions = 0777 # begin with 0 to use octal notation
|
||||
# (change requires restart)
|
||||
#bonjour = off # advertise server via Bonjour
|
||||
# (change requires restart)
|
||||
#bonjour_name = '' # defaults to the computer name
|
||||
# (change requires restart)
|
||||
|
||||
# - TCP Keepalives -
|
||||
# see "man 7 tcp" for details
|
||||
|
||||
tcp_keepalives_idle = 180 # TCP_KEEPIDLE, in seconds;
|
||||
# 0 selects the system default
|
||||
tcp_keepalives_interval = 10 # TCP_KEEPINTVL, in seconds;
|
||||
# 0 selects the system default
|
||||
tcp_keepalives_count = 6 # TCP_KEEPCNT;
|
||||
# 0 selects the system default
|
||||
|
||||
# - Authentication -
|
||||
|
||||
#authentication_timeout = 1min # 1s-600s
|
||||
#password_encryption = md5 # md5 or scram-sha-256
|
||||
#db_user_namespace = off
|
||||
|
||||
# GSSAPI using Kerberos
|
||||
#krb_server_keyfile = ''
|
||||
#krb_caseins_users = off
|
||||
|
||||
# - SSL -
|
||||
|
||||
#ssl = off
|
||||
#ssl_ca_file = ''
|
||||
#ssl_cert_file = 'server.crt'
|
||||
#ssl_crl_file = ''
|
||||
#ssl_key_file = 'server.key'
|
||||
#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
|
||||
#ssl_prefer_server_ciphers = on
|
||||
#ssl_ecdh_curve = 'prime256v1'
|
||||
#ssl_dh_params_file = ''
|
||||
#ssl_passphrase_command = ''
|
||||
#ssl_passphrase_command_supports_reload = off
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# RESOURCE USAGE (except WAL)
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# - Memory -
|
||||
|
||||
shared_buffers = 1634MB # min 128kB
|
||||
# (change requires restart)
|
||||
#huge_pages = try # on, off, or try
|
||||
# (change requires restart)
|
||||
#temp_buffers = 8MB # min 800kB
|
||||
#max_prepared_transactions = 0 # zero disables the feature
|
||||
# (change requires restart)
|
||||
# Caution: it is not advisable to set max_prepared_transactions nonzero unless
|
||||
# you actively intend to use prepared transactions.
|
||||
work_mem = 8MB # min 64kB
|
||||
maintenance_work_mem = 524MB # min 1MB
|
||||
#autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem
|
||||
#max_stack_depth = 2MB # min 100kB
|
||||
dynamic_shared_memory_type = posix # the default is the first option
|
||||
# supported by the operating system:
|
||||
# posix
|
||||
# sysv
|
||||
# windows
|
||||
# mmap
|
||||
# use none to disable dynamic shared memory
|
||||
# (change requires restart)
|
||||
|
||||
# - Disk -
|
||||
|
||||
#temp_file_limit = -1 # limits per-process temp file space
|
||||
# in kB, or -1 for no limit
|
||||
|
||||
# - Kernel Resources -
|
||||
|
||||
#max_files_per_process = 1000 # min 25
|
||||
# (change requires restart)
|
||||
|
||||
# - Cost-Based Vacuum Delay -
|
||||
|
||||
#vacuum_cost_delay = 0 # 0-100 milliseconds
|
||||
#vacuum_cost_page_hit = 1 # 0-10000 credits
|
||||
#vacuum_cost_page_miss = 10 # 0-10000 credits
|
||||
#vacuum_cost_page_dirty = 20 # 0-10000 credits
|
||||
#vacuum_cost_limit = 200 # 1-10000 credits
|
||||
|
||||
# - Background Writer -
|
||||
|
||||
#bgwriter_delay = 200ms # 10-10000ms between rounds
|
||||
#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables
|
||||
#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round
|
||||
#bgwriter_flush_after = 512kB # measured in pages, 0 disables
|
||||
|
||||
# - Asynchronous Behavior -
|
||||
|
||||
effective_io_concurrency = 16 # 1-1000; 0 disables prefetching
|
||||
#max_worker_processes = 8 # (change requires restart)
|
||||
#max_parallel_maintenance_workers = 2 # taken from max_parallel_workers
|
||||
#max_parallel_workers_per_gather = 2 # taken from max_parallel_workers
|
||||
#parallel_leader_participation = on
|
||||
#max_parallel_workers = 8 # maximum number of max_worker_processes that
|
||||
# can be used in parallel operations
|
||||
#old_snapshot_threshold = -1 # 1min-60d; -1 disables; 0 is immediate
|
||||
# (change requires restart)
|
||||
#backend_flush_after = 0 # measured in pages, 0 disables
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# WRITE-AHEAD LOG
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# - Settings -
|
||||
|
||||
wal_level = logical # minimal, replica, or logical
|
||||
# (change requires restart)
|
||||
#fsync = on # flush data to disk for crash safety
|
||||
# (turning this off can cause
|
||||
# unrecoverable data corruption)
|
||||
synchronous_commit = off # synchronization level;
|
||||
# off, local, remote_write, remote_apply, or on
|
||||
#wal_sync_method = fsync # the default is the first option
|
||||
# supported by the operating system:
|
||||
# open_datasync
|
||||
# fdatasync (default on Linux)
|
||||
# fsync
|
||||
# fsync_writethrough
|
||||
# open_sync
|
||||
#full_page_writes = on # recover from partial page writes
|
||||
#wal_compression = off # enable compression of full-page writes
|
||||
#wal_log_hints = off # also do full page writes of non-critical updates
|
||||
# (change requires restart)
|
||||
wal_buffers = 17MB # min 32kB, -1 sets based on shared_buffers
|
||||
# (change requires restart)
|
||||
#wal_writer_delay = 200ms # 1-10000 milliseconds
|
||||
#wal_writer_flush_after = 1MB # measured in pages, 0 disables
|
||||
|
||||
#commit_delay = 0 # range 0-100000, in microseconds
|
||||
#commit_siblings = 5 # range 1-1000
|
||||
|
||||
# - Checkpoints -
|
||||
|
||||
checkpoint_timeout = 3600s # range 30s-1d
|
||||
max_wal_size = 6818MB
|
||||
min_wal_size = 80MB
|
||||
checkpoint_completion_target = 0.75 # checkpoint target duration, 0.0 - 1.0
|
||||
#checkpoint_flush_after = 256kB # measured in pages, 0 disables
|
||||
#checkpoint_warning = 30s # 0 disables
|
||||
|
||||
# - Archiving -
|
||||
|
||||
#archive_mode = off # enables archiving; off, on, or always
|
||||
# (change requires restart)
|
||||
#archive_command = '' # command to use to archive a logfile segment
|
||||
# placeholders: %p = path of file to archive
|
||||
# %f = file name only
|
||||
# e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
|
||||
archive_timeout = 300 # force a logfile segment switch after this
|
||||
# number of seconds; 0 disables
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# REPLICATION
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# - Sending Servers -
|
||||
|
||||
# Set these on the master and on any standby that will send replication data.
|
||||
|
||||
max_wal_senders = 8 # max number of walsender processes
|
||||
# (change requires restart)
|
||||
wal_keep_segments = 426 # in logfile segments; 0 disables
|
||||
#wal_sender_timeout = 60s # in milliseconds; 0 disables
|
||||
|
||||
max_replication_slots = 8 # max number of replication slots
|
||||
# (change requires restart)
|
||||
#track_commit_timestamp = off # collect timestamp of transaction commit
|
||||
# (change requires restart)
|
||||
|
||||
# - Master Server -
|
||||
|
||||
# These settings are ignored on a standby server.
|
||||
|
||||
#synchronous_standby_names = '' # standby servers that provide sync rep
|
||||
# method to choose sync standbys, number of sync standbys,
|
||||
# and comma-separated list of application_name
|
||||
# from standby(s); '*' = all
|
||||
#vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed
|
||||
|
||||
# - Standby Servers -
|
||||
|
||||
# These settings are ignored on a master server.
|
||||
|
||||
#hot_standby = on # "off" disallows queries during recovery
|
||||
# (change requires restart)
|
||||
#max_standby_archive_delay = 30s # max delay before canceling queries
|
||||
# when reading WAL from archive;
|
||||
# -1 allows indefinite delay
|
||||
#max_standby_streaming_delay = 30s # max delay before canceling queries
|
||||
# when reading streaming WAL;
|
||||
# -1 allows indefinite delay
|
||||
#wal_receiver_status_interval = 10s # send replies at least this often
|
||||
# 0 disables
|
||||
#hot_standby_feedback = off # send info from standby to prevent
|
||||
# query conflicts
|
||||
#wal_receiver_timeout = 60s # time that receiver waits for
|
||||
# communication from master
|
||||
# in milliseconds; 0 disables
|
||||
#wal_retrieve_retry_interval = 5s # time to wait before retrying to
|
||||
# retrieve WAL after a failed attempt
|
||||
|
||||
# - Subscribers -
|
||||
|
||||
# These settings are ignored on a publisher.
|
||||
|
||||
#max_logical_replication_workers = 4 # taken from max_worker_processes
|
||||
# (change requires restart)
|
||||
#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# QUERY TUNING
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# - Planner Method Configuration -
|
||||
|
||||
#enable_bitmapscan = on
|
||||
#enable_hashagg = on
|
||||
#enable_hashjoin = on
|
||||
#enable_indexscan = on
|
||||
#enable_indexonlyscan = on
|
||||
#enable_material = on
|
||||
#enable_mergejoin = on
|
||||
#enable_nestloop = on
|
||||
#enable_parallel_append = on
|
||||
#enable_seqscan = on
|
||||
#enable_sort = on
|
||||
#enable_tidscan = on
|
||||
#enable_partitionwise_join = off
|
||||
#enable_partitionwise_aggregate = off
|
||||
#enable_parallel_hash = on
|
||||
#enable_partition_pruning = on
|
||||
|
||||
# - Planner Cost Constants -
|
||||
|
||||
#seq_page_cost = 1.0 # measured on an arbitrary scale
|
||||
random_page_cost = 1.0 # same scale as above
|
||||
#cpu_tuple_cost = 0.01 # same scale as above
|
||||
#cpu_index_tuple_cost = 0.005 # same scale as above
|
||||
#cpu_operator_cost = 0.0025 # same scale as above
|
||||
#parallel_tuple_cost = 0.1 # same scale as above
|
||||
#parallel_setup_cost = 1000.0 # same scale as above
|
||||
|
||||
#jit_above_cost = 100000 # perform JIT compilation if available
|
||||
# and query more expensive than this;
|
||||
# -1 disables
|
||||
#jit_inline_above_cost = 500000 # inline small functions if query is
|
||||
# more expensive than this; -1 disables
|
||||
#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if
|
||||
# query is more expensive than this;
|
||||
# -1 disables
|
||||
|
||||
#min_parallel_table_scan_size = 8MB
|
||||
#min_parallel_index_scan_size = 512kB
|
||||
effective_cache_size = 4899MB
|
||||
|
||||
# - Genetic Query Optimizer -
|
||||
|
||||
#geqo = on
|
||||
#geqo_threshold = 12
|
||||
#geqo_effort = 5 # range 1-10
|
||||
#geqo_pool_size = 0 # selects default based on effort
|
||||
#geqo_generations = 0 # selects default based on effort
|
||||
#geqo_selection_bias = 2.0 # range 1.5-2.0
|
||||
#geqo_seed = 0.0 # range 0.0-1.0
|
||||
|
||||
# - Other Planner Options -
|
||||
|
||||
#default_statistics_target = 100 # range 1-10000
|
||||
#constraint_exclusion = partition # on, off, or partition
|
||||
#cursor_tuple_fraction = 0.1 # range 0.0-1.0
|
||||
#from_collapse_limit = 8
|
||||
#join_collapse_limit = 8 # 1 disables collapsing of explicit
|
||||
# JOIN clauses
|
||||
#force_parallel_mode = off
|
||||
#jit = off # allow JIT compilation
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# REPORTING AND LOGGING
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# - Where to Log -
|
||||
|
||||
#log_destination = 'stderr' # Valid values are combinations of
|
||||
# stderr, csvlog, syslog, and eventlog,
|
||||
# depending on platform. csvlog
|
||||
# requires logging_collector to be on.
|
||||
|
||||
# This is used when logging to stderr:
|
||||
#logging_collector = off # Enable capturing of stderr and csvlog
|
||||
# into log files. Required to be on for
|
||||
# csvlogs.
|
||||
# (change requires restart)
|
||||
|
||||
# These are only used if logging_collector is on:
|
||||
#log_directory = 'log' # directory where log files are written,
|
||||
# can be absolute or relative to PGDATA
|
||||
#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern,
|
||||
# can include strftime() escapes
|
||||
#log_file_mode = 0600 # creation mode for log files,
|
||||
# begin with 0 to use octal notation
|
||||
#log_truncate_on_rotation = off # If on, an existing log file with the
|
||||
# same name as the new log file will be
|
||||
# truncated rather than appended to.
|
||||
# But such truncation only occurs on
|
||||
# time-driven rotation, not on restarts
|
||||
# or size-driven rotation. Default is
|
||||
# off, meaning append to existing files
|
||||
# in all cases.
|
||||
#log_rotation_age = 1d # Automatic rotation of logfiles will
|
||||
# happen after that time. 0 disables.
|
||||
#log_rotation_size = 10MB # Automatic rotation of logfiles will
|
||||
# happen after that much log output.
|
||||
# 0 disables.
|
||||
|
||||
# These are relevant when logging to syslog:
|
||||
#syslog_facility = 'LOCAL0'
|
||||
#syslog_ident = 'postgres'
|
||||
#syslog_sequence_numbers = on
|
||||
#syslog_split_messages = on
|
||||
|
||||
# This is only relevant when logging to eventlog (win32):
|
||||
# (change requires restart)
|
||||
#event_source = 'PostgreSQL'
|
||||
|
||||
# - When to Log -
|
||||
|
||||
#log_min_messages = warning # values in order of decreasing detail:
|
||||
# debug5
|
||||
# debug4
|
||||
# debug3
|
||||
# debug2
|
||||
# debug1
|
||||
# info
|
||||
# notice
|
||||
# warning
|
||||
# error
|
||||
# log
|
||||
# fatal
|
||||
# panic
|
||||
|
||||
#log_min_error_statement = error # values in order of decreasing detail:
|
||||
# debug5
|
||||
# debug4
|
||||
# debug3
|
||||
# debug2
|
||||
# debug1
|
||||
# info
|
||||
# notice
|
||||
# warning
|
||||
# error
|
||||
# log
|
||||
# fatal
|
||||
# panic (effectively off)
|
||||
|
||||
log_min_duration_statement = 1000 # -1 is disabled, 0 logs all statements
|
||||
# and their durations, > 0 logs only
|
||||
# statements running at least this number
|
||||
# of milliseconds
|
||||
|
||||
|
||||
# - What to Log -
|
||||
|
||||
#debug_print_parse = off
|
||||
#debug_print_rewritten = off
|
||||
#debug_print_plan = off
|
||||
#debug_pretty_print = on
|
||||
log_checkpoints = on
|
||||
log_connections = on
|
||||
log_disconnections = on
|
||||
#log_duration = off
|
||||
#log_error_verbosity = default # terse, default, or verbose messages
|
||||
#log_hostname = off
|
||||
log_line_prefix = 'user=%u,db=%d,app=%a,client=%h ' # special values:
|
||||
# %a = application name
|
||||
# %u = user name
|
||||
# %d = database name
|
||||
# %r = remote host and port
|
||||
# %h = remote host
|
||||
# %p = process ID
|
||||
# %t = timestamp without milliseconds
|
||||
# %m = timestamp with milliseconds
|
||||
# %n = timestamp with milliseconds (as a Unix epoch)
|
||||
# %i = command tag
|
||||
# %e = SQL state
|
||||
# %c = session ID
|
||||
# %l = session line number
|
||||
# %s = session start timestamp
|
||||
# %v = virtual transaction ID
|
||||
# %x = transaction ID (0 if none)
|
||||
# %q = stop here in non-session
|
||||
# processes
|
||||
# %% = '%'
|
||||
# e.g. '<%u%%%d> '
|
||||
log_lock_waits = on # log lock waits >= deadlock_timeout
|
||||
#log_statement = 'none' # none, ddl, mod, all
|
||||
#log_replication_commands = off
|
||||
log_temp_files = 100 # log temporary files equal or larger
|
||||
# than the specified size in kilobytes;
|
||||
# -1 disables, 0 logs all temp files
|
||||
log_timezone = 'Etc/UTC'
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# PROCESS TITLE
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
#cluster_name = '' # added to process titles if nonempty
|
||||
# (change requires restart)
|
||||
#update_process_title = on
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# STATISTICS
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# - Query and Index Statistics Collector -
|
||||
|
||||
#track_activities = on
|
||||
#track_counts = on
|
||||
#track_io_timing = off
|
||||
#track_functions = none # none, pl, all
|
||||
#track_activity_query_size = 1024 # (change requires restart)
|
||||
#stats_temp_directory = 'pg_stat_tmp'
|
||||
|
||||
|
||||
# - Monitoring -
|
||||
|
||||
#log_parser_stats = off
|
||||
#log_planner_stats = off
|
||||
#log_executor_stats = off
|
||||
#log_statement_stats = off
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# AUTOVACUUM
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
#autovacuum = on # Enable autovacuum subprocess? 'on'
|
||||
# requires track_counts to also be on.
|
||||
log_autovacuum_min_duration = 1000 # -1 disables, 0 logs all actions and
|
||||
# their durations, > 0 logs only
|
||||
# actions running at least this number
|
||||
# of milliseconds.
|
||||
#autovacuum_max_workers = 3 # max number of autovacuum subprocesses
|
||||
# (change requires restart)
|
||||
#autovacuum_naptime = 1min # time between autovacuum runs
|
||||
#autovacuum_vacuum_threshold = 50 # min number of row updates before
|
||||
# vacuum
|
||||
#autovacuum_analyze_threshold = 50 # min number of row updates before
|
||||
# analyze
|
||||
#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum
|
||||
#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze
|
||||
autovacuum_freeze_max_age = 1000000000 # maximum XID age before forced vacuum
|
||||
# (change requires restart)
|
||||
#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age
|
||||
# before forced vacuum
|
||||
# (change requires restart)
|
||||
#autovacuum_vacuum_cost_delay = 20ms # default vacuum cost delay for
|
||||
# autovacuum, in milliseconds;
|
||||
# -1 means use vacuum_cost_delay
|
||||
#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for
|
||||
# autovacuum, -1 means use
|
||||
# vacuum_cost_limit
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# CLIENT CONNECTION DEFAULTS
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# - Statement Behavior -
|
||||
|
||||
#client_min_messages = notice # values in order of decreasing detail:
|
||||
# debug5
|
||||
# debug4
|
||||
# debug3
|
||||
# debug2
|
||||
# debug1
|
||||
# log
|
||||
# notice
|
||||
# warning
|
||||
# error
|
||||
#search_path = '"$user", public' # schema names
|
||||
#row_security = on
|
||||
#default_tablespace = '' # a tablespace name, '' uses the default
|
||||
#temp_tablespaces = '' # a list of tablespace names, '' uses
|
||||
# only default tablespace
|
||||
#check_function_bodies = on
|
||||
#default_transaction_isolation = 'read committed'
|
||||
#default_transaction_read_only = off
|
||||
#default_transaction_deferrable = off
|
||||
#session_replication_role = 'origin'
|
||||
#statement_timeout = 0 # in milliseconds, 0 is disabled
|
||||
#lock_timeout = 0 # in milliseconds, 0 is disabled
|
||||
#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled
|
||||
#vacuum_freeze_min_age = 50000000
|
||||
#vacuum_freeze_table_age = 150000000
|
||||
#vacuum_multixact_freeze_min_age = 5000000
|
||||
#vacuum_multixact_freeze_table_age = 150000000
|
||||
#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples
|
||||
# before index cleanup, 0 always performs
|
||||
# index cleanup
|
||||
#bytea_output = 'hex' # hex, escape
|
||||
#xmlbinary = 'base64'
|
||||
#xmloption = 'content'
|
||||
#gin_fuzzy_search_limit = 0
|
||||
#gin_pending_list_limit = 4MB
|
||||
|
||||
# - Locale and Formatting -
|
||||
|
||||
datestyle = 'iso, mdy'
|
||||
#intervalstyle = 'postgres'
|
||||
timezone = 'Etc/UTC'
|
||||
#timezone_abbreviations = 'Default' # Select the set of available time zone
|
||||
# abbreviations. Currently, there are
|
||||
# Default
|
||||
# Australia (historical usage)
|
||||
# India
|
||||
# You can create your own file in
|
||||
# share/timezonesets/.
|
||||
#extra_float_digits = 0 # min -15, max 3
|
||||
#client_encoding = sql_ascii # actually, defaults to database
|
||||
# encoding
|
||||
|
||||
# These settings are initialized by initdb, but they can be changed.
|
||||
lc_messages = 'en_US.utf8' # locale for system error message
|
||||
# strings
|
||||
lc_monetary = 'en_US.utf8' # locale for monetary formatting
|
||||
lc_numeric = 'en_US.utf8' # locale for number formatting
|
||||
lc_time = 'en_US.utf8' # locale for time formatting
|
||||
|
||||
# default configuration for text search
|
||||
default_text_search_config = 'pg_catalog.simple'
|
||||
|
||||
# - Shared Library Preloading -
|
||||
|
||||
#shared_preload_libraries = '' # (change requires restart)
|
||||
shared_preload_libraries = 'pg_stat_statements'
|
||||
pg_stat_statements.track = all
|
||||
|
||||
#local_preload_libraries = ''
|
||||
#session_preload_libraries = ''
|
||||
#jit_provider = 'llvmjit' # JIT library to use
|
||||
|
||||
# - Other Defaults -
|
||||
|
||||
#dynamic_library_path = '$libdir'
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# LOCK MANAGEMENT
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
#deadlock_timeout = 1s
|
||||
#max_locks_per_transaction = 64 # min 10
|
||||
# (change requires restart)
|
||||
#max_pred_locks_per_transaction = 64 # min 10
|
||||
# (change requires restart)
|
||||
#max_pred_locks_per_relation = -2 # negative values mean
|
||||
# (max_pred_locks_per_transaction
|
||||
# / -max_pred_locks_per_relation) - 1
|
||||
#max_pred_locks_per_page = 2 # min 0
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# VERSION AND PLATFORM COMPATIBILITY
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# - Previous PostgreSQL Versions -
|
||||
|
||||
#array_nulls = on
|
||||
#backslash_quote = safe_encoding # on, off, or safe_encoding
|
||||
#default_with_oids = off
|
||||
#escape_string_warning = on
|
||||
#lo_compat_privileges = off
|
||||
#operator_precedence_warning = off
|
||||
#quote_all_identifiers = off
|
||||
#standard_conforming_strings = on
|
||||
#synchronize_seqscans = on
|
||||
|
||||
# - Other Platforms and Clients -
|
||||
|
||||
#transform_null_equals = off
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# ERROR HANDLING
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
#exit_on_error = off # terminate session on any error?
|
||||
#restart_after_crash = on # reinitialize after backend crash?
|
||||
#data_sync_retry = off # retry or panic on failure to fsync
|
||||
# data?
|
||||
# (change requires restart)
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# CONFIG FILE INCLUDES
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# These options allow settings to be loaded from files other than the
|
||||
# default postgresql.conf.
|
||||
|
||||
#include_dir = '' # include files ending in '.conf' from
|
||||
# a directory, e.g., 'conf.d'
|
||||
#include_if_exists = '' # include file only if it exists
|
||||
#include = '' # include file
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# CUSTOMIZED OPTIONS
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# Add settings for extensions here
|
||||
|
|
@ -2,23 +2,57 @@ version: "3.7"
|
|||
|
||||
# Service Definitions
|
||||
services:
|
||||
|
||||
db:
|
||||
environment:
|
||||
POSTGRES_DB: "t2_stats"
|
||||
POSTGRES_USER: "dev"
|
||||
POSTGRES_PASSWORD: "dev"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- ./build/postgres/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
|
||||
- ./build/postgres/export_local_db.sh:/export_local_db.sh
|
||||
|
||||
- ./build/postgres/export_local_db.sh:/export_local_db.sh
|
||||
# - ./build/postgres/postgresql.conf:/var/lib/postgresql/data/postgresql.conf
|
||||
|
||||
parser:
|
||||
environment:
|
||||
environment:
|
||||
DATABASE_URL: "postgres://dev:dev@db:5432/t2_stats"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8000:8080"
|
||||
volumes:
|
||||
- ./app/t2-stat-parser:/app
|
||||
|
||||
api:
|
||||
environment:
|
||||
NODE_ENV: "development" # auto-reloads app on save
|
||||
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8443:8443"
|
||||
volumes:
|
||||
- ./build/api/ecosystem._PROD_.config.js:/opt/node_app/ecosystem._PROD_.config.js
|
||||
- ./build/api/ecosystem._DEV_.config.js:/opt/node_app/ecosystem._DEV_.config.js
|
||||
- ./app/api:/opt/node_app/app:delegated
|
||||
|
||||
# temp vols
|
||||
- notused:/opt/node_app/app/node_modules
|
||||
- builtincontainer:/opt/node_app/app/dist
|
||||
|
||||
|
||||
# pghero:
|
||||
# image: ankane/pghero
|
||||
# ports:
|
||||
# - "9999:8080"
|
||||
# environment:
|
||||
# DATABASE_URL: "postgres://dev:dev@db:5432/t2_stats"
|
||||
# networks:
|
||||
# - internal
|
||||
# - external
|
||||
|
||||
|
||||
|
||||
volumes:
|
||||
notused:
|
||||
builtincontainer:
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ version: "3.7"
|
|||
|
||||
# Service Definitions
|
||||
services:
|
||||
|
||||
db:
|
||||
image: "amineo/t2-stats-db:v0.1.2"
|
||||
image: "amineo/t2-stats-db:v0.2.0"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./build/postgres/Dockerfile
|
||||
|
|
@ -22,35 +21,46 @@ services:
|
|||
mode: replicated
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints: [node.role == manager]
|
||||
|
||||
constraints: [node.role == manager]
|
||||
|
||||
parser:
|
||||
image: "amineo/t2-stats-parser:v0.1.2"
|
||||
image: "amineo/t2-stats-parser:v0.5.0"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./build/go-t2-stat-parser/Dockerfile
|
||||
environment:
|
||||
environment:
|
||||
DATABASE_URL: "postgres://${POSTGRES_USER}:{POSTGRES_PASSWORD}@db:5432/t2_stats"
|
||||
FTP_HOST: "${FTP_HOST}"
|
||||
FTP_USER: "${FTP_USER}"
|
||||
FTP_PW: "${FTP_PW}"
|
||||
FTP_PW: "${FTP_PW}"
|
||||
FTP_PATH: "${FTP_PATH}"
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- internal
|
||||
- external
|
||||
deploy:
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
|
||||
api:
|
||||
image: "amineo/t2-stats-api:v0.0.26"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./build/api/Dockerfile
|
||||
environment:
|
||||
NODE_ENV: "production" # set as default in image
|
||||
APP_NAME: "T2StatsAPI" # set as default in image
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- internal
|
||||
- external
|
||||
|
||||
volumes:
|
||||
psqldata:
|
||||
|
||||
|
||||
networks:
|
||||
external:
|
||||
internal:
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"rootDirs": [
|
||||
"./app/api"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue