mirror of
https://github.com/amineo/t2-stat-parser.git
synced 2026-01-20 01:34:47 +00:00
Compare commits
121 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 | ||
|
|
915b396bc5 | ||
|
|
15d89ae5e1 | ||
|
|
e596b0ca7c | ||
|
|
9518a8a6b8 | ||
|
|
9c8a60e626 | ||
|
|
9071887407 | ||
|
|
15294ad2e5 | ||
|
|
788cc09aec | ||
|
|
cc1053eff0 |
11
.env.example
11
.env.example
|
|
@ -7,3 +7,14 @@ POSTGRES_PASSWORD="dev"
|
||||||
FTP_HOST="127.0.0.1"
|
FTP_HOST="127.0.0.1"
|
||||||
FTP_USER="user"
|
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"
|
||||||
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -1,4 +1,15 @@
|
||||||
.env
|
.env
|
||||||
|
node_modules
|
||||||
|
|
||||||
notes.md
|
notes.md
|
||||||
|
|
||||||
app/t2-stat-parser/serverStats/stats
|
app/t2-stat-parser/serverStats/stats
|
||||||
|
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"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
//ShellToUse is...
|
//ShellToUse is...
|
||||||
|
|
@ -24,12 +25,22 @@ func Shellout(command string) (error, string, string) {
|
||||||
func initFTP() {
|
func initFTP() {
|
||||||
|
|
||||||
ftpHOST := os.Getenv("FTP_HOST")
|
ftpHOST := os.Getenv("FTP_HOST")
|
||||||
|
ftpPath := os.Getenv("FTP_PATH")
|
||||||
ftpUSER := os.Getenv("FTP_USER")
|
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 {
|
if err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
log.Printf("error: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
@ -39,3 +50,5 @@ func initFTP() {
|
||||||
fmt.Println(errout)
|
fmt.Println(errout)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,15 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println("Starting FTP stat file download")
|
// fmt.Println("Starting FTP stat file download")
|
||||||
initFTP()
|
initFTP()
|
||||||
fmt.Println("Stat files downloaded!")
|
// fmt.Println("Stat files downloaded!")
|
||||||
|
|
||||||
fmt.Println("Starting stat parser")
|
fmt.Println("Starting stat parser")
|
||||||
initParser()
|
initParser()
|
||||||
fmt.Println("All done!")
|
fmt.Println("All done!", time.Now().Format(time.RFC850))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@ echo "T2 Stat Parser Running...";
|
||||||
# Run this initially, then we'll execute this on a schedule
|
# Run this initially, then we'll execute this on a schedule
|
||||||
./start.sh
|
./start.sh
|
||||||
|
|
||||||
# Keep container running
|
echo "T2 Stat Parser Done!";
|
||||||
tail -f /dev/null
|
# Keep container running with the cron schedule
|
||||||
|
#crond -l 2 -f
|
||||||
|
|
@ -32,8 +32,8 @@ import (
|
||||||
var (
|
var (
|
||||||
connectionString = flag.String("conn", getenvWithDefault("DATABASE_URL", ""), "PostgreSQL connection string")
|
connectionString = flag.String("conn", getenvWithDefault("DATABASE_URL", ""), "PostgreSQL connection string")
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
maxStatOverwrite int = 100
|
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
|
debugLevel int = 1 // 0 off,min | 1 Basic output checks | 2 Output all the things
|
||||||
)
|
)
|
||||||
|
|
||||||
func getenvWithDefault(name, defaultValue string) string {
|
func getenvWithDefault(name, defaultValue string) string {
|
||||||
|
|
@ -59,11 +59,12 @@ type Game struct {
|
||||||
dbStatOverWrite int `db.players:"stat_overwrite"`
|
dbStatOverWrite int `db.players:"stat_overwrite"`
|
||||||
statOverWrite int
|
statOverWrite int
|
||||||
|
|
||||||
gameMap string `db.games:"map"`
|
gameMap string `db.game_detail:"map"`
|
||||||
gameType string `db.games:"gametype"`
|
gameID int `db.game_detail:"game_id"`
|
||||||
dateStamp string `db.games:"datestamp"`
|
gameType string `db.game_detail:"gametype"`
|
||||||
stats string `db.games:"stats"`
|
dateStamp string `db.game_detail:"datestamp"`
|
||||||
uuid string `db.games:"uuid"`
|
stats string `db.game_detail:"stats"`
|
||||||
|
uuid string `db.game_detail:"uuid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func initParser() {
|
func initParser() {
|
||||||
|
|
@ -163,6 +164,9 @@ func parseGameTypeStats(gt string) {
|
||||||
break
|
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)
|
checkPlayer(g)
|
||||||
g.dbStatOverWrite = getDBStatOverWrite(g.playerGUID, strings.ToLower(gt))
|
g.dbStatOverWrite = getDBStatOverWrite(g.playerGUID, strings.ToLower(gt))
|
||||||
|
|
||||||
|
|
@ -175,32 +179,39 @@ func parseGameTypeStats(gt string) {
|
||||||
fmt.Println("Stat Overwrite", g.statOverWrite)
|
fmt.Println("Stat Overwrite", g.statOverWrite)
|
||||||
fmt.Println("maxStatOverwrite", maxStatOverwrite)
|
fmt.Println("maxStatOverwrite", maxStatOverwrite)
|
||||||
|
|
||||||
|
fmt.Println("statArrayMaxLength", statArrayMaxLength)
|
||||||
|
|
||||||
fmt.Println("g.dbStatOverWrite", g.dbStatOverWrite)
|
fmt.Println("g.dbStatOverWrite", g.dbStatOverWrite)
|
||||||
}
|
}
|
||||||
|
|
||||||
statCron := 0
|
// statEntryDiff := 0
|
||||||
if g.statOverWrite < g.dbStatOverWrite {
|
// if g.statOverWrite < g.dbStatOverWrite {
|
||||||
// 100 -
|
// // 100 -
|
||||||
statCron = (maxStatOverwrite - g.statOverWrite) + g.dbStatOverWrite
|
// statEntryDiff = (maxStatOverwrite - g.statOverWrite) + g.dbStatOverWrite
|
||||||
} else {
|
// } else {
|
||||||
statCron = g.statOverWrite - g.dbStatOverWrite
|
// statEntryDiff = g.statOverWrite - g.dbStatOverWrite
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Reset statCron if it flows over maxStatOverwrite
|
// // Reset statEntryDiff if it flows over maxStatOverwrite
|
||||||
if statCron > maxStatOverwrite {
|
// if statEntryDiff > maxStatOverwrite {
|
||||||
statCron = 0
|
// statEntryDiff = 0
|
||||||
}
|
// }
|
||||||
|
|
||||||
if debugLevel >= 1 {
|
// if debugLevel >= 1 {
|
||||||
fmt.Println("statCron", statCron)
|
// fmt.Println("statEntryDiff", statEntryDiff)
|
||||||
}
|
// }
|
||||||
|
|
||||||
for i := 0; i <= statCron; i++ {
|
for i := 0; i < statArrayMaxLength; i++ {
|
||||||
// arrPosition := i - 1
|
fmt.Println("index", i)
|
||||||
// fmt.Println(arrPosition)
|
|
||||||
parseStatOverWriteLine(g, mStatLine, i, strings.ToLower(gt))
|
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("---")
|
fmt.Println("---")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,8 +230,13 @@ func parseStatOverWriteLine(g Game, mStatLine map[string][]string, arrPosition i
|
||||||
|
|
||||||
cleanStatLine := make(map[string][]string)
|
cleanStatLine := make(map[string][]string)
|
||||||
for index, element := range mStatLine {
|
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])
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,13 +249,31 @@ func parseStatOverWriteLine(g Game, mStatLine map[string][]string, arrPosition i
|
||||||
g.uuid = genXid()
|
g.uuid = genXid()
|
||||||
g.gameMap = cleanStatLine["map"][0]
|
g.gameMap = cleanStatLine["map"][0]
|
||||||
|
|
||||||
|
if value, ok := mStatLine["gameID"]; ok {
|
||||||
|
fmt.Println("gameID", value[arrPosition])
|
||||||
|
g.gameID, err = strconv.Atoi(value[arrPosition])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Couldn't convert gameID to an int", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("No gameID found!")
|
||||||
|
g.gameID = 0
|
||||||
|
}
|
||||||
|
|
||||||
if debugLevel >= 2 {
|
if debugLevel >= 2 {
|
||||||
// log the game struct
|
// log the game struct
|
||||||
fmt.Println(g)
|
fmt.Println(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert game stat
|
// Check if we need to create a new game record
|
||||||
addPlayerGameStat(g, strings.ToLower(gt))
|
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 {
|
func rowExists(query string, args ...interface{}) bool {
|
||||||
|
|
@ -252,6 +286,28 @@ func rowExists(query string, args ...interface{}) bool {
|
||||||
return exists
|
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) {
|
func checkPlayer(g Game) {
|
||||||
check := rowExists("select player_guid from players where player_guid = $1", g.playerGUID)
|
check := rowExists("select player_guid from players where player_guid = $1", g.playerGUID)
|
||||||
if !check {
|
if !check {
|
||||||
|
|
@ -261,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) {
|
func createPlayer(uuid string, g Game) {
|
||||||
fmt.Println("Creating new player", g.playerName)
|
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)
|
_, err := db.Exec("insert into players(uuid, player_guid, player_name) values($1,$2,$3)", uuid, g.playerGUID, g.playerName)
|
||||||
|
|
@ -281,17 +347,27 @@ func getDBStatOverWrite(playerGUID int, gt string) int {
|
||||||
return dbStatOverWrite
|
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) {
|
func addPlayerGameStat(g Game, gt string) {
|
||||||
|
|
||||||
if debugLevel == 1 {
|
if debugLevel == 1 {
|
||||||
fmt.Println("g.dbStatOverWrite", g.dbStatOverWrite, "g.statOverWrite", g.statOverWrite)
|
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
|
// Insert new stat line
|
||||||
fmt.Println("New stat line!", g.playerName, g.dateStamp)
|
fmt.Println("New stat line!", g.playerName, g.dateStamp)
|
||||||
sqlInsert := `insert into games(player_guid, player_name, stat_overwrite, map, stats, datestamp, uuid, gametype) values($1,$2,$3,$4,$5,$6,$7,$8)`
|
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.stats, g.dateStamp, g.uuid, g.gameType)
|
_, 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 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Unable to add player's game stat: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Unable to add player's game stat: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -304,6 +380,9 @@ func addPlayerGameStat(g Game, gt string) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
||||||
|
|
@ -7,5 +7,6 @@ then
|
||||||
/app/main
|
/app/main
|
||||||
else
|
else
|
||||||
echo "No build found, running from source"
|
echo "No build found, running from source"
|
||||||
|
cd /app
|
||||||
go run *.go
|
go run *.go
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
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
|
FROM golang:1.14-alpine
|
||||||
LABEL maintainer="Anthony Mineo <anthonymineo@gmail.com>"
|
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
|
#Set TimeZone
|
||||||
ENV TZ=America/New_York
|
ENV TZ=America/New_York
|
||||||
|
|
||||||
#Set cron schedule (every day at 8am est)
|
# #Set cron schedule (every day at 11:30am est)
|
||||||
RUN crontab -l -u root | echo "0 8 * * * sh -c \"/app/start.sh\"" | crontab -u root -
|
# RUN echo '30 11 * * * /app/start.sh' > /etc/crontabs/root
|
||||||
|
|
||||||
WORKDIR /app
|
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/start.sh /app/start.sh
|
||||||
COPY ./app/t2-stat-parser/parser-daemon.sh /app/parser-daemon.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,26 +31,54 @@ CREATE TABLE "public"."players" (
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS "public"."games";
|
DROP TABLE IF EXISTS "public"."games";
|
||||||
CREATE SEQUENCE IF NOT EXISTS games_id_seq;
|
|
||||||
|
|
||||||
-- Table Definition
|
-- Table Definition
|
||||||
CREATE TABLE "public"."games" (
|
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),
|
"id" int4 NOT NULL DEFAULT nextval('games_id_seq'::regclass),
|
||||||
"player_guid" numeric NOT NULL,
|
"player_guid" numeric NOT NULL,
|
||||||
"player_name" text NOT NULL,
|
"player_name" text NOT NULL,
|
||||||
"stat_overwrite" numeric NOT NULL,
|
"stat_overwrite" numeric NOT NULL,
|
||||||
"map" text NOT NULL,
|
"map" text NOT NULL,
|
||||||
|
"game_id" numeric NOT NULL,
|
||||||
"stats" jsonb NOT NULL,
|
"stats" jsonb NOT NULL,
|
||||||
"datestamp" timestamp NOT NULL,
|
"datestamp" timestamp NOT NULL,
|
||||||
"uuid" text NOT NULL UNIQUE,
|
"uuid" text NOT NULL UNIQUE,
|
||||||
"gametype" text NOT NULL,
|
"gametype" text NOT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
CONSTRAINT games_pk PRIMARY KEY (id),
|
CONSTRAINT games_pk PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (game_id) REFERENCES games (game_id),
|
||||||
FOREIGN KEY (player_guid) REFERENCES players (player_guid)
|
FOREIGN KEY (player_guid) REFERENCES players (player_guid)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
|
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
echo "Setting up database..."
|
echo "Setting up database..."
|
||||||
BACKUP_FILE_PATH=/docker-entrypoint-initdb.d/backup/t2_stats.sql
|
BACKUP_FILE_PATH=/docker-entrypoint-initdb.d/backup/t2_stats.sql
|
||||||
|
|
||||||
psql -d t2_stats -U dev -p 5432 -a -q -f $BACKUP_FILE_PATH
|
psql -d t2_stats -U $POSTGRES_USER -p 5432 -a -q -f $BACKUP_FILE_PATH
|
||||||
# -h PostgreSQL server IP address
|
# -h PostgreSQL server IP address
|
||||||
# -d database name
|
# -d database name
|
||||||
# -U user name
|
# -U user name
|
||||||
|
|
|
||||||
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,19 +2,57 @@ version: "3.7"
|
||||||
|
|
||||||
# Service Definitions
|
# Service Definitions
|
||||||
services:
|
services:
|
||||||
|
|
||||||
db:
|
db:
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: "t2_stats"
|
POSTGRES_DB: "t2_stats"
|
||||||
POSTGRES_USER: "dev"
|
POSTGRES_USER: "dev"
|
||||||
POSTGRES_PASSWORD: "dev"
|
POSTGRES_PASSWORD: "dev"
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- ./build/postgres/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
|
- ./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:
|
parser:
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: "postgres://dev:dev@db:5432/t2_stats"
|
DATABASE_URL: "postgres://dev:dev@db:5432/t2_stats"
|
||||||
|
ports:
|
||||||
|
- "8000:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./app/t2-stat-parser:/app
|
- ./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,14 +2,11 @@ version: "3.7"
|
||||||
|
|
||||||
# Service Definitions
|
# Service Definitions
|
||||||
services:
|
services:
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: "amineo/t2-stats-db:v0.1.1"
|
image: "amineo/t2-stats-db:v0.2.0"
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./build/postgres/Dockerfile
|
dockerfile: ./build/postgres/Dockerfile
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: "t2_stats"
|
POSTGRES_DB: "t2_stats"
|
||||||
POSTGRES_USER: "${POSTGRES_USER}"
|
POSTGRES_USER: "${POSTGRES_USER}"
|
||||||
|
|
@ -18,31 +15,52 @@ services:
|
||||||
- psqldata:/var/lib/postgresql/data
|
- psqldata:/var/lib/postgresql/data
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
|
deploy:
|
||||||
|
labels:
|
||||||
|
- traefik.enable=false
|
||||||
|
mode: replicated
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
|
||||||
parser:
|
parser:
|
||||||
image: "amineo/t2-stats-parser:v0.1.1"
|
image: "amineo/t2-stats-parser:v0.5.0"
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./build/go-t2-stat-parser/Dockerfile
|
dockerfile: ./build/go-t2-stat-parser/Dockerfile
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: "postgres://${POSTGRES_USER}:{POSTGRES_PASSWORD}@db:5432/t2_stats"
|
DATABASE_URL: "postgres://${POSTGRES_USER}:{POSTGRES_PASSWORD}@db:5432/t2_stats"
|
||||||
FTP_HOST: "${FTP_HOST}"
|
FTP_HOST: "${FTP_HOST}"
|
||||||
FTP_USER: "${FTP_USER}"
|
FTP_USER: "${FTP_USER}"
|
||||||
FTP_PW: "${FTP_PW}"
|
FTP_PW: "${FTP_PW}"
|
||||||
|
FTP_PATH: "${FTP_PATH}"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
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:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
- external
|
- external
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
psqldata:
|
psqldata:
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
external:
|
external:
|
||||||
internal:
|
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