├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── dependabot.yml ├── pr-reviewers.yml └── workflows │ ├── auto-assign.yml │ ├── build-base-image.yml │ ├── deploy-production.yml │ ├── deploy-staging.yml │ ├── eslint.yml │ └── ts-compile.yml ├── .gitignore ├── .gitmodules ├── .husky └── pre-commit ├── .yamllint.yml ├── LICENCE ├── README.md ├── module-update.sh ├── nest-cli.json ├── package.json ├── provisioning ├── ansible.cfg ├── base │ ├── Dockerfile │ └── files │ │ └── entrypoint.sh ├── dev │ ├── Dockerfile │ ├── init.yml │ ├── start.yml │ └── stop.yml ├── production │ ├── Dockerfile │ ├── entrypoint.sh │ └── values.yaml ├── staging │ ├── Dockerfile │ ├── entrypoint.sh │ └── values.yaml └── vars.yml ├── src ├── app.module.ts ├── auth │ ├── auth.module.ts │ └── basic.strategy.ts ├── config │ ├── config.module.ts │ └── index.ts ├── controllers │ └── default.controller.ts ├── filters │ └── type-orm.filter.ts ├── instrument.ts ├── interfaces │ └── InternalApiAuthConfigInterface.ts ├── main.ts ├── modules │ ├── aggregator │ │ ├── aggregator.data.handler.ts │ │ ├── aggregator.module.ts │ │ ├── controllers │ │ │ ├── aggregates │ │ │ │ ├── global │ │ │ │ │ ├── aggregator.global.character.aggregate.controller.ts │ │ │ │ │ ├── aggregator.global.facility.control.aggregate.controller.ts │ │ │ │ │ ├── aggregator.global.faction.combat.aggregate.controller.ts │ │ │ │ │ ├── aggregator.global.loadout.aggregate.controller.ts │ │ │ │ │ ├── aggregator.global.outfit.aggregate.controller.ts │ │ │ │ │ ├── aggregator.global.vehicle.aggregate.controller.ts │ │ │ │ │ ├── aggregator.global.vehicle.character.aggregate.controller.ts │ │ │ │ │ ├── aggregator.global.victory.aggregate.controller.ts │ │ │ │ │ └── aggregator.global.weapon.aggregate.controller.ts │ │ │ │ └── instance │ │ │ │ │ ├── aggregator.instance.character.aggregate.controller.ts │ │ │ │ │ ├── aggregator.instance.facility.control.aggregate.controller.ts │ │ │ │ │ ├── aggregator.instance.faction.combat.aggregate.controller.ts │ │ │ │ │ ├── aggregator.instance.loadout.aggregate.controller.ts │ │ │ │ │ ├── aggregator.instance.outfit.aggregate.controller.ts │ │ │ │ │ ├── aggregator.instance.population.aggregate.controller.ts │ │ │ │ │ ├── aggregator.instance.vehicle.aggregate.controller.ts │ │ │ │ │ ├── aggregator.instance.vehicle.character.aggregate.controller.ts │ │ │ │ │ └── aggregator.instance.weapon.aggregate.controller.ts │ │ │ └── events │ │ │ │ ├── aggregator.instance.death.event.controller.ts │ │ │ │ └── aggregator.instance.facility.control.event.controller.ts │ │ └── interfaces │ │ │ ├── aggregator.message.interface.ts │ │ │ └── global.aggregator.message.interface.ts │ ├── cron │ │ ├── CronModule.ts │ │ ├── bracket.cron.ts │ │ ├── combat.history.cron.ts │ │ ├── outfitwars.rankings.cron.ts │ │ ├── population.history.cron.ts │ │ └── xpm.cron.ts │ ├── data │ │ ├── data.module.ts │ │ └── entities │ │ │ ├── aggregate │ │ │ ├── common │ │ │ │ ├── character.embed.ts │ │ │ │ ├── combat.stats.embed.ts │ │ │ │ ├── facility.embed.ts │ │ │ │ ├── facility.faction.control.embed.ts │ │ │ │ ├── faction.versus.faction.embed.ts │ │ │ │ ├── factionvsfaction │ │ │ │ │ ├── FacitonNSOVersionFactionEmbed.ts │ │ │ │ │ ├── FactionNCVersusFactionEmbed.ts │ │ │ │ │ ├── FactionTRVersusFactionEmbed.ts │ │ │ │ │ └── FactionVSVersusFactionEmbed.ts │ │ │ │ ├── item.embed.ts │ │ │ │ ├── metagame.territory.result.embed.ts │ │ │ │ ├── outfit.embed.ts │ │ │ │ ├── outfitwars.teams.embed.ts │ │ │ │ ├── outfitwars.territory.result.embed.ts │ │ │ │ ├── vehicle.vs.vehicle.embed.ts │ │ │ │ ├── xperminute.embed.ts │ │ │ │ └── xperminute.outfit.embed.ts │ │ │ ├── global │ │ │ │ ├── global.character.aggregate.entity.ts │ │ │ │ ├── global.facility.control.aggregate.entity.ts │ │ │ │ ├── global.faction.combat.aggregate.entity.ts │ │ │ │ ├── global.loadout.aggregate.entity.ts │ │ │ │ ├── global.outfit.aggregate.entity.ts │ │ │ │ ├── global.vehicle.aggregate.entity.ts │ │ │ │ ├── global.vehicle.character.aggregate.entity.ts │ │ │ │ ├── global.victory.aggregate.entity.ts │ │ │ │ └── global.weapon.aggregate.entity.ts │ │ │ └── instance │ │ │ │ ├── instance.character.aggregate.entity.ts │ │ │ │ ├── instance.combat.history.aggregate.entity.ts │ │ │ │ ├── instance.facility.control.aggregate.entity.ts │ │ │ │ ├── instance.faction.combat.aggregate.entity.ts │ │ │ │ ├── instance.loadout.aggregate.entity.ts │ │ │ │ ├── instance.outfit.aggregate.entity.ts │ │ │ │ ├── instance.population.aggregate.entity.ts │ │ │ │ ├── instance.population.averages.aggregate.entity.ts │ │ │ │ ├── instance.vehicle.aggregate.entity.ts │ │ │ │ ├── instance.vehicle.character.aggregate.entity.ts │ │ │ │ └── instance.weapon.aggregate.entity.ts │ │ │ └── instance │ │ │ ├── instance.death.entity.ts │ │ │ ├── instance.facilitycontrol.entity.ts │ │ │ ├── instance.features.embed.ts │ │ │ ├── instance.metagame.territory.entity.ts │ │ │ ├── instance.outfitwars.territory.entity.ts │ │ │ ├── outfitwars.facility.control.entity.ts │ │ │ ├── outfitwars.mapcontrol.embed.ts │ │ │ ├── outfitwars.metadata.embed.ts │ │ │ ├── outfitwars.ranking.entity.ts │ │ │ ├── outfitwars.ranking.params.embed.ts │ │ │ └── territory.control.mapcontrol.embed.ts │ ├── healthcheck │ │ ├── HealthCheckModule.ts │ │ ├── controllers │ │ │ └── healthcheck.controller.ts │ │ └── indicators │ │ │ ├── CronHealthIndicator.ts │ │ │ ├── DatabaseHealthIndicator.ts │ │ │ └── RedisHealthIndicator.ts │ └── rest │ │ ├── Dto │ │ ├── CreateFacilityControlDto.ts │ │ ├── CreateInstanceMetagameDto.ts │ │ ├── UpdateFacilityControlDto.ts │ │ ├── UpdateInstanceMetagameDto.ts │ │ └── outfitwars │ │ │ ├── CreateFacilityControlOutfitWarsDto.ts │ │ │ ├── CreateInstanceOutfitWarsDto.ts │ │ │ ├── UpdateFacilityControlOutfitWarsDto.ts │ │ │ ├── UpdateInstanceOutfitWarsDto.ts │ │ │ └── UpdateRankingOutfitWarsDto.ts │ │ ├── controllers │ │ ├── aggregates │ │ │ ├── global │ │ │ │ ├── BaseGlobalAggregateController.ts │ │ │ │ ├── rest.aggregate.global.character.controller.ts │ │ │ │ ├── rest.aggregate.global.facility.control.controller.ts │ │ │ │ ├── rest.aggregate.global.faction.combat.controller.ts │ │ │ │ ├── rest.aggregate.global.loadout.controller.ts │ │ │ │ ├── rest.aggregate.global.outfit.controller.ts │ │ │ │ ├── rest.aggregate.global.vehicle.character.controller.ts │ │ │ │ ├── rest.aggregate.global.vehicle.controller.ts │ │ │ │ ├── rest.aggregate.global.victory.controller.ts │ │ │ │ └── rest.aggregate.global.weapon.controller.ts │ │ │ └── instance │ │ │ │ ├── rest.aggregate.instance.character.controller.ts │ │ │ │ ├── rest.aggregate.instance.combat.history.controller.ts │ │ │ │ ├── rest.aggregate.instance.facility.control.controller.ts │ │ │ │ ├── rest.aggregate.instance.faction.combat.controller.ts │ │ │ │ ├── rest.aggregate.instance.loadout.controller.ts │ │ │ │ ├── rest.aggregate.instance.outfit.controller.ts │ │ │ │ ├── rest.aggregate.instance.population.averages.controller.ts │ │ │ │ ├── rest.aggregate.instance.population.controller.ts │ │ │ │ ├── rest.aggregate.instance.vehicle.character.controller.ts │ │ │ │ ├── rest.aggregate.instance.vehicle.controller.ts │ │ │ │ └── rest.aggregate.instance.weapon.controller.ts │ │ ├── census │ │ │ └── rest.census.continent.polyfill.controller.ts │ │ ├── common │ │ │ ├── rest.bracket.query.ts │ │ │ ├── rest.common.queries.ts │ │ │ ├── rest.date.query.ts │ │ │ ├── rest.instance.query.ts │ │ │ ├── rest.outfitwars.queries.ts │ │ │ ├── rest.pagination.queries.ts │ │ │ ├── rest.ps2AlertsEventType.query.ts │ │ │ ├── rest.result.victor.query.ts │ │ │ ├── rest.time.started.query.ts │ │ │ ├── rest.world.query.ts │ │ │ └── rest.zone.query.ts │ │ ├── rest.instance.facility.control.controller.ts │ │ ├── rest.instance.metagame.controller.ts │ │ └── rest.outfitwars.controller.ts │ │ ├── pipes │ │ ├── BracketPipe.ts │ │ ├── MandatoryIntPipe.ts │ │ ├── OptionalBoolPipe.ts │ │ ├── OptionalDatePipe.ts │ │ ├── OptionalIntPipe.ts │ │ └── Ps2AlertsEventTypePipe.ts │ │ └── rest.module.ts └── services │ ├── cache │ └── redis.cache.service.ts │ ├── databases │ └── mongo.config.ts │ └── mongo │ ├── mongo.operations.service.ts │ ├── pagination.ts │ └── range.ts ├── tsconfig.build.json ├── tsconfig.json ├── vars.local.yml.dist └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/**/* 2 | *.js 3 | 4 | # Modules 5 | src/modules/data/ps2alerts-constants/**/* 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # These are supported funding model platforms 3 | 4 | patreon: PS2Alerts 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | # Fetch and update latest `github-actions` pkgs 5 | - package-ecosystem: github-actions 6 | directory: '/' 7 | schedule: 8 | interval: weekly 9 | day: "saturday" 10 | open-pull-requests-limit: 10 11 | reviewers: 12 | - maelstromeous 13 | assignees: 14 | - maelstromeous 15 | commit-message: 16 | prefix: fix 17 | prefix-development: chore 18 | include: scope 19 | -------------------------------------------------------------------------------- /.github/pr-reviewers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Set to true to add reviewers to pull requests 3 | addReviewers: true 4 | 5 | # Set to true to add assignees to pull requests 6 | addAssignees: author 7 | 8 | # A list of reviewers to be added to pull requests (GitHub user name) 9 | reviewers: 10 | - Maelstromeous 11 | 12 | # A number of reviewers added to the pull request 13 | # Set 0 to add all the reviewers (default: 0) 14 | numberOfReviewers: 0 15 | 16 | # A list of assignees, overrides reviewers if set 17 | # assignees: 18 | # - assigneeA 19 | 20 | # A number of assignees to add to the pull request 21 | # Set to 0 to add all of the assignees. 22 | # Uses numberOfReviewers if unset. 23 | # numberOfAssignees: 2 24 | 25 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 26 | # skipKeywords: 27 | # - wip 28 | -------------------------------------------------------------------------------- /.github/workflows/auto-assign.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Auto Assign PRs' 3 | on: pull_request 4 | 5 | jobs: 6 | add-reviewers: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: kentaro-m/auto-assign-action@v1.2.1 10 | with: 11 | configuration-path: ".github/pr-reviewers.yml" 12 | -------------------------------------------------------------------------------- /.github/workflows/build-base-image.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build base image 3 | on: 4 | schedule: 5 | # Runs 6am on Mondays (see https://crontab.guru) 6 | - cron: '0 6 * * 1' 7 | workflow_dispatch: 8 | jobs: 9 | build-docker-images: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | node: [20] 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | submodules: true 23 | 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@v2 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v2 29 | with: 30 | version: latest 31 | 32 | - name: Login to DockerHub 33 | uses: docker/login-action@v2 34 | with: 35 | username: ${{ secrets.DOCKERHUB_USERNAME }} 36 | password: ${{ secrets.DOCKERHUB_TOKEN }} 37 | 38 | - name: Build and push base image 39 | uses: docker/build-push-action@v2 40 | with: 41 | context: provisioning/base 42 | file: provisioning/base/Dockerfile 43 | platforms: linux/amd64,linux/arm64 44 | push: true 45 | tags: maelstromeous/ps2alerts:api-base 46 | -------------------------------------------------------------------------------- /.github/workflows/deploy-production.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build & Deploy Production 3 | on: 4 | push: 5 | branches: [master] 6 | jobs: 7 | build-docker-images: 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest] 13 | node: [16] 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set current date as env variable 22 | run: echo "NOW=$(date +'%Y-%m-%dT%H:%M:%S')" >> $GITHUB_ENV 23 | 24 | - name: Set Short SHA variable 25 | run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-7`" >> $GITHUB_ENV 26 | 27 | - name: Get Latest tag 28 | id: latesttag 29 | uses: "WyriHaximus/github-action-get-previous-tag@v1" 30 | with: 31 | fallback: 'UNKNOWN' # Optional fallback tag to use when no tag can be found 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v2 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v2 38 | with: 39 | version: latest 40 | 41 | - name: Login to DockerHub 42 | uses: docker/login-action@v2 43 | with: 44 | username: ${{ secrets.DOCKERHUB_USERNAME }} 45 | password: ${{ secrets.DOCKERHUB_TOKEN }} 46 | 47 | - name: Build and push production image (AMD64) 48 | uses: docker/build-push-action@v2 49 | with: 50 | file: provisioning/production/Dockerfile 51 | platforms: linux/amd64 52 | push: true 53 | tags: | 54 | maelstromeous/ps2alerts:api-production-${{ github.sha }} 55 | maelstromeous/ps2alerts:api-production-latest 56 | build-args: | 57 | BUILD_SHA=${{ env.SHORT_SHA }} 58 | BUILT=${{ env.NOW }} 59 | VERSION=${{ steps.latesttag.outputs.tag }} 60 | 61 | - name: Build and push production image (ARM64) 62 | uses: docker/build-push-action@v2 63 | with: 64 | file: provisioning/production/Dockerfile 65 | platforms: linux/arm64 66 | push: true 67 | tags: | 68 | maelstromeous/ps2alerts:api-production-arm 69 | build-args: | 70 | BUILD_SHA=${{ env.SHORT_SHA }} 71 | BUILT=${{ env.NOW }} 72 | VERSION=${{ steps.latesttag.outputs.tag }} 73 | 74 | 75 | # deploy-to-k8s: 76 | # needs: build-docker-images 77 | # runs-on: ubuntu-latest 78 | # steps: 79 | # - name: Checkout 80 | # uses: actions/checkout@v4 81 | # with: 82 | # fetch-depth: 0 83 | # submodules: true 84 | # 85 | # - name: Find and replace version 86 | # uses: jacobtomlinson/gha-find-replace@v2 87 | # with: 88 | # find: "{{ version }}" 89 | # replace: "${{ github.sha }}" 90 | # include: provisioning/production/values.yaml 91 | # 92 | # - name: Deploy Helm 93 | # uses: glopezep/helm@v1.7.1 94 | # with: 95 | # helm: helm3 96 | # release: ps2alerts-api-production 97 | # namespace: ps2alerts 98 | # chart: provisioning/helm/charts/microservice 99 | # secrets: ${{ toJSON(secrets) }} 100 | # token: ${{ github.token }} 101 | # value-files: provisioning/production/values.yaml 102 | # version: ${{ github.sha }} 103 | # # timeout: 120s # Apparently this isn't supported but it's in the docs? :shrug: 104 | # env: 105 | # KUBECONFIG_FILE: "${{ secrets.KUBE_CONFIG_RAW }}" 106 | -------------------------------------------------------------------------------- /.github/workflows/deploy-staging.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build & Deploy Staging 3 | on: 4 | push: 5 | branches: [staging] 6 | jobs: 7 | build-docker-images: 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest] 13 | node: [20] 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | submodules: true 21 | 22 | - name: Set current date as env variable 23 | run: echo "NOW=$(date +'%Y-%m-%dT%H:%M:%S')" >> $GITHUB_ENV 24 | 25 | - name: Set Short SHA variable 26 | run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-7`" >> $GITHUB_ENV 27 | 28 | - name: Get Latest tag 29 | id: latesttag 30 | uses: "WyriHaximus/github-action-get-previous-tag@v1" 31 | with: 32 | fallback: 'UNKNOWN' # Optional fallback tag to use when no tag can be found 33 | 34 | - name: Set up QEMU 35 | uses: docker/setup-qemu-action@v2 36 | 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v2 39 | with: 40 | version: latest 41 | 42 | - name: Login to DockerHub 43 | uses: docker/login-action@v2 44 | with: 45 | username: ${{ secrets.DOCKERHUB_USERNAME }} 46 | password: ${{ secrets.DOCKERHUB_TOKEN }} 47 | 48 | - name: Build and push staging image 49 | uses: docker/build-push-action@v2 50 | with: 51 | file: provisioning/staging/Dockerfile 52 | platforms: linux/amd64 53 | push: true 54 | tags: | 55 | maelstromeous/ps2alerts:api-staging-${{ github.sha }} 56 | maelstromeous/ps2alerts:api-staging-latest 57 | build-args: | 58 | BUILD_SHA=${{ env.SHORT_SHA }} 59 | BUILT=${{ env.NOW }} 60 | VERSION=${{ steps.latesttag.outputs.tag }} 61 | # 62 | # deploy-to-k8s: 63 | # needs: build-docker-images 64 | # runs-on: ubuntu-latest 65 | # steps: 66 | # - name: Checkout 67 | # uses: actions/checkout@v4 68 | # with: 69 | # fetch-depth: 0 70 | # submodules: true 71 | # 72 | # - name: Find and replace version 73 | # uses: jacobtomlinson/gha-find-replace@v2 74 | # with: 75 | # find: "{{ version }}" 76 | # replace: "${{ github.sha }}" 77 | # include: provisioning/staging/values.yaml 78 | # 79 | # - name: Deploy Helm 80 | # uses: glopezep/helm@v1.7.1 81 | # with: 82 | # helm: helm3 83 | # release: ps2alerts-api-staging 84 | # namespace: ps2alerts 85 | # chart: provisioning/helm/charts/microservice 86 | # secrets: ${{ toJSON(secrets) }} 87 | # token: ${{ github.token }} 88 | # value-files: provisioning/staging/values.yaml 89 | # version: ${{ github.sha }} 90 | # # timeout: 120s # Apparently this isn't supported but it's in the docs? :shrug: 91 | # env: 92 | # KUBECONFIG_FILE: "${{ secrets.KUBE_CONFIG_RAW }}" 93 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ESLint 3 | on: 4 | push: 5 | pull_request: 6 | jobs: 7 | eslint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | submodules: true 15 | 16 | - name: install node v16 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 16 20 | 21 | - name: yarn install 22 | run: yarn install 23 | 24 | - name: yarn run lint 25 | run: yarn run lint 26 | -------------------------------------------------------------------------------- /.github/workflows/ts-compile.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Typescript Compile 3 | on: 4 | push: 5 | pull_request: 6 | jobs: 7 | typescript-compile: 8 | name: Typescript Compile 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | submodules: true 16 | 17 | - name: Install node v16 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | 22 | - name: Install modules 23 | run: yarn install 24 | 25 | - name: Build application 26 | run: yarn run build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Provisioning 6 | vars.local.yml 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # OS 17 | .DS_Store 18 | 19 | # Tests 20 | /coverage 21 | /.nyc_output 22 | 23 | # IDEs and editors 24 | /.idea 25 | .project 26 | .classpath 27 | .c9/ 28 | *.launch 29 | .settings/ 30 | *.sublime-workspace 31 | 32 | # IDE - VSCode 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json 38 | 39 | .tfplan 40 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/modules/data/ps2alerts-constants"] 2 | path = src/modules/data/ps2alerts-constants 3 | url = https://github.com/ps2alerts/constants 4 | branch = main 5 | [submodule "provisioning/helm"] 6 | path = provisioning/helm 7 | url = https://github.com/ps2alerts/helm-charts 8 | branch = master 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | rules: 4 | comments-indentation: disable 5 | line-length: disable 6 | truthy: disable 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ps2alerts/api 2 | 3 | This project is currently under development. If you wish to contribute, please join our Discord located at: https://discord.gg/7xF65ap and check out the channel _#if-you-wish-to-contribute_. 4 | 5 | # Repo submodules 6 | 7 | NOTE: After you have cloned this repository, you also need to install the git submodule which contain various constants used across the PS2Alerts repositories. Simply execute 8 | 9 | ``` 10 | git submodule update --init --recursive 11 | ``` 12 | 13 | If in the future your application is performing weird saying it can't find references to things, pull the latest modules in via running `./module-update.sh`. 14 | 15 | # Starting the module 16 | 17 | To start the environment for the first time, execute `ps2alerts-api-init`. If you don't have this command available, pull in the [Stack](https://github.com/ps2alerts/stack) and follow the instructions there to get set up properly. 18 | 19 | tl;dr: 20 | 21 | `ps2alerts-stack-start` if stack not already started. If you wish to see output of this module, run `ps2alerts-api-dev`. 22 | 23 | # Responsibilities 24 | 25 | The API is a very dumb overlay over MongoDB, which inserts and updates data. It acts as a single source of truth for data access. 26 | 27 | # Interaction with PS2Alerts Aggregator 28 | 29 | The [PS2Alerts Aggregator](https://github.com/ps2alerts/websocket) consumes data coming in from the Census Streaming Service and generates aggregates / statistics for us to store. 30 | 31 | The Aggregator service pushes the data to the API over a RabbitMQ queue. The API consumes this queue, and routes it to the endpoint controllers via a feature within NestJS called `MessagePattern`. This forwards anything listening on this message pattern to the endpoint / controller in question. 32 | 33 | In order for this to work, messages must be supplied to the queue NestJS is listening to in the following format: 34 | 35 | ``` 36 | { 37 | "pattern": "instanceDeath", 38 | "data": { 39 | "foo": "bar" 40 | } 41 | } 42 | ``` 43 | 44 | Here, any endpoint / function with `@EventPattern('instanceDeath')` will consume the message and process it. 45 | -------------------------------------------------------------------------------- /module-update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git submodule update --remote 3 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ps2alerts-api", 3 | "version": "4.4.2", 4 | "description": "The API that powers PS2Alerts.com", 5 | "author": "", 6 | "private": true, 7 | "license": "GNU", 8 | "engines": { 9 | "node": ">=16.0.0" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/ps2alerts/api/issues" 13 | }, 14 | "homepage": "https://github.com/ps2alerts/api#readme", 15 | "engineStrict": true, 16 | "scripts": { 17 | "prebuild": "rimraf dist", 18 | "build": "nest build", 19 | "start": "nest start", 20 | "start:dev": "nest start --watch", 21 | "start:debug": "nest start --debug --watch", 22 | "start:prod": "node dist/main", 23 | "lint": "eslint ./src --ext .ts", 24 | "lint-fix": "eslint ./src --ext .ts --fix", 25 | "test": "jest", 26 | "test:watch": "jest --watch", 27 | "test:cov": "jest --coverage", 28 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 29 | "test:e2e": "jest --config ./test/jest-e2e.json", 30 | "prepare": "husky install" 31 | }, 32 | "resolutions": { 33 | "schema-utils": "^4.0.0" 34 | }, 35 | "dependencies": { 36 | "@fastify/compress": "^6.2.1", 37 | "@fastify/helmet": "^10.1.0", 38 | "@fastify/static": "^6.9.0", 39 | "@nestjs/axios": "^2.0.0", 40 | "@nestjs/common": "^9.3.12", 41 | "@nestjs/config": "^2.3.1", 42 | "@nestjs/core": "^9.3.12", 43 | "@nestjs/microservices": "^9.3.12", 44 | "@nestjs/passport": "^9.0.3", 45 | "@nestjs/platform-fastify": "^9.3.12", 46 | "@nestjs/schedule": "^2.2.0", 47 | "@nestjs/swagger": "^6.2.1", 48 | "@nestjs/terminus": "^9.2.1", 49 | "@nestjs/typeorm": "^9.0.1", 50 | "@nestjs/websockets": "^9.3.12", 51 | "@sentry/browser": "^8.4.0", 52 | "@sentry/node": "^8.4.0", 53 | "@sentry/profiling-node": "^8.4.0", 54 | "@types/cache-manager-ioredis": "^2.0.3", 55 | "@types/cron": "^2.0.1", 56 | "@willsoto/nestjs-prometheus": "^6.0.0", 57 | "amqp-connection-manager": "^4.1.11", 58 | "amqplib": "^0.10.3", 59 | "axios": "^1.8.2", 60 | "cache-manager": "^4.1.0", 61 | "cache-manager-ioredis": "^2.1.0", 62 | "class-transformer": "^0.5.1", 63 | "class-validator": "^0.14.0", 64 | "mongodb": "^4.17.1", 65 | "nestjs-swagger-api-implicit-queries-decorator": "^1.0.0", 66 | "passport": "^0.6.0", 67 | "passport-http": "^0.3.0", 68 | "prom-client": "^15.1.2", 69 | "reflect-metadata": "^0.1.13", 70 | "rxjs": "^7.8.0", 71 | "typeorm": "^0.3.20" 72 | }, 73 | "devDependencies": { 74 | "@nestjs/cli": "9.5.0", 75 | "@types/node": "^18.15.11", 76 | "@types/passport-http": "^0.3.9", 77 | "@types/password": "^0.1.0", 78 | "@types/supertest": "^2.0.12", 79 | "@types/validator": "^13.7.13", 80 | "@types/ws": "^8.5.4", 81 | "@typescript-eslint/eslint-plugin": "^5.57.0", 82 | "@typescript-eslint/parser": "^5.57.0", 83 | "eslint": "^8.36.0", 84 | "eslint-plugin-import": "^2.27.5", 85 | "husky": "^8.0.3", 86 | "lint-staged": "^13.2.0", 87 | "ts-node": "^10.9.1", 88 | "typescript": "^4.9.5" 89 | }, 90 | "jest": { 91 | "moduleFileExtensions": [ 92 | "js", 93 | "json", 94 | "ts" 95 | ], 96 | "rootDir": "src", 97 | "testRegex": ".spec.ts$", 98 | "transform": { 99 | "^.+\\.(t|j)s$": "ts-jest" 100 | }, 101 | "coverageDirectory": "../coverage", 102 | "testEnvironment": "node" 103 | }, 104 | "lint-staged": { 105 | "src/**/*.ts": [ 106 | "eslint --fix" 107 | ] 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /provisioning/ansible.cfg: -------------------------------------------------------------------------------- 1 | [default] 2 | stdout_error=debug 3 | stdout_debug=debug -------------------------------------------------------------------------------- /provisioning/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | USER root 3 | COPY ./files/entrypoint.sh /etc/service/entrypoint.sh 4 | 5 | USER node 6 | ENTRYPOINT /etc/service/entrypoint.sh 7 | -------------------------------------------------------------------------------- /provisioning/base/files/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "=============== STARTING API (DEV) ===================" 4 | cd /app && yarn run start:dev 5 | -------------------------------------------------------------------------------- /provisioning/dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ps2alerts/api:base 2 | 3 | USER root 4 | 5 | RUN apt-get update \ 6 | && apt-get install -y --no-install-recommends \ 7 | watch \ 8 | && rm -rf /var/lib/apt/lists/* /var/cache/apk/* 9 | 10 | USER node -------------------------------------------------------------------------------- /provisioning/dev/init.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | vars_files: 4 | - "{{ playbook_dir }}/../vars.yml" 5 | # - "{{ playbook_dir }}/../vars_local.yml" 6 | tasks: 7 | - debug: 8 | msg: "========= 🔗 Initializing API Module 🔗 =========" 9 | 10 | - name: Install docker 11 | pip: 12 | name: docker 13 | state: present 14 | 15 | - name: Get user's current UID 16 | command: "id -u {{ ansible_user_id }}" 17 | register: id 18 | when: id is undefined 19 | 20 | - name: Create docker network 21 | docker_network: 22 | name: ps2alerts 23 | state: present 24 | 25 | - name: Build API Base image (grab a snickers) 26 | docker_image: 27 | name: ps2alerts/api 28 | state: present 29 | source: build 30 | tag: base 31 | force_source: yes 32 | build: 33 | pull: no 34 | path: "{{ root_dir | realpath }}/provisioning/base" 35 | 36 | - name: Build API Dev image 37 | docker_image: 38 | name: ps2alerts/api 39 | state: present 40 | source: build 41 | tag: dev 42 | force_source: yes 43 | build: 44 | pull: no 45 | path: "{{ root_dir | realpath }}/provisioning/dev" 46 | 47 | # Issues with MacOSX due to docker root directory permission errors which can't be solved. Run yarn install manually if you're on OSX. 48 | - name: Run npm install for API deps 49 | docker_container: 50 | name: ps2alerts-api-yarn 51 | image: node:16 52 | volumes: 53 | - "{{ root_dir | realpath }}:/app:rw" 54 | user: "{{ id.stdout }}:{{ id.stdout }}" 55 | command: "cd /app && yarn install" 56 | labels: 57 | traefik.enable: "false" 58 | -------------------------------------------------------------------------------- /provisioning/dev/start.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | vars_files: 4 | - "{{ playbook_dir }}/../vars.yml" 5 | - ../../vars.local.yml 6 | # - "{{ playbook_dir }}/../vars_local.yml" 7 | tasks: 8 | - debug: 9 | msg: "========= 🔗 Starting API Module 🔗 =========" 10 | 11 | - name: Delete dist folder 12 | file: 13 | path: "{{ root_dir }}/dist" 14 | state: absent 15 | 16 | - name: Start API Dev Image 17 | docker_container: 18 | name: ps2alerts-api 19 | image: ps2alerts/api:dev 20 | state: started 21 | restart: no 22 | recreate: yes 23 | restart_policy: "no" 24 | volumes: 25 | - "{{ root_dir | realpath }}:/app:rw" 26 | networks: 27 | - name: "ps2alerts" 28 | tty: true 29 | ports: 30 | - "3000:3000" 31 | env: 32 | NODE_ENV: "development" 33 | VERSION: "12345" 34 | HTTP_PORT: "3000" 35 | DB_HOST: "ps2alerts-db" 36 | DB_PORT: "27017" 37 | DB_USER: "root" 38 | DB_PASS: "foobar" 39 | DB_NAME: "ps2alerts" 40 | DB_DEBUG: "true" 41 | REDIS_HOST: "ps2alerts-redis" 42 | REDIS_PORT: "6379" 43 | REDIS_DB: "1" 44 | RABBITMQ_HOST: "ps2alerts-mq" 45 | AGGREGATOR_ENABLED: "true" 46 | CRON_ENABLED: "true" 47 | REST_ENABLED: "true" 48 | INTERNAL_API_USER: "ps2alerts" 49 | INTERNAL_API_PASS: "foobar" 50 | CENSUS_SERVICE_ID: "{{ census_service_id }}" 51 | labels: 52 | traefik.enable: "true" 53 | traefik.http.routers.ps2alerts_api_http.rule: "Host(`dev.api.ps2alerts.com`)" 54 | traefik.http.routers.ps2alerts_api_http.entrypoints: "web" 55 | traefik.http.routers.ps2alerts_api.middlewares: "redirect-to-https" 56 | traefik.http.middlewares.redirect-to-https.redirectscheme.scheme: "https" 57 | traefik.http.routers.ps2alerts_api.entrypoints: "websecure" 58 | traefik.http.routers.ps2alerts_api.rule: "Host(`dev.api.ps2alerts.com`)" 59 | traefik.http.routers.ps2alerts_api.tls: "true" 60 | traefik.http.routers.ps2alerts_api.tls.domains[0].main: "dev.api.ps2alerts.com" 61 | -------------------------------------------------------------------------------- /provisioning/dev/stop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | tasks: 4 | - name: Stop API container 5 | docker_container: 6 | name: ps2alerts-api 7 | state: stopped 8 | ignore_errors: true 9 | -------------------------------------------------------------------------------- /provisioning/production/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maelstromeous/ps2alerts:api-base 2 | 3 | USER root 4 | COPY provisioning/production/entrypoint.sh /etc/service/entrypoint.sh 5 | 6 | USER node 7 | COPY --chown=node:node . /app 8 | RUN cd /app && yarn install && yarn run build 9 | -------------------------------------------------------------------------------- /provisioning/production/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "=============== STARTING API (PRODUCTION) ===================" 4 | cd /app && npm run start:prod 5 | -------------------------------------------------------------------------------- /provisioning/staging/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maelstromeous/ps2alerts:api-base 2 | 3 | USER root 4 | COPY provisioning/staging/entrypoint.sh /etc/service/entrypoint.sh 5 | 6 | USER node 7 | COPY --chown=node:node . /app 8 | RUN cd /app && yarn install && yarn run build 9 | -------------------------------------------------------------------------------- /provisioning/staging/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "=============== STARTING API (STAGING) ===================" 4 | cd /app && npm run start:prod 5 | -------------------------------------------------------------------------------- /provisioning/vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root_dir: "{{ playbook_dir }}/../../" 3 | app_dir: "{{ root_dir }}/src" 4 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import {Module} from '@nestjs/common'; 2 | import ConfigModule from './config/config.module'; 3 | import {DefaultController} from './controllers/default.controller'; 4 | import {RestModule} from './modules/rest/rest.module'; 5 | import {AggregatorModule} from './modules/aggregator/aggregator.module'; 6 | import {TypeOrmModule} from '@nestjs/typeorm'; 7 | import {MongoConfig} from './services/databases/mongo.config'; 8 | import {ScheduleModule} from '@nestjs/schedule'; 9 | import {CronModule} from './modules/cron/CronModule'; 10 | import {HealthCheckModule} from './modules/healthcheck/HealthCheckModule'; 11 | import {PrometheusModule} from '@willsoto/nestjs-prometheus'; 12 | 13 | const metadata = { 14 | imports: [ 15 | ConfigModule, // Must come first 16 | TypeOrmModule.forRootAsync({ 17 | imports: [ConfigModule], 18 | useClass: MongoConfig, 19 | }), 20 | HealthCheckModule, 21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access 22 | PrometheusModule.register(), 23 | ], 24 | controllers: [DefaultController], 25 | providers: [], 26 | }; 27 | 28 | if (process.env.AGGREGATOR_ENABLED === 'true') { 29 | metadata.imports.push(AggregatorModule); 30 | } 31 | 32 | if (process.env.CRON_ENABLED === 'true') { 33 | metadata.imports.push(ScheduleModule.forRoot(), CronModule); 34 | } 35 | 36 | if (process.env.REST_ENABLED === 'true') { 37 | metadata.imports.push(RestModule); 38 | } 39 | 40 | @Module(metadata) 41 | export class AppModule {} 42 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import {PassportModule} from '@nestjs/passport'; 2 | import {ConfigModule} from '@nestjs/config'; 3 | import {Module} from '@nestjs/common'; 4 | import {BasicStrategy} from './basic.strategy'; 5 | 6 | @Module({ 7 | imports: [PassportModule, ConfigModule], 8 | providers: [BasicStrategy], 9 | }) 10 | 11 | export class AuthModule {} 12 | -------------------------------------------------------------------------------- /src/auth/basic.strategy.ts: -------------------------------------------------------------------------------- 1 | import {PassportStrategy} from '@nestjs/passport'; 2 | import {BasicStrategy as Strategy} from 'passport-http'; 3 | import {ConfigService} from '@nestjs/config'; 4 | import {Injectable, UnauthorizedException} from '@nestjs/common'; 5 | import {InternalApiAuthConfigInterface} from '../interfaces/InternalApiAuthConfigInterface'; 6 | 7 | // The class name here is important as it overrides default behaviour. 8 | // noinspection JSUnusedGlobalSymbols 9 | @Injectable() 10 | export class BasicStrategy extends PassportStrategy(Strategy) { 11 | constructor( 12 | private readonly config: ConfigService, 13 | ) { 14 | super({ 15 | passRegToCallback: true, 16 | }); 17 | } 18 | 19 | validate(username: string, password: string): boolean { 20 | const internalApiAuthConfig: InternalApiAuthConfigInterface | null = this.config.get('internalApiAuth') ?? null; 21 | 22 | if (!internalApiAuthConfig?.password) { 23 | throw new Error('Could not find internal API config!'); 24 | } 25 | 26 | if ( 27 | internalApiAuthConfig.username === username && 28 | internalApiAuthConfig.password === password 29 | ) { 30 | return true; 31 | } 32 | 33 | throw new UnauthorizedException(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import {ConfigModule} from '@nestjs/config'; 2 | import {config} from './index'; 3 | 4 | export default ConfigModule.forRoot({ 5 | isGlobal: true, 6 | load: [config], 7 | }); 8 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/explicit-function-return-type 3 | export const config = () => ({ 4 | cors: {}, 5 | 6 | env: process.env.NODE_ENV, 7 | 8 | census: { 9 | serviceId: process.env.CENSUS_SERVICE_ID, 10 | }, 11 | 12 | database: { 13 | mongo: { 14 | type: 'mongodb', 15 | host: process.env.DB_HOST ?? 'localhost', 16 | port: process.env.DB_PORT ?? 27017, 17 | username: process.env.DB_USER ?? 'root', 18 | password: encodeURIComponent(process.env.DB_PASS ?? 'foobar'), 19 | database: process.env.DB_NAME ?? 'ps2alerts', 20 | synchronize: true, 21 | logging: true, 22 | authSource: process.env.DB_AUTH_SOURCE ?? 'admin', 23 | useUnifiedTopology: false, 24 | useNewUrlParser: false, 25 | }, 26 | }, 27 | 28 | http: { 29 | port: parseInt((process.env.HTTP_PORT ?? '3000'), 10), 30 | }, 31 | 32 | rabbitmq: { 33 | url: [process.env.RABBITMQ_URL ?? `amqp://${process.env.RABBITMQ_USER ?? 'guest'}:${process.env.RABBITMQ_PASS ?? 'guest'}@${process.env.RABBITMQ_HOST ?? 'localhost'}:5672${process.env.RABBITMQ_VHOST ?? ''}?heartbeat=10&connection_timeout=10000`], 34 | queue: process.env.RABBITMQ_QUEUE ?? 'api-queue', 35 | queueOptions: { 36 | durable: true, 37 | messageTtl: 10800000, // 3 hours 38 | arguments: { 39 | 'x-queue-mode': 'lazy', 40 | }, 41 | }, 42 | noAck: false, 43 | prefetchCount: process.env.RABBITMQ_PREFETCH ? parseInt(process.env.RABBITMQ_PREFETCH, 10) : 2000, 44 | }, 45 | 46 | redis: { 47 | host: process.env.REDIS_HOST ?? 'ps2alerts-redis', 48 | port: process.env.REDIS_PORT ?? 6379, 49 | password: process.env.REDIS_PASS ?? undefined, 50 | db: process.env.REDIS_DB ?? 1, 51 | }, 52 | 53 | internalApiAuth: { 54 | username: process.env.INTERNAL_API_USER ?? undefined, 55 | password: process.env.INTERNAL_API_PASS ?? undefined, 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /src/controllers/default.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get} from '@nestjs/common'; 2 | 3 | @Controller() 4 | export class DefaultController { 5 | @Get('/hello') 6 | getHello(): string { 7 | return 'Hello there, General Kenobi'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/filters/type-orm.filter.ts: -------------------------------------------------------------------------------- 1 | import {BaseExceptionFilter} from '@nestjs/core'; 2 | import {Catch, NotFoundException} from '@nestjs/common'; 3 | import {EntityNotFoundError} from 'typeorm'; 4 | 5 | @Catch(EntityNotFoundError) 6 | export class TypeOrmFilter extends BaseExceptionFilter { 7 | catch(): void { 8 | throw new NotFoundException(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/instrument.ts: -------------------------------------------------------------------------------- 1 | // Import with `const Sentry = require("@sentry/node");` if you are using CJS 2 | import * as Sentry from '@sentry/node'; 3 | import {nodeProfilingIntegration} from '@sentry/profiling-node'; 4 | import {browserTracingIntegration} from '@sentry/browser'; 5 | 6 | Sentry.init({ 7 | dsn: 'https://09b6521e230a7a987e3ca046f774760c@o4507319323262976.ingest.de.sentry.io/4507319628922960', 8 | environment: process.env.NODE_ENV, 9 | release: process.env.VERSION, 10 | autoSessionTracking: true, 11 | integrations: [ 12 | nodeProfilingIntegration(), 13 | browserTracingIntegration(), 14 | ], 15 | // Performance Monitoring 16 | tracesSampleRate: 1.0, // Capture 100% of the transactions 17 | tracePropagationTargets: ['localhost', /^https:\/\/api.ps2alerts\.com\/.*/], 18 | 19 | // Set sampling rate for profiling - this is relative to tracesSampleRate 20 | profilesSampleRate: 1.0, 21 | }); 22 | -------------------------------------------------------------------------------- /src/interfaces/InternalApiAuthConfigInterface.ts: -------------------------------------------------------------------------------- 1 | export interface InternalApiAuthConfigInterface { 2 | username: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {NestFactory, BaseExceptionFilter, HttpAdapterHost} from '@nestjs/core'; 2 | import {FastifyAdapter, NestFastifyApplication} from '@nestjs/platform-fastify'; 3 | import {RmqOptions, Transport} from '@nestjs/microservices'; 4 | import {ConfigService} from '@nestjs/config'; 5 | import {AppModule} from './app.module'; 6 | import {DocumentBuilder, SwaggerModule} from '@nestjs/swagger'; 7 | import {ValidationPipe} from '@nestjs/common'; 8 | import {TypeOrmFilter} from './filters/type-orm.filter'; 9 | import compression from '@fastify/compress'; 10 | import {fastifyHelmet} from '@fastify/helmet'; 11 | import './instrument.js'; 12 | import * as Sentry from '@sentry/node'; 13 | 14 | async function bootstrap(): Promise { 15 | const app = await NestFactory.create( 16 | AppModule, 17 | new FastifyAdapter({ 18 | logger: process.env.NODE_ENV === 'development', 19 | }), 20 | { 21 | logger: process.env.NODE_ENV === 'development' ? ['debug', 'log', 'warn', 'error'] : ['log', 'warn', 'error'], 22 | }, 23 | ); 24 | 25 | // Sentry stuff 26 | const {httpAdapter} = app.get(HttpAdapterHost); 27 | Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter)); 28 | 29 | const config = app.get(ConfigService); 30 | 31 | // Fastify stuff 32 | app.enableCors(config.get('config')); 33 | void app.register(fastifyHelmet, { 34 | contentSecurityPolicy: { 35 | directives: { 36 | defaultSrc: ['\'self\''], 37 | styleSrc: ['\'self\'', '\'unsafe-inline\''], 38 | imgSrc: ['\'self\'', 'data:', 'validator.swagger.io'], 39 | scriptSrc: ['\'self\'', 'https: \'unsafe-inline\''], 40 | }, 41 | }, 42 | }); 43 | void app.register(compression, {encodings: ['gzip', 'deflate']}); 44 | 45 | // Type ORM stuff 46 | app.useGlobalFilters(new TypeOrmFilter()); 47 | app.useGlobalPipes(new ValidationPipe({whitelist: true, transform: true, disableErrorMessages: false})); 48 | 49 | if (process.env.AGGREGATOR_ENABLED === 'true') { 50 | app.connectMicroservice({ 51 | transport: Transport.RMQ, 52 | options: { 53 | urls: config.get('rabbitmq.url'), 54 | queue: config.get('rabbitmq.queue'), 55 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 56 | queueOptions: config.get('rabbitmq.queueOptions'), 57 | noAck: config.get('rabbitmq.noAck'), 58 | prefetchCount: config.get('rabbitmq.prefetchCount'), 59 | }, 60 | }); 61 | } 62 | 63 | // Swagger stuff 64 | const options = new DocumentBuilder() 65 | .setTitle('PS2Alerts API') 66 | .setDescription('PS2Alerts API. Please visit our GitHub project for more information or to support the project.

There are the following limits applied to this API:
  1. You are limited to taking 1000 maximum documents from any */global/* endpoint, you must then paginate thereafter. */instance/* endpoints don\'t have such limitations.
  2. There are currently no rate limits, however requests are being monitored and any abuse will result in rate limit implementation.
') 67 | .setVersion(process.env.VERSION ?? 'UNKNOWN') 68 | .addBasicAuth() 69 | .build(); 70 | const document = SwaggerModule.createDocument(app, options); 71 | SwaggerModule.setup('/', app, document); 72 | 73 | // Connects to Rabbit etc 74 | void app.startAllMicroservices(); 75 | 76 | await app.listen(config.get('http.port') ?? 3000, '0.0.0.0'); 77 | } 78 | 79 | void bootstrap(); 80 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/global/aggregator.global.character.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Logger} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import GlobalCharacterAggregateEntity from '../../../../data/entities/aggregate/global/global.character.aggregate.entity'; 5 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 6 | import GlobalAggregatorMessageInterface from '../../../interfaces/global.aggregator.message.interface'; 7 | 8 | @Controller() 9 | export default class AggregatorGlobalCharacterAggregateController { 10 | private readonly logger = new Logger(AggregatorGlobalCharacterAggregateController.name); 11 | 12 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 13 | 14 | @EventPattern(MqAcceptedPatterns.GLOBAL_CHARACTER_AGGREGATE) 15 | public async process(@Payload() data: GlobalAggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 16 | try { 17 | await this.aggregatorDataHandler.upsertGlobal( 18 | await this.aggregatorDataHandler.transformGlobal(data), 19 | context, 20 | GlobalCharacterAggregateEntity, 21 | ); 22 | } catch (e) { 23 | if (e instanceof Error && !e.message.includes('does not exist')) { 24 | this.logger.error(`Unable to process ${MqAcceptedPatterns.GLOBAL_CHARACTER_AGGREGATE} message for instance ${data.instance}! Error: ${e.message}`); 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access 28 | await context.getChannelRef().ack(context.getMessage()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/global/aggregator.global.facility.control.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Logger} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import GlobalFacilityControlAggregateEntity from '../../../../data/entities/aggregate/global/global.facility.control.aggregate.entity'; 5 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 6 | import GlobalAggregatorMessageInterface from '../../../interfaces/global.aggregator.message.interface'; 7 | 8 | @Controller() 9 | export default class AggregatorGlobalFacilityControlAggregateController { 10 | private readonly logger = new Logger(AggregatorGlobalFacilityControlAggregateController.name); 11 | 12 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 13 | 14 | @EventPattern(MqAcceptedPatterns.GLOBAL_FACILITY_CONTROL_AGGREGATE) 15 | public async process(@Payload() data: GlobalAggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 16 | try { 17 | await this.aggregatorDataHandler.upsertGlobal( 18 | await this.aggregatorDataHandler.transformGlobal(data), 19 | context, 20 | GlobalFacilityControlAggregateEntity, 21 | ); 22 | } catch (e) { 23 | if (e instanceof Error && !e.message.includes('does not exist')) { 24 | this.logger.error(`Unable to process ${MqAcceptedPatterns.GLOBAL_FACILITY_CONTROL_AGGREGATE} message for instance ${data.instance}! Error: ${e.message}`); 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access 28 | await context.getChannelRef().ack(context.getMessage()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/global/aggregator.global.faction.combat.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Logger} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import GlobalFactionCombatAggregateEntity from '../../../../data/entities/aggregate/global/global.faction.combat.aggregate.entity'; 5 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 6 | import GlobalAggregatorMessageInterface from '../../../interfaces/global.aggregator.message.interface'; 7 | 8 | @Controller() 9 | export default class AggregatorGlobalFactionCombatAggregateController { 10 | private readonly logger = new Logger(AggregatorGlobalFactionCombatAggregateController.name); 11 | 12 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 13 | 14 | @EventPattern(MqAcceptedPatterns.GLOBAL_FACTION_COMBAT_AGGREGATE) 15 | public async process(@Payload() data: GlobalAggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 16 | try { 17 | await this.aggregatorDataHandler.upsertGlobal( 18 | await this.aggregatorDataHandler.transformGlobal(data), 19 | context, 20 | GlobalFactionCombatAggregateEntity, 21 | ); 22 | } catch (e) { 23 | if (e instanceof Error && !e.message.includes('does not exist')) { 24 | this.logger.error(`Unable to process ${MqAcceptedPatterns.GLOBAL_FACTION_COMBAT_AGGREGATE} message for instance ${data.instance}! Error: ${e.message}`); 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access 28 | await context.getChannelRef().ack(context.getMessage()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/global/aggregator.global.loadout.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Logger} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import GlobalLoadoutAggregateEntity from '../../../../data/entities/aggregate/global/global.loadout.aggregate.entity'; 5 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 6 | import GlobalAggregatorMessageInterface from '../../../interfaces/global.aggregator.message.interface'; 7 | 8 | @Controller() 9 | export default class AggregatorGlobalLoadoutAggregateController { 10 | private readonly logger = new Logger(AggregatorGlobalLoadoutAggregateController.name); 11 | 12 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 13 | 14 | @EventPattern(MqAcceptedPatterns.GLOBAL_LOADOUT_AGGREGATE) 15 | public async process(@Payload() data: GlobalAggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 16 | try { 17 | await this.aggregatorDataHandler.upsertGlobal( 18 | await this.aggregatorDataHandler.transformGlobal(data), 19 | context, 20 | GlobalLoadoutAggregateEntity, 21 | ); 22 | } catch (e) { 23 | if (e instanceof Error && !e.message.includes('does not exist')) { 24 | this.logger.error(`Unable to process ${MqAcceptedPatterns.GLOBAL_LOADOUT_AGGREGATE} message for instance ${data.instance}! Error: ${e.message}`); 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access 28 | await context.getChannelRef().ack(context.getMessage()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/global/aggregator.global.outfit.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Logger} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import GlobalOutfitAggregateEntity from '../../../../data/entities/aggregate/global/global.outfit.aggregate.entity'; 5 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 6 | import GlobalAggregatorMessageInterface from '../../../interfaces/global.aggregator.message.interface'; 7 | 8 | @Controller() 9 | export default class AggregatorGlobalOutfitAggregateController { 10 | private readonly logger = new Logger(AggregatorGlobalOutfitAggregateController.name); 11 | 12 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 13 | 14 | @EventPattern(MqAcceptedPatterns.GLOBAL_OUTFIT_AGGREGATE) 15 | public async process(@Payload() data: GlobalAggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 16 | try { 17 | await this.aggregatorDataHandler.upsertGlobal( 18 | await this.aggregatorDataHandler.transformGlobal(data), 19 | context, 20 | GlobalOutfitAggregateEntity, 21 | ); 22 | } catch (e) { 23 | if (e instanceof Error && !e.message.includes('does not exist')) { 24 | this.logger.error(`Unable to process ${MqAcceptedPatterns.GLOBAL_OUTFIT_AGGREGATE} message for instance ${data.instance}! Error: ${e.message}`); 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access 28 | await context.getChannelRef().ack(context.getMessage()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/global/aggregator.global.vehicle.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Logger} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 5 | import GlobalVehicleAggregateEntity from '../../../../data/entities/aggregate/global/global.vehicle.aggregate.entity'; 6 | import GlobalAggregatorMessageInterface from '../../../interfaces/global.aggregator.message.interface'; 7 | 8 | @Controller() 9 | export default class AggregatorGlobalVehicleAggregateController { 10 | private readonly logger = new Logger(AggregatorGlobalVehicleAggregateController.name); 11 | 12 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 13 | 14 | @EventPattern(MqAcceptedPatterns.GLOBAL_VEHICLE_AGGREGATE) 15 | public async process(@Payload() data: GlobalAggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 16 | try { 17 | await this.aggregatorDataHandler.upsertGlobal( 18 | await this.aggregatorDataHandler.transformGlobal(data), 19 | context, 20 | GlobalVehicleAggregateEntity, 21 | ); 22 | } catch (e) { 23 | if (e instanceof Error && !e.message.includes('does not exist')) { 24 | this.logger.error(`Unable to process ${MqAcceptedPatterns.GLOBAL_VEHICLE_AGGREGATE} message for instance ${data.instance}! Error: ${e.message}`); 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access 28 | await context.getChannelRef().ack(context.getMessage()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/global/aggregator.global.vehicle.character.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Logger} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 5 | import GlobalVehicleCharacterAggregateEntity from '../../../../data/entities/aggregate/global/global.vehicle.character.aggregate.entity'; 6 | import GlobalAggregatorMessageInterface from '../../../interfaces/global.aggregator.message.interface'; 7 | 8 | @Controller() 9 | export default class AggregatorGlobalVehicleCharacterAggregateController { 10 | private readonly logger = new Logger(AggregatorGlobalVehicleCharacterAggregateController.name); 11 | 12 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 13 | 14 | @EventPattern(MqAcceptedPatterns.GLOBAL_VEHICLE_CHARACTER_AGGREGATE) 15 | public async process(@Payload() data: GlobalAggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 16 | try { 17 | await this.aggregatorDataHandler.upsertGlobal( 18 | await this.aggregatorDataHandler.transformGlobal(data), 19 | context, 20 | GlobalVehicleCharacterAggregateEntity, 21 | ); 22 | } catch (e) { 23 | if (e instanceof Error && !e.message.includes('does not exist')) { 24 | this.logger.error(`Unable to process ${MqAcceptedPatterns.GLOBAL_VEHICLE_CHARACTER_AGGREGATE} message for instance ${data.instance}! Error: ${e.message}`); 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access 28 | await context.getChannelRef().ack(context.getMessage()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/global/aggregator.global.victory.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Logger} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 5 | import GlobalVictoryAggregateEntity from '../../../../data/entities/aggregate/global/global.victory.aggregate.entity'; 6 | import GlobalAggregatorMessageInterface from '../../../interfaces/global.aggregator.message.interface'; 7 | 8 | @Controller() 9 | export default class AggregatorGlobalVictoryAggregateController { 10 | private readonly logger = new Logger(AggregatorGlobalVictoryAggregateController.name); 11 | 12 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 13 | 14 | @EventPattern(MqAcceptedPatterns.GLOBAL_VICTORY_AGGREGATE) 15 | public async process(@Payload() data: GlobalAggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 16 | try { 17 | await this.aggregatorDataHandler.upsertGlobal( 18 | await this.aggregatorDataHandler.transformGlobal(data), 19 | context, 20 | GlobalVictoryAggregateEntity, 21 | ); 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | } catch (err: any) { 24 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/restrict-template-expressions 25 | this.logger.error(`Unable to process ${MqAcceptedPatterns.GLOBAL_VICTORY_AGGREGATE} message for instance ${data.instance}! Error: ${err.message}`); 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access 28 | await context.getChannelRef().ack(context.getMessage()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/global/aggregator.global.weapon.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Logger} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import GlobalWeaponAggregateEntity from '../../../../data/entities/aggregate/global/global.weapon.aggregate.entity'; 5 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 6 | import GlobalAggregatorMessageInterface from '../../../interfaces/global.aggregator.message.interface'; 7 | 8 | @Controller() 9 | export default class AggregatorGlobalWeaponAggregateController { 10 | private readonly logger = new Logger(AggregatorGlobalWeaponAggregateController.name); 11 | 12 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 13 | 14 | @EventPattern(MqAcceptedPatterns.GLOBAL_WEAPON_AGGREGATE) 15 | public async process(@Payload() data: GlobalAggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 16 | try { 17 | await this.aggregatorDataHandler.upsertGlobal( 18 | await this.aggregatorDataHandler.transformGlobal(data), 19 | context, 20 | GlobalWeaponAggregateEntity, 21 | ); 22 | } catch (e) { 23 | if (e instanceof Error && !e.message.includes('does not exist')) { 24 | this.logger.error(`Unable to process ${MqAcceptedPatterns.GLOBAL_WEAPON_AGGREGATE} message for instance ${data.instance}! Error: ${e.message}`); 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access 28 | await context.getChannelRef().ack(context.getMessage()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/instance/aggregator.instance.character.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorMessageInterface from '../../../interfaces/aggregator.message.interface'; 5 | import InstanceCharacterAggregateEntity from '../../../../data/entities/aggregate/instance/instance.character.aggregate.entity'; 6 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 7 | 8 | @Controller() 9 | export default class AggregatorInstanceCharacterAggregateController { 10 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 11 | 12 | @EventPattern(MqAcceptedPatterns.INSTANCE_CHARACTER_AGGREGATE) 13 | public async process(@Payload() data: AggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 14 | await this.aggregatorDataHandler.upsert(data, context, InstanceCharacterAggregateEntity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/instance/aggregator.instance.facility.control.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorMessageInterface from '../../../interfaces/aggregator.message.interface'; 5 | import InstanceFacilityControlAggregateEntity from '../../../../data/entities/aggregate/instance/instance.facility.control.aggregate.entity'; 6 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 7 | 8 | @Controller() 9 | export default class AggregatorInstanceFacilityControlAggregateController { 10 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 11 | 12 | @EventPattern(MqAcceptedPatterns.INSTANCE_FACILITY_CONTROL_AGGREGATE) 13 | public async process(@Payload() data: AggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 14 | await this.aggregatorDataHandler.upsert(data, context, InstanceFacilityControlAggregateEntity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/instance/aggregator.instance.faction.combat.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorMessageInterface from '../../../interfaces/aggregator.message.interface'; 5 | import InstanceFactionCombatAggregateEntity from '../../../../data/entities/aggregate/instance/instance.faction.combat.aggregate.entity'; 6 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 7 | 8 | @Controller() 9 | export default class AggregatorInstanceFactionCombatAggregateController { 10 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 11 | 12 | @EventPattern(MqAcceptedPatterns.INSTANCE_FACTION_COMBAT_AGGREGATE) 13 | public async process(@Payload() data: AggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 14 | await this.aggregatorDataHandler.upsert(data, context, InstanceFactionCombatAggregateEntity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/instance/aggregator.instance.loadout.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorMessageInterface from '../../../interfaces/aggregator.message.interface'; 5 | import InstanceLoadoutAggregateEntity from '../../../../data/entities/aggregate/instance/instance.loadout.aggregate.entity'; 6 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 7 | 8 | @Controller() 9 | export default class AggregatorInstanceLoadoutAggregateController { 10 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 11 | 12 | @EventPattern(MqAcceptedPatterns.INSTANCE_LOADOUT_AGGREGATE) 13 | public async process(@Payload() data: AggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 14 | await this.aggregatorDataHandler.upsert(data, context, InstanceLoadoutAggregateEntity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/instance/aggregator.instance.outfit.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorMessageInterface from '../../../interfaces/aggregator.message.interface'; 5 | import InstanceOutfitAggregateEntity from '../../../../data/entities/aggregate/instance/instance.outfit.aggregate.entity'; 6 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 7 | 8 | @Controller() 9 | export default class AggregatorInstanceOutfitAggregateController { 10 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 11 | 12 | @EventPattern(MqAcceptedPatterns.INSTANCE_OUTFIT_AGGREGATE) 13 | public async process(@Payload() data: AggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 14 | await this.aggregatorDataHandler.upsert(data, context, InstanceOutfitAggregateEntity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/instance/aggregator.instance.population.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorMessageInterface from '../../../interfaces/aggregator.message.interface'; 5 | import InstancePopulationAggregateEntity from '../../../../data/entities/aggregate/instance/instance.population.aggregate.entity'; 6 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 7 | 8 | @Controller() 9 | export default class AggregatorInstancePopulationAggregateController { 10 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 11 | 12 | @EventPattern(MqAcceptedPatterns.INSTANCE_POPULATION_AGGREGATE) 13 | public async process(@Payload() data: AggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 14 | await this.aggregatorDataHandler.create(data, context, InstancePopulationAggregateEntity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/instance/aggregator.instance.vehicle.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorMessageInterface from '../../../interfaces/aggregator.message.interface'; 5 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 6 | import InstanceVehicleAggregateEntity from '../../../../data/entities/aggregate/instance/instance.vehicle.aggregate.entity'; 7 | 8 | @Controller() 9 | export default class AggregatorInstanceVehicleAggregateController { 10 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 11 | 12 | @EventPattern(MqAcceptedPatterns.INSTANCE_VEHICLE_AGGREGATE) 13 | public async process(@Payload() data: AggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 14 | await this.aggregatorDataHandler.upsert(data, context, InstanceVehicleAggregateEntity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/instance/aggregator.instance.vehicle.character.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorMessageInterface from '../../../interfaces/aggregator.message.interface'; 5 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 6 | import InstanceVehicleCharacterAggregateEntity from '../../../../data/entities/aggregate/instance/instance.vehicle.character.aggregate.entity'; 7 | 8 | @Controller() 9 | export default class AggregatorInstanceVehicleCharacterAggregateControllerAggregateController { 10 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 11 | 12 | @EventPattern(MqAcceptedPatterns.INSTANCE_VEHICLE_CHARACTER_AGGREGATE) 13 | public async process(@Payload() data: AggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 14 | await this.aggregatorDataHandler.upsert(data, context, InstanceVehicleCharacterAggregateEntity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/aggregates/instance/aggregator.instance.weapon.aggregate.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import {MqAcceptedPatterns} from '../../../../data/ps2alerts-constants/mqAcceptedPatterns'; 4 | import AggregatorMessageInterface from '../../../interfaces/aggregator.message.interface'; 5 | import InstanceWeaponAggregateEntity from '../../../../data/entities/aggregate/instance/instance.weapon.aggregate.entity'; 6 | import AggregatorDataHandler from '../../../aggregator.data.handler'; 7 | 8 | @Controller() 9 | export default class AggregatorInstanceWeaponAggregateController { 10 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 11 | 12 | @EventPattern(MqAcceptedPatterns.INSTANCE_WEAPON_AGGREGATE) 13 | public async process(@Payload() data: AggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 14 | await this.aggregatorDataHandler.upsert(data, context, InstanceWeaponAggregateEntity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/events/aggregator.instance.death.event.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import InstanceDeathEntity from '../../../data/entities/instance/instance.death.entity'; 4 | import AggregatorMessageInterface from '../../interfaces/aggregator.message.interface'; 5 | import {MqAcceptedPatterns} from '../../../data/ps2alerts-constants/mqAcceptedPatterns'; 6 | import AggregatorDataHandler from '../../aggregator.data.handler'; 7 | 8 | @Controller() 9 | export default class AggregatorInstanceDeathEventController { 10 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 11 | 12 | @EventPattern(MqAcceptedPatterns.INSTANCE_DEATH) 13 | public async process(@Payload() data: AggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 14 | await this.aggregatorDataHandler.create(data, context, InstanceDeathEntity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/aggregator/controllers/events/aggregator.instance.facility.control.event.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Ctx, EventPattern, Payload, RmqContext} from '@nestjs/microservices'; 3 | import AggregatorMessageInterface from '../../interfaces/aggregator.message.interface'; 4 | import {MqAcceptedPatterns} from '../../../data/ps2alerts-constants/mqAcceptedPatterns'; 5 | import InstanceFacilityControlEntity from '../../../data/entities/instance/instance.facilitycontrol.entity'; 6 | import AggregatorDataHandler from '../../aggregator.data.handler'; 7 | 8 | @Controller() 9 | export default class AggregatorInstanceFacilityControlEventController { 10 | constructor(private readonly aggregatorDataHandler: AggregatorDataHandler) {} 11 | 12 | @EventPattern(MqAcceptedPatterns.INSTANCE_FACILITY_CONTROL) 13 | public async process(@Payload() data: AggregatorMessageInterface, @Ctx() context: RmqContext): Promise { 14 | await this.aggregatorDataHandler.create(data, context, InstanceFacilityControlEntity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/aggregator/interfaces/aggregator.message.interface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export default interface AggregatorMessageInterface { 3 | docs: any[]; 4 | conditionals: any[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/aggregator/interfaces/global.aggregator.message.interface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import {Bracket} from '../../data/ps2alerts-constants/bracket'; 3 | 4 | export default interface GlobalAggregatorMessageInterface { 5 | instance: string; 6 | bracket?: Bracket; 7 | docs: any[]; 8 | conditionals: any[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/cron/CronModule.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | import {CacheModule, Module} from '@nestjs/common'; 3 | import {HttpModule} from '@nestjs/axios'; 4 | import {CombatHistoryCron} from './combat.history.cron'; 5 | import MongoOperationsService from '../../services/mongo/mongo.operations.service'; 6 | import {TypeOrmModule} from '@nestjs/typeorm'; 7 | import InstanceMetagameTerritoryEntity from '../data/entities/instance/instance.metagame.territory.entity'; 8 | import InstanceFactionCombatAggregateEntity 9 | from '../data/entities/aggregate/instance/instance.faction.combat.aggregate.entity'; 10 | import InstanceCombatHistoryAggregateEntity 11 | from '../data/entities/aggregate/instance/instance.combat.history.aggregate.entity'; 12 | import {BracketCron} from './bracket.cron'; 13 | import ConfigModule from '../../config/config.module'; 14 | import {ConfigService} from '@nestjs/config'; 15 | import * as redisStore from 'cache-manager-ioredis'; 16 | import {RedisCacheService} from '../../services/cache/redis.cache.service'; 17 | import {XpmCron} from './xpm.cron'; 18 | // import {OutfitWarsRankingsCron} from './outfitwars.rankings.cron'; 19 | // import OutfitwarsRankingEntity from '../data/entities/instance/outfitwars.ranking.entity'; 20 | 21 | @Module({ 22 | imports: [ 23 | HttpModule, 24 | TypeOrmModule.forFeature([ 25 | InstanceCombatHistoryAggregateEntity, 26 | InstanceFactionCombatAggregateEntity, 27 | InstanceMetagameTerritoryEntity, 28 | // OutfitwarsRankingEntity, 29 | ]), 30 | CacheModule.registerAsync({ 31 | imports: [ConfigModule], 32 | inject: [ConfigService], 33 | useFactory: (config: ConfigService) => { 34 | return { 35 | store: redisStore, 36 | host: config.get('redis.host'), 37 | port: config.get('redis.port'), 38 | db: config.get('redis.db'), 39 | password: config.get('redis.password'), 40 | }; 41 | }, 42 | }), 43 | ], 44 | providers: [ 45 | MongoOperationsService, 46 | RedisCacheService, 47 | CombatHistoryCron, 48 | BracketCron, 49 | // OutfitWarsRankingsCron, 50 | XpmCron, 51 | ], 52 | }) 53 | export class CronModule {} 54 | -------------------------------------------------------------------------------- /src/modules/cron/combat.history.cron.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable, Logger} from '@nestjs/common'; 2 | import {Cron, CronExpression} from '@nestjs/schedule'; 3 | import MongoOperationsService from '../../services/mongo/mongo.operations.service'; 4 | import {Ps2AlertsEventState} from '../data/ps2alerts-constants/ps2AlertsEventState'; 5 | import InstanceMetagameTerritoryEntity from '../data/entities/instance/instance.metagame.territory.entity'; 6 | import InstanceFactionCombatAggregateEntity 7 | from '../data/entities/aggregate/instance/instance.faction.combat.aggregate.entity'; 8 | import InstanceCombatHistoryAggregateEntity 9 | from '../data/entities/aggregate/instance/instance.combat.history.aggregate.entity'; 10 | import {RedisCacheService} from '../../services/cache/redis.cache.service'; 11 | import InstanceOutfitWarsTerritoryEntity from '../data/entities/instance/instance.outfitwars.territory.entity'; 12 | 13 | @Injectable() 14 | export class CombatHistoryCron { 15 | private readonly logger = new Logger(CombatHistoryCron.name); 16 | constructor( 17 | @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, 18 | private readonly cacheService: RedisCacheService, 19 | ) {} 20 | 21 | @Cron(CronExpression.EVERY_MINUTE) 22 | async handleCron(): Promise { 23 | this.logger.log('Running Combat History job'); 24 | 25 | const documents = []; 26 | 27 | // Grab the current actives 28 | const territoryActives: InstanceMetagameTerritoryEntity[] = await this.mongoOperationsService.findMany(InstanceMetagameTerritoryEntity, {state: Ps2AlertsEventState.STARTED}); 29 | const outfitWarsActives: InstanceOutfitWarsTerritoryEntity[] = await this.mongoOperationsService.findMany(InstanceOutfitWarsTerritoryEntity, {state: Ps2AlertsEventState.STARTED}); 30 | 31 | const combinedActives = [...territoryActives, ...outfitWarsActives]; 32 | 33 | for await (const row of combinedActives) { 34 | // If instance is overdue, don't process 35 | if (Date.now() > (row.timeStarted.getTime() + row.duration)) { 36 | this.logger.warn(`Instance [${row.instanceId}] is overdue, skipping combat history job`); 37 | continue; 38 | } 39 | 40 | // Pull latest faction combat entity 41 | try { 42 | const factionCombat: InstanceFactionCombatAggregateEntity = await this.mongoOperationsService.findOne( 43 | InstanceFactionCombatAggregateEntity, 44 | {instance: row.instanceId}, 45 | ); 46 | delete factionCombat._id; 47 | 48 | documents.push(Object.assign( 49 | factionCombat, 50 | { 51 | timestamp: new Date(), 52 | }, 53 | )); 54 | 55 | this.logger.log(`Updated combat history for instance ${row.instanceId}`); 56 | } catch (e) { 57 | // Ignore error if there isn't any 58 | } 59 | } 60 | 61 | if (documents.length > 0) { 62 | await this.mongoOperationsService.insertMany( 63 | InstanceCombatHistoryAggregateEntity, 64 | documents, 65 | ); 66 | } 67 | 68 | // @See CronHealthIndicator 69 | // This sets the fact that the cron has run, so if it hasn't been run it will be terminated. 70 | const key = '/crons/combatHistory'; 71 | await this.cacheService.set(key, Date.now(), 65); // 65 seconds = deadline for this cron 72 | this.logger.debug('Set combat cron run time'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/modules/data/data.module.ts: -------------------------------------------------------------------------------- 1 | import {Module} from '@nestjs/common'; 2 | 3 | @Module({ 4 | imports: [], 5 | controllers: [], 6 | providers: [], 7 | }) 8 | export class DataModule {} 9 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/character.embed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | import {Faction} from '../../../ps2alerts-constants/faction'; 5 | import {World} from '../../../ps2alerts-constants/world'; 6 | import OutfitEmbed from './outfit.embed'; 7 | 8 | export default class CharacterEmbed { 9 | @ApiProperty({example: '5428010618035323201', description: 'Character Census ID'}) 10 | @Column({ 11 | type: 'string', 12 | }) 13 | id: string; 14 | 15 | @ApiProperty({example: 'Maelstrome26', description: 'Character name'}) 16 | @Column({ 17 | type: 'string', 18 | }) 19 | name: string; 20 | 21 | @ApiProperty({example: 1, description: 'Character faction'}) 22 | @Column({ 23 | type: 'number', 24 | }) 25 | faction: Faction; 26 | 27 | @ApiProperty({example: 10, description: 'Character world ID'}) 28 | @Column({ 29 | type: 'number', 30 | }) 31 | world: World; 32 | 33 | @ApiProperty({example: 120, description: 'Character Battle Rank'}) 34 | @Column({ 35 | type: 'number', 36 | }) 37 | battleRank: number; 38 | 39 | @ApiProperty({example: 1, description: 'Character ASP Rank'}) 40 | @Column({ 41 | type: 'number', 42 | default: 0, 43 | }) 44 | asp = 0; 45 | 46 | @ApiProperty({example: 210, description: 'Character Adjusted Battle Rank'}) 47 | @Column({ 48 | type: 'number', 49 | }) 50 | adjustedBattleRank: number; 51 | 52 | @ApiProperty({type: OutfitEmbed, description: 'Character outfit information'}) 53 | @Column(() => OutfitEmbed) 54 | outfit: OutfitEmbed | null; 55 | } 56 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/combat.stats.embed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | 5 | export default class CombatStatsEmbed { 6 | @ApiProperty({example: 22, description: 'Total number of kills'}) 7 | @Column({ 8 | type: 'number', 9 | }) 10 | kills: number; 11 | 12 | @ApiProperty({example: 18, description: 'Total number of deaths'}) 13 | @Column({ 14 | type: 'number', 15 | }) 16 | deaths: number; 17 | 18 | @ApiProperty({example: 3, description: 'Total number of team kills'}) 19 | @Column({ 20 | type: 'number', 21 | }) 22 | teamKills: number; 23 | 24 | @ApiProperty({example: 2, description: 'Total number of suicides'}) 25 | @Column({ 26 | type: 'number', 27 | }) 28 | suicides: number; 29 | 30 | @ApiProperty({example: 15, description: 'Total number of headshots'}) 31 | @Column({ 32 | type: 'number', 33 | }) 34 | headshots: number; 35 | } 36 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/facility.embed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | import {Zone} from '../../../ps2alerts-constants/zone'; 5 | 6 | export default class FacilityEmbed { 7 | @ApiProperty({example: '6200', description: 'Facility ID'}) 8 | @Column({ 9 | type: 'string', 10 | }) 11 | id: string; 12 | 13 | @ApiProperty({example: 'The Crown', description: 'Facility name'}) 14 | @Column({ 15 | type: 'string', 16 | }) 17 | name: string; 18 | 19 | @ApiProperty({example: 5, description: 'Facility type id'}) 20 | @Column({ 21 | type: 'number', 22 | }) 23 | type: number; 24 | 25 | @ApiProperty({example: 2, description: 'Zone ID'}) 26 | @Column({ 27 | type: 'number', 28 | }) 29 | zone: Zone; 30 | 31 | @ApiProperty({example: 2306, description: 'Facility map region ID'}) 32 | @Column({ 33 | type: 'number', 34 | }) 35 | region: number; 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/facility.faction.control.embed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | import FactionVSVersusFactionEmbed from './factionvsfaction/FactionVSVersusFactionEmbed'; 5 | 6 | export default class FacilityFactionControlEmbed { 7 | @ApiProperty({example: 3, description: 'Number of Facility captures'}) 8 | @Column({ 9 | type: 'number', 10 | default: 0, 11 | }) 12 | captures: number; 13 | 14 | @ApiProperty({example: 1, description: 'Number of Facility defences'}) 15 | @Column({ 16 | type: 'number', 17 | default: 0, 18 | }) 19 | defences: number; 20 | 21 | @ApiProperty({ 22 | type: FactionVSVersusFactionEmbed, 23 | description: 'Facilities taken from other factions', 24 | example: {nc: 123, tr: 123}, 25 | }) 26 | @Column(() => FactionVSVersusFactionEmbed) 27 | takenFrom: FactionVSVersusFactionEmbed; 28 | 29 | @ApiProperty({ 30 | type: FactionVSVersusFactionEmbed, 31 | description: 'Facilities lost to other factions', 32 | example: {nc: 123, tr: 123}, 33 | }) 34 | @Column(() => FactionVSVersusFactionEmbed) 35 | lostTo: FactionVSVersusFactionEmbed; 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/faction.versus.faction.embed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | import FactionVSVersusFactionEmbed from './factionvsfaction/FactionVSVersusFactionEmbed'; 5 | import FactionNCVersusFactionEmbed from './factionvsfaction/FactionNCVersusFactionEmbed'; 6 | import FactionTRVersusFactionEmbed from './factionvsfaction/FactionTRVersusFactionEmbed'; 7 | import FactionNSOVersusFactionEmbed from './factionvsfaction/FacitonNSOVersionFactionEmbed'; 8 | 9 | export default class FactionVersusFactionEmbed { 10 | // For some reason the examples aren't pulled out of embeds of embeds 11 | @ApiProperty({type: FactionVSVersusFactionEmbed, example: {nc: 123, tr: 123, nso: 123}, description: 'Kills made by VS against other factions'}) 12 | @Column(() => FactionVSVersusFactionEmbed) 13 | vs: FactionVSVersusFactionEmbed; 14 | 15 | @ApiProperty({type: FactionNCVersusFactionEmbed, example: {vs: 123, tr: 123, nso: 123}, description: 'Kills made by NC against other factions'}) 16 | @Column(() => FactionNCVersusFactionEmbed) 17 | nc: FactionNCVersusFactionEmbed; 18 | 19 | @ApiProperty({type: FactionTRVersusFactionEmbed, example: {vs: 123, nc: 123, nso: 123}, description: 'Kills made by TR against other factions'}) 20 | @Column(() => FactionTRVersusFactionEmbed) 21 | tr: FactionTRVersusFactionEmbed; 22 | 23 | @ApiProperty({type: FactionNSOVersusFactionEmbed, example: {vs: 123, nc: 123, tr: 123}, description: 'Kills made by NSO against other factions'}) 24 | @Column(() => FactionNSOVersusFactionEmbed) 25 | nso: FactionNSOVersusFactionEmbed; 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/factionvsfaction/FacitonNSOVersionFactionEmbed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | 5 | export default class FactionNSOVersusFactionEmbed { 6 | @ApiProperty({example: 123, description: 'Count for VS'}) 7 | @Column({ 8 | type: 'number', 9 | }) 10 | vs: number; 11 | 12 | @ApiProperty({example: 123, description: 'Count for NC'}) 13 | @Column({ 14 | type: 'number', 15 | }) 16 | nc: number; 17 | 18 | @ApiProperty({example: 123, description: 'Count for TR'}) 19 | @Column({ 20 | type: 'number', 21 | }) 22 | tr: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/factionvsfaction/FactionNCVersusFactionEmbed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | 5 | export default class FactionNCVersusFactionEmbed { 6 | @ApiProperty({example: 123, description: 'Count for VS'}) 7 | @Column({ 8 | type: 'number', 9 | }) 10 | vs: number; 11 | 12 | @ApiProperty({example: 123, description: 'Count for TR'}) 13 | @Column({ 14 | type: 'number', 15 | }) 16 | tr: number; 17 | 18 | @ApiProperty({example: 123, description: 'Count for NSO'}) 19 | @Column({ 20 | type: 'number', 21 | }) 22 | nso: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/factionvsfaction/FactionTRVersusFactionEmbed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | 5 | export default class FactionTRVersusFactionEmbed { 6 | @ApiProperty({example: 123, description: 'Count for VS'}) 7 | @Column({ 8 | type: 'number', 9 | }) 10 | vs: number; 11 | 12 | @ApiProperty({example: 123, description: 'Count for NC'}) 13 | @Column({ 14 | type: 'number', 15 | }) 16 | nc: number; 17 | 18 | @ApiProperty({example: 123, description: 'Count for NSO'}) 19 | @Column({ 20 | type: 'number', 21 | }) 22 | nso: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/factionvsfaction/FactionVSVersusFactionEmbed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | 5 | export default class FactionVSVersusFactionEmbed { 6 | @ApiProperty({example: 123, description: 'Count for VS'}) 7 | @Column({ 8 | type: 'number', 9 | }) 10 | vs?: number; 11 | 12 | @ApiProperty({example: 123, description: 'Count for NC'}) 13 | @Column({ 14 | type: 'number', 15 | }) 16 | nc?: number; 17 | 18 | @ApiProperty({example: 123, description: 'Count for TR'}) 19 | @Column({ 20 | type: 'number', 21 | }) 22 | tr?: number; 23 | 24 | @ApiProperty({example: 123, description: 'Count for NSO'}) 25 | @Column({ 26 | type: 'number', 27 | }) 28 | nso?: number; 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/item.embed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | import {Faction} from '../../../ps2alerts-constants/faction'; 5 | 6 | export default class ItemEmbed { 7 | @ApiProperty({example: 882, description: 'Census ID of the item'}) 8 | @Column({ 9 | type: 'number', 10 | }) 11 | id: number; 12 | 13 | @ApiProperty({example: 'Sticky Grenade', description: 'Name of the item'}) 14 | @Column({ 15 | type: 'number', 16 | }) 17 | name: number; 18 | 19 | @ApiProperty({example: 1, description: 'Faction ID of the item'}) 20 | @Column({ 21 | type: 'number', 22 | }) 23 | faction: Faction; 24 | 25 | @ApiProperty({example: 17, description: 'Category ID of the item'}) 26 | @Column({ 27 | type: 'number', 28 | }) 29 | categoryId: number; 30 | 31 | @ApiProperty({example: false, description: 'If is vehicle weapon or not'}) 32 | @Column({ 33 | type: 'boolean', 34 | }) 35 | isVehicleWeapon: boolean; 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/metagame.territory.result.embed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {Column} from 'typeorm'; 3 | import {Faction, factionArray} from '../../../ps2alerts-constants/faction'; 4 | import {ApiProperty} from '@nestjs/swagger'; 5 | 6 | export default class MetagameTerritoryResultEmbed { 7 | @ApiProperty({example: 34, description: 'VS capture percentage'}) 8 | @Column({ 9 | type: 'number', 10 | }) 11 | vs: number; 12 | 13 | @ApiProperty({example: 33, description: 'NC capture percentage'}) 14 | @Column({ 15 | type: 'number', 16 | }) 17 | nc: number; 18 | 19 | @ApiProperty({example: 33, description: 'TR capture percentage'}) 20 | @Column({ 21 | type: 'number', 22 | }) 23 | tr: number; 24 | 25 | @ApiProperty({example: 0, description: 'Percentage of bases cut off from warpgates (which don\'t contribute to faction score)'}) 26 | @Column({ 27 | type: 'number', 28 | }) 29 | cutoff: number; 30 | 31 | @ApiProperty({example: 0, description: 'Percentage of bases out of play and not cannot be captured (e.g. for underpowered alerts)'}) 32 | @Column({ 33 | type: 'number', 34 | }) 35 | outOfPlay: number; 36 | 37 | @ApiProperty({example: Faction.VANU_SOVEREIGNTY, enum: factionArray, description: 'victor of the instance. 1 = VS, 2 = NC, 3 = TR'}) 38 | @Column({ 39 | type: 'number', 40 | enum: factionArray, 41 | }) 42 | victor: Faction; 43 | 44 | @ApiProperty({example: false, description: 'If instance was a draw or not. In-game this won\'t be a draw (technically), but we represent it as one in the meta.'}) 45 | @Column({ 46 | type: 'boolean', 47 | }) 48 | draw: boolean; 49 | 50 | @ApiProperty({example: false, description: 'Per base capture worth in percentage'}) 51 | @Column({ 52 | type: 'decimal', 53 | precision: 2, 54 | scale: 2, 55 | }) 56 | perBasePercentage: number; 57 | } 58 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/outfit.embed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | import {Faction} from '../../../ps2alerts-constants/faction'; 5 | import {World} from '../../../ps2alerts-constants/world'; 6 | import {OutfitwarsOutfitDataInterface} from '../../../ps2alerts-constants/interfaces/OutfitwarsRankingInterface'; 7 | 8 | export default class OutfitEmbed implements OutfitwarsOutfitDataInterface { 9 | @ApiProperty({example: '37509488620604883', description: 'Census outfit ID'}) 10 | @Column({ 11 | type: 'string', 12 | }) 13 | id: string; 14 | 15 | @ApiProperty({example: 'Dignity of War', description: 'Outfit Name'}) 16 | @Column({ 17 | type: 'string', 18 | }) 19 | name: string; 20 | 21 | @ApiProperty({example: 1, description: 'Outfit faction ID'}) 22 | @Column({ 23 | type: 'number', 24 | }) 25 | faction: Faction; 26 | 27 | @ApiProperty({example: 10, description: 'Outfit world ID'}) 28 | @Column({ 29 | type: 'number', 30 | }) 31 | world: World; 32 | 33 | @ApiProperty({example: '8276172967445322465', description: 'Census Character ID of Leader'}) 34 | @Column({ 35 | type: 'string', 36 | }) 37 | leader: string; 38 | 39 | @ApiProperty({example: 'DIG', description: 'Outfit Tag'}) 40 | @Column({ 41 | type: 'string', 42 | }) 43 | tag: string | null; 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/outfitwars.teams.embed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {Column} from 'typeorm'; 3 | import {ApiProperty} from '@nestjs/swagger'; 4 | import OutfitEmbed from './outfit.embed'; 5 | 6 | export default class OutfitWarsTeamsEmbed { 7 | @ApiProperty({description: 'Blue team outfit info'}) 8 | @Column(() => OutfitEmbed) 9 | blue: OutfitEmbed; 10 | 11 | @ApiProperty({description: 'Red team outfit info'}) 12 | @Column(() => OutfitEmbed) 13 | red: OutfitEmbed; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/outfitwars.territory.result.embed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {Column} from 'typeorm'; 3 | import {ApiProperty} from '@nestjs/swagger'; 4 | import {Team, outfitWarsTeamArray} from '../../../ps2alerts-constants/outfitwars/team'; 5 | 6 | export default class OutfitWarsTerritoryResultEmbed { 7 | @ApiProperty({example: 33, description: 'Blue team capture percentage'}) 8 | @Column({ 9 | type: 'number', 10 | }) 11 | blue: number; 12 | 13 | @ApiProperty({example: 33, description: 'Red team capture percentage'}) 14 | @Column({ 15 | type: 'number', 16 | }) 17 | red: number; 18 | 19 | @ApiProperty({example: 33, description: 'Percentage of bases cut off from warpgates (which don\'t contribute to team score)'}) 20 | @Column({ 21 | type: 'number', 22 | }) 23 | cutoff: number; 24 | 25 | @ApiProperty({example: Team.RED, enum: outfitWarsTeamArray, description: 'Victor of the match. 1 = Red, 2 = Blue'}) 26 | @Column({ 27 | type: 'number', 28 | enum: outfitWarsTeamArray, 29 | }) 30 | victor: Team; 31 | 32 | @ApiProperty({example: false, description: 'Per base capture worth in percentage'}) 33 | @Column({ 34 | type: 'decimal', 35 | precision: 2, 36 | scale: 2, 37 | }) 38 | perBasePercentage: number; 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/vehicle.vs.vehicle.embed.ts: -------------------------------------------------------------------------------- 1 | import {Column} from 'typeorm'; 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | 4 | export default class VehicleStatsEmbed { 5 | @ApiProperty({example: 53, description: 'Total number of kills for parent metric'}) 6 | @Column({ 7 | type: 'number', 8 | }) 9 | kills: number; 10 | 11 | @ApiProperty({example: 23, description: 'Total number of deaths for parent metric'}) 12 | @Column({ 13 | type: 'number', 14 | }) 15 | deaths: number; 16 | 17 | @ApiProperty({example: 11, description: 'Total number of teamkills for parent metric'}) 18 | @Column({ 19 | type: 'number', 20 | }) 21 | teamkills: number; 22 | 23 | @ApiProperty({example: 4, description: 'Total number of times teamkilled for parent metric'}) 24 | @Column({ 25 | type: 'number', 26 | }) 27 | teamkilled: number; 28 | } 29 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/xperminute.embed.ts: -------------------------------------------------------------------------------- 1 | import {ApiProperty} from '@nestjs/swagger'; 2 | import {Column} from 'typeorm'; 3 | 4 | export default class XperminuteEmbed { 5 | @ApiProperty({example: 3.47, description: 'Kills per minute made using the durationFirstSeen value'}) 6 | @Column({ 7 | type: 'number', 8 | }) 9 | killsPerMinute: number; 10 | 11 | @ApiProperty({example: 2.69, description: 'Deaths per minute made using the durationFirstSeen value'}) 12 | @Column({ 13 | type: 'number', 14 | }) 15 | deathsPerMinute: number; 16 | 17 | @ApiProperty({example: 0.16, description: 'Team Kills per minute made using the durationFirstSeen value'}) 18 | @Column({ 19 | type: 'number', 20 | }) 21 | teamKillsPerMinute: number; 22 | 23 | @ApiProperty({example: 0.03, description: 'Suicides per minute made using the durationFirstSeen value'}) 24 | @Column({ 25 | type: 'number', 26 | }) 27 | suicidesPerMinute: number; 28 | 29 | @ApiProperty({example: 0.88, description: 'Headshots per minute made using the durationFirstSeen value'}) 30 | @Column({ 31 | type: 'number', 32 | }) 33 | headshotsPerMinute: number; 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/common/xperminute.outfit.embed.ts: -------------------------------------------------------------------------------- 1 | import XperminuteEmbed from './xperminute.embed'; 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | 5 | export default class XperminuteOutfitEmbed extends XperminuteEmbed { 6 | @ApiProperty({example: 3.47, description: 'Kills per minute per participant made using the durationFirstSeen value'}) 7 | @Column({ 8 | type: 'number', 9 | }) 10 | killsPerMinutePerParticipant: number; 11 | 12 | @ApiProperty({example: 2.69, description: 'Deaths per minute per participant made using the durationFirstSeen value'}) 13 | @Column({ 14 | type: 'number', 15 | }) 16 | deathsPerMinutePerParticipant: number; 17 | 18 | @ApiProperty({example: 0.16, description: 'Team Kills per minute per participant made using the durationFirstSeen value'}) 19 | @Column({ 20 | type: 'number', 21 | }) 22 | teamKillsPerMinutePerParticipant: number; 23 | 24 | @ApiProperty({example: 0.03, description: 'Suicides per minute per participant made using the durationFirstSeen value'}) 25 | @Column({ 26 | type: 'number', 27 | }) 28 | suicidesPerMinutePerParticipant: number; 29 | 30 | @ApiProperty({example: 0.88, description: 'Headshots per minute per participant made using the durationFirstSeen value'}) 31 | @Column({ 32 | type: 'number', 33 | }) 34 | headshotsPerMinutePerParticipant: number; 35 | } 36 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/global/global.character.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 5 | import {World, worldArray} from '../../../ps2alerts-constants/world'; 6 | import CharacterEmbed from '../common/character.embed'; 7 | import {Bracket, ps2alertsBracketArray} from '../../../ps2alerts-constants/bracket'; 8 | import FactionVersusFactionEmbed from '../common/faction.versus.faction.embed'; 9 | import {Ps2AlertsEventType} from '../../../ps2alerts-constants/ps2AlertsEventType'; 10 | 11 | @Entity({ 12 | name: 'aggregate_global_characters', 13 | }) 14 | @Index(['world', 'character.id', 'bracket', 'ps2AlertsEventType'], {unique: true}) 15 | @Index(['character.id']) 16 | @Index(['character.name']) 17 | @Index(['bracket']) 18 | @Index(['kills']) 19 | @Index(['deaths']) 20 | @Index(['teamKills']) 21 | @Index(['teamKilled']) 22 | @Index(['suicides']) 23 | @Index(['headshots']) 24 | @Index(['ps2AlertsEventType']) 25 | export default class GlobalCharacterAggregateEntity { 26 | @ObjectIdColumn() 27 | @Exclude() 28 | @ApiHideProperty() 29 | _id: ObjectId; 30 | 31 | @ApiProperty({enum: worldArray, example: 10, description: 'Server / World ID'}) 32 | @Column({ 33 | type: 'enum', 34 | enum: worldArray, 35 | }) 36 | world: World; 37 | 38 | @ApiProperty({example: Bracket.PRIME, enum: ps2alertsBracketArray, description: 'Activity bracket level of the Aggregate'}) 39 | @Column({ 40 | type: 'enum', 41 | enum: ps2alertsBracketArray, 42 | }) 43 | bracket: Bracket; 44 | 45 | @ApiProperty({type: CharacterEmbed, description: 'Character details'}) 46 | @Column(() => CharacterEmbed) 47 | character: CharacterEmbed; 48 | 49 | @ApiProperty({example: 22, description: 'Total number of kills'}) 50 | @Column({ 51 | type: 'number', 52 | default: 0, 53 | }) 54 | kills: number; 55 | 56 | @ApiProperty({example: 18, description: 'Total number of deaths'}) 57 | @Column({ 58 | type: 'number', 59 | default: 0, 60 | }) 61 | deaths: number; 62 | 63 | @ApiProperty({example: 3, description: 'Total number of team kills'}) 64 | @Column({ 65 | type: 'number', 66 | default: 0, 67 | }) 68 | teamKills: number; 69 | 70 | @ApiProperty({example: 5, description: 'Total number of times teamkilled'}) 71 | @Column({ 72 | type: 'number', 73 | default: 0, 74 | }) 75 | teamKilled: number; 76 | 77 | @ApiProperty({example: 2, description: 'Total number of suicides'}) 78 | @Column({ 79 | type: 'number', 80 | default: 0, 81 | }) 82 | suicides: number; 83 | 84 | @ApiProperty({example: 15, description: 'Total number of headshots'}) 85 | @Column({ 86 | type: 'number', 87 | default: 0, 88 | }) 89 | headshots: number; 90 | 91 | @ApiProperty({type: FactionVersusFactionEmbed, description: 'Kills broken down by faction'}) 92 | @Column(() => FactionVersusFactionEmbed) 93 | factionKills: FactionVersusFactionEmbed; 94 | 95 | @ApiProperty({ 96 | example: Ps2AlertsEventType.LIVE_METAGAME, 97 | description: 'PS2Alerts Event Type for the aggregate', 98 | }) 99 | @Column({ 100 | type: 'number', 101 | default: Ps2AlertsEventType.LIVE_METAGAME, 102 | }) 103 | ps2AlertsEventType: Ps2AlertsEventType; 104 | } 105 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/global/global.facility.control.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 3 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 4 | import {Exclude} from 'class-transformer'; 5 | import {World, worldArray} from '../../../ps2alerts-constants/world'; 6 | import FacilityFactionControl from '../common/facility.faction.control.embed'; 7 | import {Bracket, ps2alertsBracketArray} from '../../../ps2alerts-constants/bracket'; 8 | import FacilityEmbed from '../common/facility.embed'; 9 | import {Ps2AlertsEventType} from '../../../ps2alerts-constants/ps2AlertsEventType'; 10 | 11 | @Entity({ 12 | name: 'aggregate_global_facility_controls', 13 | }) 14 | @Index(['world', 'facility.id', 'bracket', 'ps2AlertsEventType'], {unique: true}) 15 | @Index(['bracket']) 16 | @Index(['facility.zone']) 17 | @Index(['ps2AlertsEventType']) 18 | export default class GlobalFacilityControlAggregateEntity { 19 | @ObjectIdColumn() 20 | @Exclude() 21 | @ApiHideProperty() 22 | _id: ObjectId; 23 | 24 | @ApiProperty({enum: worldArray, example: 10, description: 'Server / World ID'}) 25 | @Column({ 26 | type: 'enum', 27 | enum: worldArray, 28 | }) 29 | world: World; 30 | 31 | @ApiProperty({example: Bracket.PRIME, enum: ps2alertsBracketArray, description: 'Activity bracket level of the Aggregate'}) 32 | @Column({ 33 | type: 'enum', 34 | enum: ps2alertsBracketArray, 35 | }) 36 | bracket: Bracket; 37 | 38 | @ApiProperty({type: FacilityEmbed, description: 'Facility details'}) 39 | @Column(() => FacilityEmbed) 40 | facility: FacilityEmbed; 41 | 42 | @ApiProperty({type: FacilityFactionControl, description: 'Facility Capture / Defenses for VS faction'}) 43 | @Column(() => FacilityFactionControl) 44 | vs: FacilityFactionControl; 45 | 46 | @ApiProperty({type: FacilityFactionControl, description: 'Facility Capture / Defenses for NC faction'}) 47 | @Column(() => FacilityFactionControl) 48 | nc: FacilityFactionControl; 49 | 50 | @ApiProperty({type: FacilityFactionControl, description: 'Facility Capture / Defenses for TR faction'}) 51 | @Column(() => FacilityFactionControl) 52 | tr: FacilityFactionControl; 53 | 54 | // No NSO, they cannot capture bases on behalf of their faction. Their outfits can though strangely! 55 | 56 | @ApiProperty({type: FacilityFactionControl, description: 'Facility Capture / Defenses for all factions'}) 57 | @Column(() => FacilityFactionControl) 58 | totals: FacilityFactionControl; 59 | 60 | @ApiProperty({ 61 | example: Ps2AlertsEventType.LIVE_METAGAME, 62 | description: 'PS2Alerts Event Type for the aggregate', 63 | }) 64 | @Column({ 65 | type: 'number', 66 | default: Ps2AlertsEventType.LIVE_METAGAME, 67 | }) 68 | ps2AlertsEventType: Ps2AlertsEventType; 69 | } 70 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/global/global.faction.combat.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, ObjectId, Index} from 'typeorm'; 5 | import {World, worldArray} from '../../../ps2alerts-constants/world'; 6 | import CombatStats from '../common/combat.stats.embed'; 7 | import {Bracket, ps2alertsBracketArray} from '../../../ps2alerts-constants/bracket'; 8 | import FactionVersusFactionEmbed from '../common/faction.versus.faction.embed'; 9 | import {Ps2AlertsEventType} from '../../../ps2alerts-constants/ps2AlertsEventType'; 10 | 11 | @Entity({ 12 | name: 'aggregate_global_faction_combats', 13 | }) 14 | @Index(['world', 'date', 'bracket', 'ps2AlertsEventType'], {unique: true}) 15 | @Index(['date']) 16 | @Index(['bracket']) 17 | @Index(['ps2AlertsEventType']) 18 | export default class GlobalFactionCombatAggregateEntity { 19 | @ObjectIdColumn() 20 | @Exclude() 21 | @ApiHideProperty() 22 | _id: ObjectId; 23 | 24 | @ApiProperty({enum: worldArray, example: 10, description: 'Server / World ID'}) 25 | @Column({ 26 | type: 'enum', 27 | enum: worldArray, 28 | }) 29 | world: World; 30 | 31 | @ApiProperty({example: Bracket.PRIME, enum: ps2alertsBracketArray, description: 'Activity bracket level of the Aggregate'}) 32 | @Column({ 33 | type: 'enum', 34 | enum: ps2alertsBracketArray, 35 | }) 36 | bracket: Bracket; 37 | 38 | @ApiProperty({example: '2020-01-01', description: 'Date of the aggregate in UTC'}) 39 | @Column({ 40 | type: 'date', 41 | }) 42 | date: Date; 43 | 44 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for VS faction'}) 45 | @Column(() => CombatStats) 46 | vs: CombatStats; 47 | 48 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for NC faction'}) 49 | @Column(() => CombatStats) 50 | nc: CombatStats; 51 | 52 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for TR faction'}) 53 | @Column(() => CombatStats) 54 | tr: CombatStats; 55 | 56 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for NSO faction'}) 57 | @Column(() => CombatStats) 58 | nso: CombatStats; 59 | 60 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for all factions'}) 61 | @Column(() => CombatStats) 62 | totals: CombatStats; 63 | 64 | @ApiProperty({type: FactionVersusFactionEmbed, description: 'Kills broken down by faction'}) 65 | @Column(() => FactionVersusFactionEmbed) 66 | factionKills: FactionVersusFactionEmbed; 67 | 68 | @ApiProperty({ 69 | example: Ps2AlertsEventType.LIVE_METAGAME, 70 | description: 'PS2Alerts Event Type for the aggregate', 71 | }) 72 | @Column({ 73 | type: 'number', 74 | default: Ps2AlertsEventType.LIVE_METAGAME, 75 | }) 76 | ps2AlertsEventType: Ps2AlertsEventType; 77 | } 78 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/global/global.loadout.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 3 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 4 | import {Exclude} from 'class-transformer'; 5 | import {Loadout, loadoutArray} from '../../../ps2alerts-constants/loadout'; 6 | import {World, worldArray} from '../../../ps2alerts-constants/world'; 7 | import {Bracket, ps2alertsBracketArray} from '../../../ps2alerts-constants/bracket'; 8 | import FactionVersusFactionEmbed from '../common/faction.versus.faction.embed'; 9 | import {Ps2AlertsEventType} from '../../../ps2alerts-constants/ps2AlertsEventType'; 10 | 11 | @Entity({ 12 | name: 'aggregate_global_loadouts', 13 | }) 14 | @Index(['world', 'loadout', 'bracket', 'ps2AlertsEventType'], {unique: true}) 15 | @Index(['loadout']) 16 | @Index(['bracket']) 17 | @Index(['ps2AlertsEventType']) 18 | export default class GlobalLoadoutAggregateEntity { 19 | @ObjectIdColumn() 20 | @Exclude() 21 | @ApiHideProperty() 22 | _id: ObjectId; 23 | 24 | @ApiProperty({enum: worldArray, example: 10, description: 'Server / World ID'}) 25 | @Column({ 26 | type: 'enum', 27 | enum: worldArray, 28 | }) 29 | world: World; 30 | 31 | @ApiProperty({example: Bracket.PRIME, enum: ps2alertsBracketArray, description: 'Activity bracket level of the Aggregate'}) 32 | @Column({ 33 | type: 'enum', 34 | enum: ps2alertsBracketArray, 35 | }) 36 | bracket: Bracket; 37 | 38 | @ApiProperty({enum: loadoutArray, example: 3, description: 'Loadout ID'}) 39 | @Column({ 40 | type: 'enum', 41 | enum: loadoutArray, 42 | }) 43 | loadout: Loadout; 44 | 45 | @ApiProperty({example: 22, description: 'Total number of kills'}) 46 | @Column({ 47 | type: 'number', 48 | default: 0, 49 | }) 50 | kills: number; 51 | 52 | @ApiProperty({example: 18, description: 'Total number of deaths'}) 53 | @Column({ 54 | type: 'number', 55 | default: 0, 56 | }) 57 | deaths: number; 58 | 59 | @ApiProperty({example: 3, description: 'Total number of team kills'}) 60 | @Column({ 61 | type: 'number', 62 | default: 0, 63 | }) 64 | teamKills: number; 65 | 66 | @ApiProperty({example: 5, description: 'Total number of times teamkilled'}) 67 | @Column({ 68 | type: 'number', 69 | default: 0, 70 | }) 71 | teamKilled: number; 72 | 73 | @ApiProperty({example: 2, description: 'Total number of suicides'}) 74 | @Column({ 75 | type: 'number', 76 | default: 0, 77 | }) 78 | suicides: number; 79 | 80 | @ApiProperty({example: 15, description: 'Total number of headshots'}) 81 | @Column({ 82 | type: 'number', 83 | default: 0, 84 | }) 85 | headshots: number; 86 | 87 | @ApiProperty({type: FactionVersusFactionEmbed, description: 'Kills broken down by faction'}) 88 | @Column(() => FactionVersusFactionEmbed) 89 | factionKills: FactionVersusFactionEmbed; 90 | 91 | @ApiProperty({ 92 | example: Ps2AlertsEventType.LIVE_METAGAME, 93 | description: 'PS2Alerts Event Type for the aggregate', 94 | }) 95 | @Column({ 96 | type: 'number', 97 | default: Ps2AlertsEventType.LIVE_METAGAME, 98 | }) 99 | ps2AlertsEventType: Ps2AlertsEventType; 100 | } 101 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/global/global.victory.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 3 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 4 | import {Exclude} from 'class-transformer'; 5 | import {World, worldArray} from '../../../ps2alerts-constants/world'; 6 | import {Zone, zoneArray} from '../../../ps2alerts-constants/zone'; 7 | import {Bracket, ps2alertsBracketArray} from '../../../ps2alerts-constants/bracket'; 8 | import {Ps2AlertsEventType} from '../../../ps2alerts-constants/ps2AlertsEventType'; 9 | 10 | @Entity({ 11 | name: 'aggregate_global_victories', 12 | }) 13 | @Index(['world', 'zone', 'date', 'bracket', 'ps2AlertsEventType'], {unique: true}) 14 | @Index(['date']) 15 | @Index(['bracket']) 16 | @Index(['ps2AlertsEventType']) 17 | export default class GlobalVictoryAggregateEntity { 18 | @ObjectIdColumn() 19 | @Exclude() 20 | @ApiHideProperty() 21 | _id: ObjectId; 22 | 23 | @ApiProperty({enum: worldArray, example: 10, description: 'Server / World ID'}) 24 | @Column({ 25 | type: 'enum', 26 | enum: worldArray, 27 | }) 28 | world: World; 29 | 30 | @ApiProperty({enum: zoneArray, description: 'Zone ID'}) 31 | @Column({ 32 | type: 'enum', 33 | enum: zoneArray, 34 | }) 35 | zone: Zone; 36 | 37 | @ApiProperty({enum: ps2alertsBracketArray, description: 'Bracket'}) 38 | @Column({ 39 | type: 'enum', 40 | enum: ps2alertsBracketArray, 41 | }) 42 | bracket: Bracket; 43 | 44 | @ApiProperty({example: new Date(), description: 'Time the metagame instance ended in UTC'}) 45 | @Column({ 46 | type: 'date', 47 | }) 48 | date: Date; 49 | 50 | @ApiProperty({example: 123, description: 'VS victories'}) 51 | @Column({ 52 | type: 'number', 53 | }) 54 | vs: number; 55 | 56 | @ApiProperty({example: 123, description: 'NC victories'}) 57 | @Column({ 58 | type: 'number', 59 | }) 60 | nc: number; 61 | 62 | @ApiProperty({example: 123, description: 'TR victories'}) 63 | @Column({ 64 | type: 'number', 65 | }) 66 | tr: number; 67 | 68 | @ApiProperty({example: 123, description: 'Number of draws'}) 69 | @Column({ 70 | type: 'number', 71 | }) 72 | draws: number; 73 | 74 | @ApiProperty({ 75 | example: Ps2AlertsEventType.LIVE_METAGAME, 76 | description: 'PS2Alerts Event Type for the aggregate', 77 | }) 78 | @Column({ 79 | type: 'number', 80 | default: Ps2AlertsEventType.LIVE_METAGAME, 81 | }) 82 | ps2AlertsEventType: Ps2AlertsEventType; 83 | } 84 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/global/global.weapon.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 5 | import {World, worldArray} from '../../../ps2alerts-constants/world'; 6 | import ItemEmbed from '../common/item.embed'; 7 | import {Bracket, ps2alertsBracketArray} from '../../../ps2alerts-constants/bracket'; 8 | import FactionVersusFactionEmbed from '../common/faction.versus.faction.embed'; 9 | import {Ps2AlertsEventType} from '../../../ps2alerts-constants/ps2AlertsEventType'; 10 | 11 | @Entity({ 12 | name: 'aggregate_global_weapons', 13 | }) 14 | @Index(['world', 'weapon.id', 'bracket', 'ps2AlertsEventType'], {unique: true}) 15 | @Index(['weapon.id']) 16 | @Index(['bracket']) 17 | @Index(['kills']) 18 | @Index(['teamKills']) 19 | @Index(['suicides']) 20 | @Index(['headshots']) 21 | @Index(['ps2AlertsEventType']) 22 | export default class GlobalWeaponAggregateEntity { 23 | @ObjectIdColumn() 24 | @Exclude() 25 | @ApiHideProperty() 26 | _id: ObjectId; 27 | 28 | @ApiProperty({enum: worldArray, example: 10, description: 'Server / World ID'}) 29 | @Column({ 30 | type: 'enum', 31 | enum: worldArray, 32 | }) 33 | world: World; 34 | 35 | @ApiProperty({example: Bracket.PRIME, enum: ps2alertsBracketArray, description: 'Activity bracket level of the Aggregate'}) 36 | @Column({ 37 | type: 'enum', 38 | enum: ps2alertsBracketArray, 39 | }) 40 | bracket: Bracket; 41 | 42 | @ApiProperty({type: ItemEmbed, description: 'Weapon'}) 43 | @Column(() => ItemEmbed) 44 | weapon: ItemEmbed; 45 | 46 | @ApiProperty({example: 22, description: 'Total number of kills'}) 47 | @Column({ 48 | type: 'number', 49 | default: 0, 50 | }) 51 | kills: number; 52 | 53 | @ApiProperty({example: 3, description: 'Total number of team kills'}) 54 | @Column({ 55 | type: 'number', 56 | default: 0, 57 | }) 58 | teamKills: number; 59 | 60 | @ApiProperty({example: 2, description: 'Total number of suicides'}) 61 | @Column({ 62 | type: 'number', 63 | default: 0, 64 | }) 65 | suicides: number; 66 | 67 | @ApiProperty({example: 15, description: 'Total number of headshots'}) 68 | @Column({ 69 | type: 'number', 70 | default: 0, 71 | }) 72 | headshots: number; 73 | 74 | @ApiProperty({type: FactionVersusFactionEmbed, description: 'Kills broken down by faction'}) 75 | @Column(() => FactionVersusFactionEmbed) 76 | factionKills: FactionVersusFactionEmbed; 77 | 78 | @ApiProperty({ 79 | example: Ps2AlertsEventType.LIVE_METAGAME, 80 | description: 'PS2Alerts Event Type for the aggregate', 81 | }) 82 | @Column({ 83 | type: 'number', 84 | default: Ps2AlertsEventType.LIVE_METAGAME, 85 | }) 86 | ps2AlertsEventType: Ps2AlertsEventType; 87 | } 88 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/instance/instance.combat.history.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 5 | import CombatStats from '../common/combat.stats.embed'; 6 | import {Ps2AlertsEventType} from '../../../ps2alerts-constants/ps2AlertsEventType'; 7 | 8 | @Entity({ 9 | name: 'aggregate_instance_combat_histories', 10 | }) 11 | @Index(['instance', 'timestamp', 'ps2AlertsEventType'], {unique: true}) 12 | export default class InstanceCombatHistoryAggregateEntity { 13 | @ObjectIdColumn() 14 | @Exclude() 15 | @ApiHideProperty() 16 | _id: ObjectId; 17 | 18 | @ApiProperty({example: '10-12345', description: 'The Server-CensusInstanceId combination'}) 19 | @Column({ 20 | type: 'string', 21 | }) 22 | instance: string; 23 | 24 | @ApiProperty({example: new Date(), description: 'Timestamp of the aggregate instance reported in UTC'}) 25 | @Column({ 26 | type: 'date', 27 | }) 28 | timestamp: Date; 29 | 30 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for VS faction'}) 31 | @Column(() => CombatStats) 32 | vs: CombatStats; 33 | 34 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for NC faction'}) 35 | @Column(() => CombatStats) 36 | nc: CombatStats; 37 | 38 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for TR faction'}) 39 | @Column(() => CombatStats) 40 | tr: CombatStats; 41 | 42 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for NSO faction'}) 43 | @Column(() => CombatStats) 44 | nso: CombatStats; 45 | 46 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for all factions'}) 47 | @Column(() => CombatStats) 48 | totals: CombatStats; 49 | 50 | @ApiProperty({ 51 | example: Ps2AlertsEventType.LIVE_METAGAME, 52 | description: 'PS2Alerts Event Type for the aggregate', 53 | }) 54 | @Column({ 55 | type: 'number', 56 | default: Ps2AlertsEventType.LIVE_METAGAME, 57 | }) 58 | ps2AlertsEventType: Ps2AlertsEventType; 59 | } 60 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/instance/instance.facility.control.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 5 | import FacilityFactionControl from '../common/facility.faction.control.embed'; 6 | import FacilityEmbed from '../common/facility.embed'; 7 | import {Ps2AlertsEventType} from '../../../ps2alerts-constants/ps2AlertsEventType'; 8 | 9 | @Entity({ 10 | name: 'aggregate_instance_facility_controls', 11 | }) 12 | @Index(['instance', 'facility.id', 'ps2AlertsEventType'], {unique: true}) 13 | @Index(['facility.id']) 14 | @Index(['ps2AlertsEventType']) 15 | export default class InstanceFacilityControlAggregateEntity { 16 | @ObjectIdColumn() 17 | @Exclude() 18 | @ApiHideProperty() 19 | _id: ObjectId; 20 | 21 | @ApiProperty({example: '10-12345', description: 'The Server-CensusInstanceId combination'}) 22 | @Column({ 23 | type: 'string', 24 | }) 25 | instance: string; 26 | 27 | @ApiProperty({type: FacilityEmbed, description: 'Facility details'}) 28 | @Column(() => FacilityEmbed) 29 | facility: FacilityEmbed; 30 | 31 | @ApiProperty({type: FacilityFactionControl, description: 'Facility Capture / Defenses for VS faction'}) 32 | @Column(() => FacilityFactionControl) 33 | vs: FacilityFactionControl; 34 | 35 | @ApiProperty({type: FacilityFactionControl, description: 'Facility Capture / Defenses for NC faction'}) 36 | @Column(() => FacilityFactionControl) 37 | nc: FacilityFactionControl; 38 | 39 | @ApiProperty({type: FacilityFactionControl, description: 'Facility Capture / Defenses for TR faction'}) 40 | @Column(() => FacilityFactionControl) 41 | tr: FacilityFactionControl; 42 | 43 | // No NSO, they cannot capture bases on behalf of their faction. Their outfits can though strangely! 44 | 45 | @ApiProperty({type: FacilityFactionControl, description: 'Facility Capture / Defenses for all factions'}) 46 | @Column(() => FacilityFactionControl) 47 | totals: FacilityFactionControl; 48 | 49 | @ApiProperty({ 50 | example: Ps2AlertsEventType.LIVE_METAGAME, 51 | description: 'PS2Alerts Event Type for the aggregate', 52 | }) 53 | @Column({ 54 | type: 'number', 55 | default: Ps2AlertsEventType.LIVE_METAGAME, 56 | }) 57 | ps2AlertsEventType: Ps2AlertsEventType; 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/instance/instance.faction.combat.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, ObjectId, Index} from 'typeorm'; 5 | import CombatStats from '../common/combat.stats.embed'; 6 | import FactionVersusFactionEmbed from '../common/faction.versus.faction.embed'; 7 | import {Ps2AlertsEventType} from '../../../ps2alerts-constants/ps2AlertsEventType'; 8 | 9 | @Entity({ 10 | name: 'aggregate_instance_faction_combats', 11 | }) 12 | @Index(['instance', 'ps2AlertsEventType'], {unique: true}) 13 | @Index(['ps2AlertsEventType']) 14 | export default class InstanceFactionCombatAggregateEntity { 15 | @ObjectIdColumn() 16 | @Exclude() 17 | @ApiHideProperty() 18 | _id?: ObjectId; 19 | 20 | @ApiProperty({example: '10-12345', description: 'The Server-CensusInstanceId combination'}) 21 | @Column({ 22 | type: 'string', 23 | }) 24 | instance: string; 25 | 26 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for VS faction'}) 27 | @Column(() => CombatStats) 28 | vs: CombatStats; 29 | 30 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for NC faction'}) 31 | @Column(() => CombatStats) 32 | nc: CombatStats; 33 | 34 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for TR faction'}) 35 | @Column(() => CombatStats) 36 | tr: CombatStats; 37 | 38 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for NSO faction'}) 39 | @Column(() => CombatStats) 40 | nso: CombatStats; 41 | 42 | @ApiProperty({type: CombatStats, description: 'Combat Statistics for all factions'}) 43 | @Column(() => CombatStats) 44 | totals: CombatStats; 45 | 46 | @ApiProperty({type: FactionVersusFactionEmbed, description: 'Kills broken down by faction'}) 47 | @Column(() => FactionVersusFactionEmbed) 48 | factionKills: FactionVersusFactionEmbed; 49 | 50 | @ApiProperty({ 51 | example: Ps2AlertsEventType.LIVE_METAGAME, 52 | description: 'PS2Alerts Event Type for the aggregate', 53 | }) 54 | @Column({ 55 | type: 'number', 56 | default: Ps2AlertsEventType.LIVE_METAGAME, 57 | }) 58 | ps2AlertsEventType: Ps2AlertsEventType; 59 | } 60 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/instance/instance.loadout.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 5 | import {Loadout, loadoutArray} from '../../../ps2alerts-constants/loadout'; 6 | import FactionVersusFactionEmbed from '../common/faction.versus.faction.embed'; 7 | import {Ps2AlertsEventType} from '../../../ps2alerts-constants/ps2AlertsEventType'; 8 | 9 | @Entity({ 10 | name: 'aggregate_instance_loadouts', 11 | }) 12 | @Index(['instance', 'loadout', 'ps2AlertsEventType'], {unique: true}) 13 | @Index(['loadout']) 14 | @Index(['ps2AlertsEventType']) 15 | export default class InstanceLoadoutAggregateEntity { 16 | @ObjectIdColumn() 17 | @Exclude() 18 | @ApiHideProperty() 19 | _id: ObjectId; 20 | 21 | @ApiProperty({example: '10-12345', description: 'The Server-CensusInstanceId combination'}) 22 | @Column({ 23 | type: 'string', 24 | }) 25 | instance: string; 26 | 27 | @ApiProperty({enum: loadoutArray, description: 'Loadout ID'}) 28 | @Column({ 29 | type: 'enum', 30 | enum: loadoutArray, 31 | }) 32 | loadout: Loadout; // Subject to change to a PlayerInterface 33 | 34 | @ApiProperty({example: 22, description: 'Total number of kills'}) 35 | @Column({ 36 | type: 'number', 37 | default: 0, 38 | }) 39 | kills: number; 40 | 41 | @ApiProperty({example: 18, description: 'Total number of deaths'}) 42 | @Column({ 43 | type: 'number', 44 | default: 0, 45 | }) 46 | deaths: number; 47 | 48 | @ApiProperty({example: 3, description: 'Total number of team kills'}) 49 | @Column({ 50 | type: 'number', 51 | default: 0, 52 | }) 53 | teamKills: number; 54 | 55 | @ApiProperty({example: 5, description: 'Total number of times teamkilled'}) 56 | @Column({ 57 | type: 'number', 58 | default: 0, 59 | }) 60 | teamKilled: number; 61 | 62 | @ApiProperty({example: 2, description: 'Total number of suicides'}) 63 | @Column({ 64 | type: 'number', 65 | default: 0, 66 | }) 67 | suicides: number; 68 | 69 | @ApiProperty({example: 15, description: 'Total number of headshots'}) 70 | @Column({ 71 | type: 'number', 72 | default: 0, 73 | }) 74 | headshots: number; 75 | 76 | @ApiProperty({type: FactionVersusFactionEmbed, description: 'Kills broken down by faction'}) 77 | @Column(() => FactionVersusFactionEmbed) 78 | factionKills: FactionVersusFactionEmbed; 79 | 80 | @ApiProperty({ 81 | example: Ps2AlertsEventType.LIVE_METAGAME, 82 | description: 'PS2Alerts Event Type for the aggregate', 83 | }) 84 | @Column({ 85 | type: 'number', 86 | default: Ps2AlertsEventType.LIVE_METAGAME, 87 | }) 88 | ps2AlertsEventType: Ps2AlertsEventType; 89 | } 90 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/instance/instance.population.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 5 | 6 | @Entity({ 7 | name: 'aggregate_instance_populations', 8 | }) 9 | @Index(['instance', 'timestamp'], {unique: true}) 10 | export default class InstancePopulationAggregateEntity { 11 | @ObjectIdColumn() 12 | @Exclude() 13 | @ApiHideProperty() 14 | _id: ObjectId; 15 | 16 | @ApiProperty({example: '10-12345', description: 'The Server-CensusInstanceId combination'}) 17 | @Column({ 18 | type: 'string', 19 | }) 20 | instance: string; 21 | 22 | @ApiProperty({example: new Date(), description: 'Timestamp of the aggregate instance reported in UTC'}) 23 | @Column({ 24 | type: 'date', 25 | }) 26 | timestamp: Date; 27 | 28 | @ApiProperty({example: 184, description: 'Total population of VS faction'}) 29 | @Column({ 30 | type: 'number', 31 | default: 0, 32 | }) 33 | vs: number; 34 | 35 | @ApiProperty({example: 184, description: 'Total population of NC faction'}) 36 | @Column({ 37 | type: 'number', 38 | default: 0, 39 | }) 40 | nc: number; 41 | 42 | @ApiProperty({example: 184, description: 'Total population of TR faction'}) 43 | @Column({ 44 | type: 'number', 45 | default: 0, 46 | }) 47 | tr: number; 48 | 49 | @ApiProperty({example: 184, description: 'Total population of NSO faction'}) 50 | @Column({ 51 | type: 'number', 52 | default: 0, 53 | }) 54 | nso: number; 55 | 56 | @ApiProperty({example: 184, description: 'Total population of all factions'}) 57 | @Column({ 58 | type: 'number', 59 | default: 0, 60 | }) 61 | total: number; 62 | } 63 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/instance/instance.population.averages.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 5 | 6 | @Entity({ 7 | name: 'aggregate_instance_population_averages', 8 | }) 9 | @Index(['instance', 'timestamp'], {unique: true}) 10 | export default class InstancePopulationAveragesAggregateEntity { 11 | @ObjectIdColumn() 12 | @Exclude() 13 | @ApiHideProperty() 14 | _id: ObjectId; 15 | 16 | @ApiProperty({example: '10-12345', description: 'The Server-CensusInstanceId combination'}) 17 | @Column({ 18 | type: 'string', 19 | }) 20 | instance: string; 21 | 22 | @ApiProperty({example: new Date(), description: 'Timestamp of the aggregate instance reported in UTC'}) 23 | @Column({ 24 | type: 'date', 25 | }) 26 | timestamp: Date; 27 | 28 | @ApiProperty({example: 184, description: 'Total population of VS faction'}) 29 | @Column({ 30 | type: 'number', 31 | default: 0, 32 | }) 33 | vs: number; 34 | 35 | @ApiProperty({example: 184, description: 'Total population of NC faction'}) 36 | @Column({ 37 | type: 'number', 38 | default: 0, 39 | }) 40 | nc: number; 41 | 42 | @ApiProperty({example: 184, description: 'Total population of TR faction'}) 43 | @Column({ 44 | type: 'number', 45 | default: 0, 46 | }) 47 | tr: number; 48 | 49 | @ApiProperty({example: 184, description: 'Total population of NSO faction'}) 50 | @Column({ 51 | type: 'number', 52 | default: 0, 53 | }) 54 | nso: number; 55 | 56 | @ApiProperty({example: 184, description: 'Total population of all factions'}) 57 | @Column({ 58 | type: 'number', 59 | default: 0, 60 | }) 61 | total: number; 62 | } 63 | -------------------------------------------------------------------------------- /src/modules/data/entities/aggregate/instance/instance.weapon.aggregate.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiHideProperty, ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 5 | import ItemEmbed from '../common/item.embed'; 6 | import FactionVersusFactionEmbed from '../common/faction.versus.faction.embed'; 7 | import {Ps2AlertsEventType} from '../../../ps2alerts-constants/ps2AlertsEventType'; 8 | 9 | @Entity({ 10 | name: 'aggregate_instance_weapons', 11 | }) 12 | @Index(['instance', 'weapon.id', 'ps2AlertsEventType'], {unique: true}) 13 | @Index(['weapon.id']) 14 | @Index(['ps2AlertsEventType']) 15 | export default class InstanceWeaponAggregateEntity { 16 | @ObjectIdColumn() 17 | @Exclude() 18 | @ApiHideProperty() 19 | _id: ObjectId; 20 | 21 | @ApiProperty({example: '10-12345', description: 'The Server-CensusInstanceId combination'}) 22 | @Column({ 23 | type: 'string', 24 | }) 25 | instance: string; 26 | 27 | @ApiProperty({type: ItemEmbed, description: 'Weapon'}) 28 | @Column(() => ItemEmbed) 29 | weapon: ItemEmbed; 30 | 31 | @ApiProperty({example: 22, description: 'Total number of kills'}) 32 | @Column({ 33 | type: 'number', 34 | default: 0, 35 | }) 36 | kills: number; 37 | 38 | @ApiProperty({example: 3, description: 'Total number of team kills'}) 39 | @Column({ 40 | type: 'number', 41 | default: 0, 42 | }) 43 | teamKills: number; 44 | 45 | @ApiProperty({example: 2, description: 'Total number of suicides'}) 46 | @Column({ 47 | type: 'number', 48 | default: 0, 49 | }) 50 | suicides: number; 51 | 52 | @ApiProperty({example: 15, description: 'Total number of headshots'}) 53 | @Column({ 54 | type: 'number', 55 | default: 0, 56 | }) 57 | headshots: number; 58 | 59 | @ApiProperty({type: FactionVersusFactionEmbed, description: 'Kills broken down by faction'}) 60 | @Column(() => FactionVersusFactionEmbed) 61 | factionKills: FactionVersusFactionEmbed; 62 | 63 | @ApiProperty({ 64 | example: Ps2AlertsEventType.LIVE_METAGAME, 65 | description: 'PS2Alerts Event Type for the aggregate', 66 | }) 67 | @Column({ 68 | type: 'number', 69 | default: Ps2AlertsEventType.LIVE_METAGAME, 70 | }) 71 | ps2AlertsEventType: Ps2AlertsEventType; 72 | } 73 | -------------------------------------------------------------------------------- /src/modules/data/entities/instance/instance.death.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 5 | import {Loadout, loadoutArray} from '../../ps2alerts-constants/loadout'; 6 | import CharacterEmbed from '../aggregate/common/character.embed'; 7 | 8 | @Entity({ 9 | name: 'instance_deaths', 10 | }) 11 | @Index(['instance', 'attacker.id', 'character.id', 'timestamp'], {unique: true}) 12 | 13 | export default class InstanceDeathEntity { 14 | @ObjectIdColumn() 15 | @Exclude() 16 | // eslint-disable-next-line @typescript-eslint/naming-convention 17 | _id: ObjectId; 18 | 19 | @ApiProperty({example: '10-12345', description: 'The Server-CensusInstanceId combination'}) 20 | @Column({ 21 | type: 'string', 22 | }) 23 | instance: string; 24 | 25 | @ApiProperty({type: CharacterEmbed, description: 'Attacker Character details'}) 26 | @Column(() => CharacterEmbed) 27 | attacker: CharacterEmbed; 28 | 29 | @ApiProperty({type: CharacterEmbed, description: 'Character details'}) 30 | @Column(() => CharacterEmbed) 31 | character: CharacterEmbed; 32 | 33 | @ApiProperty({example: new Date(), description: 'Time of event instance in UTC'}) 34 | @Column({ 35 | type: 'date', 36 | }) 37 | timestamp: Date; 38 | 39 | @ApiProperty({example: 410, description: 'Firemode of weapon used to kill character'}) 40 | @Column({ 41 | type: 'number', 42 | }) 43 | attackerFiremode: number; 44 | 45 | @ApiProperty({enum: loadoutArray, description: 'Loadout of attacker character'}) 46 | @Column({ 47 | type: 'enum', 48 | enum: loadoutArray, 49 | }) 50 | attackerLoadout: Loadout; 51 | 52 | @ApiProperty({example: 3104, description: 'Weapon used to kill the character'}) 53 | @Column({ 54 | type: 'number', 55 | }) 56 | weapon: number; 57 | 58 | @ApiProperty({enum: loadoutArray, description: 'Loadout of killed character'}) 59 | @Column({ 60 | type: 'enum', 61 | enum: loadoutArray, 62 | }) 63 | characterLoadout: Loadout; 64 | 65 | @ApiProperty({example: false, description: 'Whether the kill was from a headshot'}) 66 | @Column({ 67 | type: 'boolean', 68 | }) 69 | isHeadshot: boolean; 70 | 71 | @ApiProperty({example: 0, description: 'Type of kill'}) 72 | @Column({ 73 | type: 'number', 74 | }) 75 | killType: number; 76 | 77 | @ApiProperty({example: 3, description: 'Vehicle ID used to kill if applicable'}) 78 | @Column({ 79 | type: 'number', 80 | nullable: true, 81 | }) 82 | vehicle?: number; 83 | } 84 | -------------------------------------------------------------------------------- /src/modules/data/entities/instance/instance.facilitycontrol.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 5 | import {Faction, factionArray} from '../../ps2alerts-constants/faction'; 6 | import TerritoryControlMapControlEmbed from './territory.control.mapcontrol.embed'; 7 | 8 | @Entity({ 9 | name: 'instance_facility_controls', 10 | }) 11 | @Index(['instance', 'facility', 'timestamp'], {unique: true}) 12 | export default class InstanceFacilityControlEntity { 13 | @ObjectIdColumn() 14 | @Exclude() 15 | _id: ObjectId; 16 | 17 | @ApiProperty({example: '10-12345', description: 'The Server-CensusInstanceId combination'}) 18 | @Column({ 19 | type: 'string', 20 | }) 21 | instance: string; 22 | 23 | @ApiProperty({example: 222, description: 'Facility ID'}) 24 | @Column({ 25 | type: 'number', 26 | }) 27 | facility: number; 28 | 29 | @ApiProperty({example: new Date(), description: 'Time of event instance in UTC'}) 30 | @Column({ 31 | type: 'date', 32 | }) 33 | timestamp: Date; 34 | 35 | @ApiProperty({enum: factionArray, description: 'Losing faction that lost control of the facility'}) 36 | @Column({ 37 | type: 'enum', 38 | enum: factionArray, 39 | }) 40 | oldFaction: Faction; 41 | 42 | @ApiProperty({enum: factionArray, description: 'Winning faction that took control of the facility'}) 43 | @Column({ 44 | type: 'enum', 45 | enum: factionArray, 46 | }) 47 | newFaction: Faction; 48 | 49 | @ApiProperty({example: 0, description: 'Time since last capture / defense event (in seconds)'}) 50 | @Column({ 51 | type: 'number', 52 | default: 0, 53 | }) 54 | durationHeld: number; 55 | 56 | @ApiProperty({example: false, description: 'Defended successfully'}) 57 | @Column({ 58 | type: 'boolean', 59 | }) 60 | isDefence: boolean; 61 | 62 | @ApiProperty({example: false, description: 'If this record is the part of the initial map state at the start of the alert.'}) 63 | @Column({ 64 | type: 'boolean', 65 | }) 66 | isInitial: boolean; 67 | 68 | @ApiProperty({example: '37509488620604883', description: 'Outfit ID of the outfit that captured the facility'}) 69 | @Column({ 70 | type: 'string', 71 | nullable: true, 72 | default: null, 73 | }) 74 | outfitCaptured?: string | null; 75 | 76 | @ApiProperty({type: TerritoryControlMapControlEmbed, description: 'Snapshot of the territory control at the point of capture'}) 77 | @Column(() => TerritoryControlMapControlEmbed) 78 | mapControl: TerritoryControlMapControlEmbed; 79 | } 80 | -------------------------------------------------------------------------------- /src/modules/data/entities/instance/instance.features.embed.ts: -------------------------------------------------------------------------------- 1 | import {ApiProperty} from '@nestjs/swagger'; 2 | import {Column} from 'typeorm'; 3 | 4 | export default class InstanceFeaturesEmbed { 5 | @ApiProperty({example: true, description: 'Whether Capture History was captured on this instance'}) 6 | @Column({ 7 | type: 'boolean', 8 | default: false, 9 | }) 10 | captureHistory: boolean; 11 | 12 | @ApiProperty({example: true, description: 'Whether XPM metrics was collected on this instance'}) 13 | @Column({ 14 | type: 'boolean', 15 | default: false, 16 | }) 17 | xpm: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/data/entities/instance/outfitwars.facility.control.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, Index, ObjectId} from 'typeorm'; 5 | import {outfitWarsTeamArray, Team} from '../../ps2alerts-constants/outfitwars/team'; 6 | import OutfitWarsTerritoryResultEmbed from '../aggregate/common/outfitwars.territory.result.embed'; 7 | 8 | @Entity({ 9 | name: 'outfitwars_facility_controls', 10 | }) 11 | @Index(['instance', 'facility', 'timestamp'], {unique: true}) 12 | export default class OutfitwarsFacilityControlEntity { 13 | @ObjectIdColumn() 14 | @Exclude() 15 | _id: ObjectId; 16 | 17 | @ApiProperty({example: 'outfitwars-1-10-34709', description: 'Unique outfitwars identifier'}) 18 | @Column({ 19 | type: 'string', 20 | }) 21 | instance: string; 22 | 23 | @ApiProperty({example: 222, description: 'Facility ID'}) 24 | @Column({ 25 | type: 'number', 26 | }) 27 | facility: number; 28 | 29 | @ApiProperty({example: new Date(), description: 'Time of event instance in UTC'}) 30 | @Column({ 31 | type: 'date', 32 | }) 33 | timestamp: Date; 34 | 35 | @ApiProperty({enum: outfitWarsTeamArray, description: 'Losing team that lost control of the facility'}) 36 | @Column({ 37 | type: 'enum', 38 | enum: outfitWarsTeamArray, 39 | }) 40 | oldFaction: Team; 41 | 42 | @ApiProperty({enum: outfitWarsTeamArray, description: 'Winning team that took control of the facility'}) 43 | @Column({ 44 | type: 'enum', 45 | enum: outfitWarsTeamArray, 46 | }) 47 | newFaction: Team; 48 | 49 | @ApiProperty({example: 0, description: 'Time since last capture / defense event (in seconds)'}) 50 | @Column({ 51 | type: 'number', 52 | default: 0, 53 | }) 54 | durationHeld: number; 55 | 56 | @ApiProperty({example: false, description: 'Defended successfully'}) 57 | @Column({ 58 | type: 'boolean', 59 | }) 60 | isDefence: boolean; 61 | 62 | @ApiProperty({example: false, description: 'If this record is the part of the initial map state at the start of the alert.'}) 63 | @Column({ 64 | type: 'boolean', 65 | }) 66 | isInitial: boolean; 67 | 68 | @ApiProperty({example: '37509488620604883', description: 'Outfit ID of the outfit that captured the facility'}) 69 | @Column({ 70 | type: 'string', 71 | nullable: true, 72 | default: null, 73 | }) 74 | outfitCaptured?: string | null; 75 | 76 | @ApiProperty({type: OutfitWarsTerritoryResultEmbed, description: 'Snapshot of the territory control at the point of capture (outfitwars version)'}) 77 | @Column(() => OutfitWarsTerritoryResultEmbed) 78 | mapControl: OutfitWarsTerritoryResultEmbed; 79 | } 80 | -------------------------------------------------------------------------------- /src/modules/data/entities/instance/outfitwars.mapcontrol.embed.ts: -------------------------------------------------------------------------------- 1 | import {ApiProperty} from '@nestjs/swagger'; 2 | import {Column} from 'typeorm'; 3 | 4 | export default class OutfitwarsMapControlEmbed { 5 | @ApiProperty({example: 20, description: 'Team 1 (blue) percentage'}) 6 | @Column({ 7 | type: 'number', 8 | }) 9 | blue: number; 10 | 11 | @ApiProperty({example: 35, description: 'Team 2 (red) percentage'}) 12 | @Column({ 13 | type: 'number', 14 | }) 15 | red: number; 16 | 17 | @ApiProperty({example: 0, description: 'Cutoff bases Percentage'}) 18 | @Column({ 19 | type: 'number', 20 | }) 21 | cutoff: number; 22 | 23 | // This will likely always be zero 24 | @ApiProperty({example: 0, description: 'Out of play percentage'}) 25 | @Column({ 26 | type: 'number', 27 | }) 28 | outOfPlay: number; 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/data/entities/instance/outfitwars.metadata.embed.ts: -------------------------------------------------------------------------------- 1 | import {ApiProperty} from '@nestjs/swagger'; 2 | import {Column} from 'typeorm'; 3 | import {Phase, phaseArray} from '../../ps2alerts-constants/outfitwars/phase'; 4 | import OutfitWarsTeamsEmbed from '../aggregate/common/outfitwars.teams.embed'; 5 | 6 | export default class OutfitwarsMetadataEmbed { 7 | @ApiProperty({ 8 | example: Phase.QUALIFIERS, 9 | enum: phaseArray, 10 | description: 'Phase of the event', 11 | }) 12 | @Column({ 13 | type: 'enum', 14 | enum: phaseArray, 15 | }) 16 | phase: Phase; 17 | 18 | @ApiProperty({ 19 | example: 2, 20 | description: 'Round count, this is incremental and is not directly linked to phase.', 21 | }) 22 | @Column({ 23 | type: 'number', 24 | }) 25 | round: number; 26 | 27 | @ApiProperty({description: 'Information about the outfits in the match'}) 28 | @Column(() => OutfitWarsTeamsEmbed) 29 | teams?: OutfitWarsTeamsEmbed; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/data/entities/instance/outfitwars.ranking.entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility,@typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Exclude} from 'class-transformer'; 4 | import {Column, ObjectIdColumn, Entity, ObjectId} from 'typeorm'; 5 | import OutfitEmbed from '../aggregate/common/outfit.embed'; 6 | import OutfitwarsRankingParamsEmbed from './outfitwars.ranking.params.embed'; 7 | import {World} from '../../ps2alerts-constants/world'; 8 | 9 | @Entity({ 10 | name: 'outfitwars_rankings', 11 | }) 12 | export default class OutfitwarsRankingEntity { 13 | @ObjectIdColumn() 14 | @Exclude() 15 | _id: ObjectId; 16 | 17 | @ApiProperty({example: new Date(), description: 'Time of ranking retrieval in UTC'}) 18 | @Column({ 19 | type: 'date', 20 | }) 21 | timestamp: Date; 22 | 23 | @ApiProperty({example: new Date(), description: 'Match start time in UTC'}) 24 | @Column({ 25 | type: 'date', 26 | }) 27 | startTime: Date; 28 | 29 | @ApiProperty({example: 1, description: 'The round during which this ranking was created'}) 30 | @Column({ 31 | type: 'number', 32 | }) 33 | round: number; 34 | 35 | @ApiProperty({example: 1, description: 'World'}) 36 | @Column({ 37 | type: 'number', 38 | }) 39 | world: World; 40 | 41 | @ApiProperty({example: 35, description: 'Outfit War Unique ID'}) 42 | @Column({ 43 | type: 'number', 44 | }) 45 | outfitWarId: number; 46 | 47 | @ApiProperty({example: '339330750175136104', description: 'Outfit War Round Unique ID'}) 48 | @Column({ 49 | type: 'string', 50 | }) 51 | roundId: string; 52 | 53 | @ApiProperty({example: 6, description: 'The order by which outfits signed up for this outfit war'}) 54 | @Column({ 55 | type: 'number', 56 | }) 57 | order: number; 58 | 59 | @ApiProperty({example: 'outfitwars-1-10-25939', description: 'The instance corresponding to the match this outfit played in this round'}) 60 | @Column({ 61 | type: 'string', 62 | nullable: true, 63 | }) 64 | instanceId: string | null; 65 | 66 | @ApiProperty({example: { 67 | id: '37570391403474491', 68 | name: 'Un1ty', 69 | faction: 3, 70 | world: 1, 71 | leader: '5428482802434229601', 72 | tag: 'UN17', 73 | }, 74 | description: 'Outfit information'}) 75 | @Column(() => OutfitEmbed) 76 | outfit: OutfitEmbed; 77 | 78 | @ApiProperty({example: { 79 | TotalScore: 0, 80 | MatchesPlayed: 0, 81 | VictoryPoints: 0, 82 | Gold: 0, 83 | Silver: 0, 84 | Bronze: 0, 85 | FactionRank: 0, 86 | GlobalRank: 12, 87 | }, 88 | description: 'The ranking parameters used to determine an outfit\'s place on the leader boards'}) 89 | @Column(() => OutfitwarsRankingParamsEmbed) 90 | rankingParameters: OutfitwarsRankingParamsEmbed; 91 | } 92 | -------------------------------------------------------------------------------- /src/modules/data/entities/instance/outfitwars.ranking.params.embed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Column} from 'typeorm'; 4 | 5 | export default class OutfitwarsRankingParamsEmbed { 6 | @ApiProperty({ 7 | example: 30444, 8 | description: 'Total number of points this outfit has accrued', 9 | }) 10 | @Column({ 11 | type: 'number', 12 | }) 13 | TotalScore: number; 14 | 15 | @ApiProperty({ 16 | example: 4, 17 | description: 'Total number of matches an outfit has played', 18 | }) 19 | @Column({ 20 | type: 'number', 21 | }) 22 | MatchesPlayed: number; 23 | 24 | @ApiProperty({ 25 | example: 3, 26 | description: 'Total number of matches an outfit has won', 27 | }) 28 | @Column({ 29 | type: 'number', 30 | }) 31 | Wins: number; 32 | 33 | @ApiProperty({ 34 | example: 1, 35 | description: 'Total number of matches an outfit has lost', 36 | }) 37 | @Column({ 38 | type: 'number', 39 | }) 40 | Losses: number; 41 | 42 | @ApiProperty({ 43 | example: 444, 44 | description: 'Total number of tiebreaker points an outfit has earned', 45 | }) 46 | @Column({ 47 | type: 'number', 48 | }) 49 | TiebreakerPoints: number; 50 | 51 | @ApiProperty({ 52 | example: 0, 53 | description: 'Rank among own faction', 54 | }) 55 | @Column({ 56 | type: 'number', 57 | }) 58 | FactionRank: number; 59 | 60 | @ApiProperty({ 61 | example: 0, 62 | description: 'Rank among all outfits on the same server, from high to low', 63 | }) 64 | @Column({ 65 | type: 'number', 66 | }) 67 | GlobalRank: number; 68 | } 69 | -------------------------------------------------------------------------------- /src/modules/data/entities/instance/territory.control.mapcontrol.embed.ts: -------------------------------------------------------------------------------- 1 | import {ApiProperty} from '@nestjs/swagger'; 2 | import {Column} from 'typeorm'; 3 | 4 | export default class TerritoryControlMapControlEmbed { 5 | @ApiProperty({example: 20, description: 'VS Capture Percentage'}) 6 | @Column({ 7 | type: 'number', 8 | }) 9 | vs: number; 10 | 11 | @ApiProperty({example: 35, description: 'NC Capture Percentage'}) 12 | @Column({ 13 | type: 'number', 14 | }) 15 | nc: number; 16 | 17 | @ApiProperty({example: 45, description: 'TR Capture Percentage'}) 18 | @Column({ 19 | type: 'number', 20 | }) 21 | tr: number; 22 | 23 | @ApiProperty({example: 0, description: 'Cutoff bases Percentage'}) 24 | @Column({ 25 | type: 'number', 26 | }) 27 | cutoff: number; 28 | 29 | @ApiProperty({example: 0, description: 'Out of play percentage'}) 30 | @Column({ 31 | type: 'number', 32 | }) 33 | outOfPlay: number; 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/healthcheck/HealthCheckModule.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | import {CacheModule, Module} from '@nestjs/common'; 3 | import {TypeOrmModule} from '@nestjs/typeorm'; 4 | import InstanceMetagameTerritoryEntity from '../data/entities/instance/instance.metagame.territory.entity'; 5 | import MongoOperationsService from '../../services/mongo/mongo.operations.service'; 6 | import {RedisCacheService} from '../../services/cache/redis.cache.service'; 7 | import ConfigModule from '../../config/config.module'; 8 | import {ConfigService} from '@nestjs/config'; 9 | import * as redisStore from 'cache-manager-ioredis'; 10 | import GlobalCharacterAggregateEntity from '../data/entities/aggregate/global/global.character.aggregate.entity'; 11 | import HealthcheckController from './controllers/healthcheck.controller'; 12 | import {TerminusModule} from '@nestjs/terminus'; 13 | import {DatabaseHealthIndicator} from './indicators/DatabaseHealthIndicator'; 14 | import {RedisHealthIndicator} from './indicators/RedisHealthIndicator'; 15 | import {CronHealthIndicator} from './indicators/CronHealthIndicator'; 16 | 17 | const metadata = { 18 | controllers: [ 19 | HealthcheckController, 20 | ], 21 | imports: [ 22 | TerminusModule, 23 | TypeOrmModule.forFeature([ 24 | GlobalCharacterAggregateEntity, 25 | InstanceMetagameTerritoryEntity, 26 | ]), 27 | CacheModule.registerAsync({ 28 | imports: [ConfigModule], 29 | inject: [ConfigService], 30 | useFactory: (config: ConfigService) => { 31 | return { 32 | store: redisStore, 33 | host: config.get('redis.host'), 34 | port: config.get('redis.port'), 35 | db: config.get('redis.db'), 36 | password: config.get('redis.password'), 37 | }; 38 | }, 39 | }), 40 | ], 41 | providers: [ 42 | MongoOperationsService, 43 | RedisCacheService, 44 | DatabaseHealthIndicator, 45 | RedisHealthIndicator, 46 | CronHealthIndicator, 47 | ], 48 | }; 49 | 50 | @Module(metadata) 51 | export class HealthCheckModule {} 52 | -------------------------------------------------------------------------------- /src/modules/healthcheck/controllers/healthcheck.controller.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type,@typescript-eslint/no-unsafe-assignment */ 2 | import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; 3 | import {Controller, Get, Inject, Logger} from '@nestjs/common'; 4 | import { 5 | HealthCheck, HealthCheckResult, 6 | HealthCheckService, 7 | MicroserviceHealthIndicator, 8 | } from '@nestjs/terminus'; 9 | import {DatabaseHealthIndicator} from '../indicators/DatabaseHealthIndicator'; 10 | import {RedisHealthIndicator} from '../indicators/RedisHealthIndicator'; 11 | import {Transport} from '@nestjs/microservices'; 12 | import {ConfigService} from '@nestjs/config'; 13 | import {CronHealthIndicator} from '../indicators/CronHealthIndicator'; 14 | 15 | @ApiTags('Healthcheck') 16 | @Controller('healthcheck') 17 | export default class HealthcheckController { 18 | private readonly logger = new Logger(HealthcheckController.name); 19 | 20 | constructor( 21 | @Inject(ConfigService) private readonly config: ConfigService, 22 | private readonly health: HealthCheckService, 23 | private readonly dbHealth: DatabaseHealthIndicator, 24 | private readonly redisHealth: RedisHealthIndicator, 25 | private readonly microserviceHealth: MicroserviceHealthIndicator, 26 | private readonly cronHealth: CronHealthIndicator, 27 | ) {} 28 | 29 | @Get('') 30 | @HealthCheck() 31 | @ApiOperation({summary: 'Runs health checks on the API service'}) 32 | @ApiResponse({ 33 | status: 200, 34 | description: 'Status of the healthcheck', 35 | }) 36 | async check(): Promise { 37 | this.logger.debug('Running health check...'); 38 | 39 | const indicators = [ 40 | async () => this.redisHealth.isHealthy('healthcheck/test'), 41 | ]; 42 | 43 | // If production, Mael's character should always be present in the database 44 | if (process.env.NODE_ENV === 'production') { 45 | indicators.push(async () => this.dbHealth.isHealthy('5428010618035323201')); 46 | } 47 | 48 | if (process.env.AGGREGATOR_ENABLED === 'true') { 49 | indicators.push( 50 | async () => this.microserviceHealth.pingCheck('rabbit', { 51 | transport: Transport.RMQ, 52 | options: { 53 | urls: this.config.get('rabbitmq.url'), 54 | queue: this.config.get('rabbitmq.queue'), 55 | queueOptions: this.config.get('rabbitmq.queueOptions'), 56 | noAck: this.config.get('rabbitmq.noAck'), 57 | prefetchCount: this.config.get('rabbitmq.prefetchCount'), 58 | }, 59 | timeout: 5000, 60 | }), 61 | ); 62 | } 63 | 64 | if (process.env.CRON_ENABLED === 'true') { 65 | indicators.push(async () => this.cronHealth.isHealthy('brackets', 65)); 66 | indicators.push(async () => this.cronHealth.isHealthy('combatHistory', 65)); 67 | // indicators.push(async () => this.cronHealth.isHealthy('outfitwarsrankings', 60 * 60 * 24 + 300)); 68 | indicators.push(async () => this.cronHealth.isHealthy('xpm', 35)); 69 | } 70 | 71 | return this.health.check(indicators); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/modules/healthcheck/indicators/CronHealthIndicator.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {HealthCheckError, HealthIndicator, HealthIndicatorResult} from '@nestjs/terminus'; 3 | import {RedisCacheService} from '../../../services/cache/redis.cache.service'; 4 | 5 | @Injectable() 6 | export class CronHealthIndicator extends HealthIndicator { 7 | constructor( 8 | private readonly cacheService: RedisCacheService, 9 | ) { 10 | super(); 11 | } 12 | 13 | // Checks if the cron has been run recently, depending on set TTL as determined by the bracket controller. 14 | // If there's no redis key, it's potentially not been run yet so mark it as "healthy" and wait for deadline. 15 | async isHealthy(cron: string, deadline: number): Promise { 16 | const key = `/crons/${cron}`; 17 | const deadlineKey = `/healthcheck/deadlines${key}`; 18 | const result: string|null = await this.cacheService.get(key) ?? null; 19 | let deadlineTime: string|null = await this.cacheService.get(deadlineKey) ?? null; 20 | 21 | if (result) { 22 | return this.getStatus(cron, true, [`Cron "${cron}" has been run recently`]); 23 | } 24 | 25 | // If there's no result and no deadline time set, set one now 26 | if (!deadlineTime) { 27 | deadlineTime = String(Date.now() + (deadline * 1000)); 28 | await this.cacheService.set(deadlineKey, deadlineTime, deadline + 30); // 30s to ensure it doesn't reset before it checks again, this should be enough time to kill the pod and restart 29 | } 30 | 31 | // If there's still no result and deadline is set, check if it's within deadline, if not the cron hasn't run and it's unhealthy 32 | if (deadlineTime) { 33 | const difference = Date.now() - parseInt(deadlineTime, 10); 34 | 35 | if (difference < 0) { 36 | return this.getStatus(cron, true, [`Cron "${cron}" is possibly still warming up and hasn't run yet, and has yet to exceed deadline.`]); 37 | } 38 | } 39 | 40 | throw new HealthCheckError(cron, this.getStatus(cron, false, [`Cron "${cron}" is unhealthy, it has exceeded deadline of ${deadline}s!`])); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/modules/healthcheck/indicators/DatabaseHealthIndicator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import {Inject, Injectable} from '@nestjs/common'; 3 | import {HealthCheckError, HealthIndicator, HealthIndicatorResult} from '@nestjs/terminus'; 4 | import GlobalCharacterAggregateEntity from '../../data/entities/aggregate/global/global.character.aggregate.entity'; 5 | import {Bracket} from '../../data/ps2alerts-constants/bracket'; 6 | import MongoOperationsService from '../../../services/mongo/mongo.operations.service'; 7 | 8 | @Injectable() 9 | export class DatabaseHealthIndicator extends HealthIndicator { 10 | constructor( 11 | @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, 12 | ) { 13 | super(); 14 | } 15 | 16 | async isHealthy(key: string): Promise { 17 | // Check if the DB is alive 18 | const character: GlobalCharacterAggregateEntity = await this.mongoOperationsService.findOne( 19 | GlobalCharacterAggregateEntity, 20 | { 21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 22 | 'character.id': key, 23 | bracket: Bracket.TOTAL, 24 | }, 25 | ); 26 | 27 | if (!character?.character || character.character.name !== 'Maelstrome26') { 28 | throw new HealthCheckError('database', this.getStatus('database', false, ['Character data did not match name correctly, or character did not return. Something is wrong!', character])); 29 | } 30 | 31 | return this.getStatus('database', true, character.character); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/healthcheck/indicators/RedisHealthIndicator.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {HealthCheckError, HealthIndicator, HealthIndicatorResult} from '@nestjs/terminus'; 3 | import {RedisCacheService} from '../../../services/cache/redis.cache.service'; 4 | 5 | @Injectable() 6 | export class RedisHealthIndicator extends HealthIndicator { 7 | constructor( 8 | private readonly cacheService: RedisCacheService, 9 | ) { 10 | super(); 11 | } 12 | 13 | async isHealthy(key: string): Promise { 14 | // Check Redis is alive 15 | await this.cacheService.set(key, 'successful', 2); 16 | const result: string = await this.cacheService.get(key) ?? 'oops'; 17 | 18 | if (result === 'oops') { 19 | throw new HealthCheckError('redis', this.getStatus('redis', false, ['Redis could not create or read a key'])); 20 | } 21 | 22 | return this.getStatus('redis', true, ['Redis is healthy']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/rest/Dto/CreateFacilityControlDto.ts: -------------------------------------------------------------------------------- 1 | // @See modules/data/entities/instance/instance.facilitycontrol.entity.ts 2 | import {Faction} from '../../data/ps2alerts-constants/faction'; 3 | import {IsBoolean, IsDateString, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString} from 'class-validator'; 4 | import TerritoryControlMapControlEmbed from '../../data/entities/instance/territory.control.mapcontrol.embed'; 5 | import {ApiProperty} from '@nestjs/swagger'; 6 | 7 | export class CreateFacilityControlDto { 8 | @IsString() 9 | @IsNotEmpty() 10 | @ApiProperty({example: '10-12345'}) 11 | instance: string; 12 | 13 | @IsNumber() 14 | @IsNotEmpty() 15 | @ApiProperty({example: 222280}) 16 | facility: number; 17 | 18 | @IsDateString() 19 | @IsNotEmpty() 20 | @ApiProperty({example: '2022-03-27T01:00:10.463Z'}) 21 | timestamp: Date; 22 | 23 | @IsNumber() 24 | @IsNotEmpty() 25 | @ApiProperty({example: Faction.NEW_CONGLOMERATE}) 26 | oldFaction: Faction; 27 | 28 | @IsNumber() 29 | @IsNotEmpty() 30 | @ApiProperty({example: Faction.VANU_SOVEREIGNTY}) 31 | newFaction: Faction; 32 | 33 | @IsNumber() 34 | @IsNotEmpty() 35 | @ApiProperty({example: 1234}) 36 | durationHeld: number; 37 | 38 | @IsBoolean() 39 | @IsNotEmpty() 40 | @IsOptional() 41 | @ApiProperty({example: false, default: false}) 42 | isInitial: boolean; 43 | 44 | @IsBoolean() 45 | @IsNotEmpty() 46 | @ApiProperty({example: false}) 47 | isDefence: boolean; 48 | 49 | @IsString() 50 | @IsOptional() 51 | @ApiProperty({example: '37509488620604880'}) 52 | outfitCaptured?: string | null; 53 | 54 | @IsObject() 55 | @IsOptional() 56 | @ApiProperty({example: null, default: null}) 57 | mapControl: TerritoryControlMapControlEmbed; 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/rest/Dto/CreateInstanceMetagameDto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsDateString, 3 | IsNotEmpty, 4 | IsNumber, 5 | IsObject, 6 | IsOptional, 7 | IsString, 8 | } from 'class-validator'; 9 | import {ApiProperty} from '@nestjs/swagger'; 10 | import {World} from '../../data/ps2alerts-constants/world'; 11 | import {Zone} from '../../data/ps2alerts-constants/zone'; 12 | import {MetagameEventType} from '../../data/ps2alerts-constants/metagameEventType'; 13 | import {Ps2AlertsEventState} from '../../data/ps2alerts-constants/ps2AlertsEventState'; 14 | import {Bracket} from '../../data/ps2alerts-constants/bracket'; 15 | import { 16 | MetagameTerritoryControlResultInterface, 17 | } from '../../data/ps2alerts-constants/interfaces/MetagameTerritoryControlResultInterface'; 18 | import { 19 | PS2AlertsInstanceFeaturesInterface, 20 | } from '../../data/ps2alerts-constants/interfaces/PS2AlertsInstanceFeaturesInterface'; 21 | import {Ps2AlertsEventType} from '../../data/ps2alerts-constants/ps2AlertsEventType'; 22 | 23 | export class CreateInstanceMetagameDto { 24 | @IsString() 25 | @IsNotEmpty() 26 | @ApiProperty({example: '10-12345'}) 27 | instanceId: string; 28 | 29 | @IsNumber() 30 | @IsNotEmpty() 31 | @ApiProperty({example: World.MILLER}) 32 | world: World; 33 | 34 | @IsDateString() 35 | @IsNotEmpty() 36 | @ApiProperty({example: '2022-04-24T19:03:12.367Z'}) 37 | timeStarted: string; 38 | 39 | @IsDateString() 40 | @IsOptional() 41 | @ApiProperty({ 42 | example: null, 43 | default: null, 44 | }) 45 | timeEnded: string | null; // Can't use null for some reason 46 | 47 | @IsObject() 48 | @IsNotEmpty() 49 | @IsOptional() 50 | @ApiProperty({ 51 | example: { 52 | vs: 30, 53 | nc: 27, 54 | tr: 40, 55 | cutoff: 1, 56 | outOfPlay: 0, 57 | victor: null, 58 | draw: false, 59 | perBasePercentage: 1.1627906976744187, 60 | }, 61 | }) 62 | result: MetagameTerritoryControlResultInterface; 63 | 64 | @IsNumber() 65 | @IsNotEmpty() 66 | @ApiProperty({example: Zone.INDAR}) 67 | zone: Zone; 68 | 69 | @IsNumber() 70 | @IsNotEmpty() 71 | @ApiProperty({example: 12345}) 72 | censusInstanceId: number; 73 | 74 | @IsNumber() 75 | @IsNotEmpty() 76 | @ApiProperty({example: MetagameEventType.AMERISH_ENLIGHTENMENT}) 77 | censusMetagameEventType: MetagameEventType; 78 | 79 | @IsNumber() 80 | @IsNotEmpty() 81 | @ApiProperty({example: 1800}) 82 | duration: number; 83 | 84 | @IsNumber() 85 | @IsNotEmpty() 86 | @ApiProperty({example: Ps2AlertsEventState.STARTED}) 87 | state: Ps2AlertsEventState; 88 | 89 | @IsNumber() 90 | @IsOptional() 91 | @ApiProperty({ 92 | example: Ps2AlertsEventType.LIVE_METAGAME, 93 | default: Ps2AlertsEventType.LIVE_METAGAME, 94 | }) 95 | ps2AlertsEventType: Ps2AlertsEventType; 96 | 97 | @IsNumber() 98 | @IsOptional() 99 | @ApiProperty({ 100 | example: Bracket.UNKNOWN, 101 | default: Bracket.UNKNOWN, 102 | }) 103 | bracket: Bracket; 104 | 105 | @IsObject() 106 | @IsNotEmpty() 107 | @ApiProperty({example: {captureHistory: true, xpm: true}}) 108 | features: PS2AlertsInstanceFeaturesInterface; 109 | 110 | @IsNotEmpty() 111 | @ApiProperty({example: '1.0'}) 112 | mapVersion: string; 113 | 114 | @IsNotEmpty() 115 | @ApiProperty({example: '1.0'}) 116 | latticeVersion: string; 117 | } 118 | -------------------------------------------------------------------------------- /src/modules/rest/Dto/UpdateFacilityControlDto.ts: -------------------------------------------------------------------------------- 1 | // @See modules/data/entities/instance/instance.facilitycontrol.entity.ts 2 | import {Faction} from '../../data/ps2alerts-constants/faction'; 3 | import {IsBoolean, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString} from 'class-validator'; 4 | import {ApiProperty} from '@nestjs/swagger'; 5 | import TerritoryControlMapControlEmbed from '../../data/entities/instance/territory.control.mapcontrol.embed'; 6 | 7 | export class UpdateFacilityControlDto { 8 | @IsNumber() 9 | @IsNotEmpty() 10 | @IsOptional() 11 | @ApiProperty({example: Faction.NEW_CONGLOMERATE}) 12 | oldFaction: Faction; 13 | 14 | @IsNumber() 15 | @IsNotEmpty() 16 | @IsOptional() 17 | @ApiProperty({example: Faction.VANU_SOVEREIGNTY}) 18 | newFaction: Faction; 19 | 20 | @IsNumber() 21 | @IsNotEmpty() 22 | @IsOptional() 23 | @ApiProperty({example: 1234}) 24 | durationHeld: number; 25 | 26 | @IsBoolean() 27 | @IsNotEmpty() 28 | @IsOptional() 29 | @ApiProperty({example: false, default: false}) 30 | isInitial: boolean; 31 | 32 | @IsBoolean() 33 | @IsNotEmpty() 34 | @IsOptional() 35 | @ApiProperty({example: false}) 36 | isDefence: boolean; 37 | 38 | @IsString() 39 | @IsOptional() 40 | @ApiProperty({example: '37509488620604880'}) 41 | outfitCaptured?: string | null; 42 | 43 | @IsObject() 44 | @IsOptional() 45 | @ApiProperty({example: null, default: null}) 46 | mapControl: TerritoryControlMapControlEmbed; 47 | } 48 | -------------------------------------------------------------------------------- /src/modules/rest/Dto/UpdateInstanceMetagameDto.ts: -------------------------------------------------------------------------------- 1 | import {IsDateString, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString} from 'class-validator'; 2 | import {ApiProperty} from '@nestjs/swagger'; 3 | import {Ps2AlertsEventState} from '../../data/ps2alerts-constants/ps2AlertsEventState'; 4 | import {Faction} from '../../data/ps2alerts-constants/faction'; 5 | import {Bracket} from '../../data/ps2alerts-constants/bracket'; 6 | import { 7 | MetagameTerritoryControlResultInterface, 8 | } from '../../data/ps2alerts-constants/interfaces/MetagameTerritoryControlResultInterface'; 9 | 10 | export class UpdateInstanceMetagameDto { 11 | @IsString() 12 | @IsNotEmpty() 13 | @IsOptional() 14 | @ApiProperty({example: '10-12345'}) 15 | instanceId: string; 16 | 17 | @IsNumber() 18 | @IsNotEmpty() 19 | @IsOptional() 20 | @ApiProperty({example: Ps2AlertsEventState.ENDED}) 21 | state: Ps2AlertsEventState; 22 | 23 | @IsNumber() 24 | @IsNotEmpty() 25 | @IsOptional() 26 | @ApiProperty({example: Bracket.PRIME}) 27 | bracket: Bracket; 28 | 29 | @IsDateString() 30 | @IsNotEmpty() 31 | @IsOptional() 32 | @ApiProperty({example: '2022-03-27T01:00:10.463Z'}) 33 | timeEnded: Date; 34 | 35 | @IsObject() 36 | @IsNotEmpty() 37 | @IsOptional() 38 | @ApiProperty({ 39 | example: { 40 | vs: 30, 41 | nc: 27, 42 | tr: 40, 43 | cutoff: 1, 44 | outOfPlay: 0, 45 | victor: Faction.TERRAN_REPUBLIC, 46 | draw: true, 47 | perBasePercentage: 1.1627906976744187, 48 | }, 49 | }) 50 | result: MetagameTerritoryControlResultInterface; 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/rest/Dto/outfitwars/CreateFacilityControlOutfitWarsDto.ts: -------------------------------------------------------------------------------- 1 | // @See modules/data/entities/instance/instance.facilitycontrol.entity.ts 2 | import {IsBoolean, IsDateString, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString} from 'class-validator'; 3 | import {ApiProperty} from '@nestjs/swagger'; 4 | import OutfitwarsMapControlEmbed from '../../../data/entities/instance/outfitwars.mapcontrol.embed'; 5 | import {Team} from '../../../data/ps2alerts-constants/outfitwars/team'; 6 | 7 | export class CreateFacilityControlOutfitWarsDto { 8 | @IsString() 9 | @IsNotEmpty() 10 | @ApiProperty({example: 'outfitwars-10-10-123'}) 11 | instance: string; 12 | 13 | @IsNumber() 14 | @IsNotEmpty() 15 | @ApiProperty({example: 222280}) 16 | facility: number; 17 | 18 | @IsDateString() 19 | @IsNotEmpty() 20 | @ApiProperty({example: '2022-03-27T01:00:10.463Z'}) 21 | timestamp: Date; 22 | 23 | @IsNumber() 24 | @IsNotEmpty() 25 | @ApiProperty({example: Team.BLUE}) 26 | oldFaction: Team; 27 | 28 | @IsNumber() 29 | @IsNotEmpty() 30 | @ApiProperty({example: Team.RED}) 31 | newFaction: Team; 32 | 33 | @IsNumber() 34 | @IsNotEmpty() 35 | @ApiProperty({example: 1234}) 36 | durationHeld: number; 37 | 38 | @IsBoolean() 39 | @IsNotEmpty() 40 | @IsOptional() 41 | @ApiProperty({example: false, default: false}) 42 | isInitial: boolean; 43 | 44 | @IsBoolean() 45 | @IsNotEmpty() 46 | @ApiProperty({example: false}) 47 | isDefence: boolean; 48 | 49 | @IsString() 50 | @IsOptional() 51 | @ApiProperty({example: '37509488620604880'}) 52 | outfitCaptured?: string | null; 53 | 54 | @IsObject() 55 | @IsOptional() 56 | @ApiProperty({example: null, default: null}) 57 | mapControl: OutfitwarsMapControlEmbed; 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/rest/Dto/outfitwars/CreateInstanceOutfitWarsDto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsDateString, 3 | IsNotEmpty, 4 | IsNumber, 5 | IsObject, 6 | IsOptional, 7 | IsString, 8 | } from 'class-validator'; 9 | import {ApiProperty} from '@nestjs/swagger'; 10 | import {World} from '../../../data/ps2alerts-constants/world'; 11 | import {Zone} from '../../../data/ps2alerts-constants/zone'; 12 | import {Ps2AlertsEventState} from '../../../data/ps2alerts-constants/ps2AlertsEventState'; 13 | import {Phase} from '../../../data/ps2alerts-constants/outfitwars/phase'; 14 | import { 15 | PS2AlertsInstanceFeaturesInterface, 16 | } from '../../../data/ps2alerts-constants/interfaces/PS2AlertsInstanceFeaturesInterface'; 17 | import { 18 | OutfitwarsTerritoryResultInterface, 19 | } from '../../../data/ps2alerts-constants/interfaces/OutfitwarsTerritoryResultInterface'; 20 | import {Ps2AlertsEventType} from '../../../data/ps2alerts-constants/ps2AlertsEventType'; 21 | import OutfitwarsMetadataEmbed from '../../../data/entities/instance/outfitwars.metadata.embed'; 22 | 23 | export class CreateInstanceOutfitWarsDto { 24 | @IsString() 25 | @IsNotEmpty() 26 | @ApiProperty({example: 'outfitwars-17-10-123'}) 27 | instanceId: string; 28 | 29 | @IsNumber() 30 | @IsNotEmpty() 31 | @ApiProperty({example: World.EMERALD}) 32 | world: World; 33 | 34 | @IsNumber() 35 | @IsNotEmpty() 36 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 37 | @ApiProperty({example: Zone.NEXUS}) 38 | zone: Zone.NEXUS; 39 | 40 | @IsNumber() 41 | @IsNotEmpty() 42 | @ApiProperty({example: 12}) 43 | zoneInstanceId: number; 44 | 45 | @IsNumber() 46 | @IsNotEmpty() 47 | @ApiProperty({example: 12345}) 48 | censusInstanceId: number; 49 | 50 | @IsDateString() 51 | @IsNotEmpty() 52 | @ApiProperty({example: '2022-04-24T19:03:12.367Z'}) 53 | timeStarted: string; 54 | 55 | @IsDateString() 56 | @IsOptional() 57 | @ApiProperty({example: null, default: null}) 58 | timeEnded: string | null; 59 | 60 | @IsObject() 61 | @IsNotEmpty() 62 | @IsOptional() 63 | @ApiProperty({ 64 | example: { 65 | blue: 55, 66 | red: 45, 67 | cutoff: 0, 68 | outOfPlay: 0, 69 | victor: null, 70 | perBasePercentage: 11.111111111111, 71 | }, 72 | }) 73 | result: OutfitwarsTerritoryResultInterface; 74 | 75 | @IsNumber() 76 | @IsNotEmpty() 77 | @ApiProperty({example: 27000000, default: 27000000}) 78 | duration: number; 79 | 80 | @IsNumber() 81 | @IsNotEmpty() 82 | @ApiProperty({example: Ps2AlertsEventState.STARTED}) 83 | state: Ps2AlertsEventState; 84 | 85 | @IsNumber() 86 | @IsNotEmpty() 87 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 88 | @ApiProperty({example: Ps2AlertsEventType.OUTFIT_WARS_AUG_2022}) 89 | ps2AlertsEventType: Ps2AlertsEventType.OUTFIT_WARS_AUG_2022; 90 | 91 | @IsObject() 92 | @IsNotEmpty() 93 | @ApiProperty({example: {phase: Phase.QUALIFIERS, round: 3, teams: null}}) 94 | outfitwars: OutfitwarsMetadataEmbed; 95 | 96 | @IsObject() 97 | @IsNotEmpty() 98 | @ApiProperty({example: {captureHistory: true, xpm: true}}) 99 | features: PS2AlertsInstanceFeaturesInterface; 100 | 101 | @IsNotEmpty() 102 | @ApiProperty({example: '1.0'}) 103 | mapVersion: string; 104 | // 105 | // @ApiProperty({description: 'Victory data for the match'}) 106 | // @Column(() => OutfitWarsTeamsEmbed) 107 | // outfits: OutfitWarsTeamsEmbed; 108 | } 109 | -------------------------------------------------------------------------------- /src/modules/rest/Dto/outfitwars/UpdateFacilityControlOutfitWarsDto.ts: -------------------------------------------------------------------------------- 1 | // @See modules/data/entities/instance/instance.facilitycontrol.entity.ts 2 | import {IsBoolean, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString} from 'class-validator'; 3 | import {ApiProperty} from '@nestjs/swagger'; 4 | import {Team} from '../../../data/ps2alerts-constants/outfitwars/team'; 5 | import OutfitwarsMapControlEmbed from '../../../data/entities/instance/outfitwars.mapcontrol.embed'; 6 | 7 | export class UpdateFacilityControlOutfitWarsDto { 8 | @IsNumber() 9 | @IsNotEmpty() 10 | @IsOptional() 11 | @ApiProperty({example: Team.BLUE}) 12 | oldFaction: Team; 13 | 14 | @IsNumber() 15 | @IsNotEmpty() 16 | @IsOptional() 17 | @ApiProperty({example: Team.RED}) 18 | newFaction: Team; 19 | 20 | @IsNumber() 21 | @IsNotEmpty() 22 | @IsOptional() 23 | @ApiProperty({example: 1234}) 24 | durationHeld: number; 25 | 26 | @IsBoolean() 27 | @IsNotEmpty() 28 | @IsOptional() 29 | @ApiProperty({example: false, default: false}) 30 | isInitial: boolean; 31 | 32 | @IsBoolean() 33 | @IsNotEmpty() 34 | @IsOptional() 35 | @ApiProperty({example: false}) 36 | isDefence: boolean; 37 | 38 | @IsString() 39 | @IsOptional() 40 | @ApiProperty({example: '37509488620604880'}) 41 | outfitCaptured?: string | null; 42 | 43 | @IsObject() 44 | @IsOptional() 45 | @ApiProperty({example: null, default: null}) 46 | mapControl: OutfitwarsMapControlEmbed; 47 | } 48 | -------------------------------------------------------------------------------- /src/modules/rest/Dto/outfitwars/UpdateInstanceOutfitWarsDto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsDateString, 3 | IsNotEmpty, 4 | IsNumber, 5 | IsObject, 6 | IsOptional, 7 | } from 'class-validator'; 8 | import {ApiProperty} from '@nestjs/swagger'; 9 | import {Ps2AlertsEventState} from '../../../data/ps2alerts-constants/ps2AlertsEventState'; 10 | import { 11 | OutfitwarsTerritoryResultInterface, 12 | } from '../../../data/ps2alerts-constants/interfaces/OutfitwarsTerritoryResultInterface'; 13 | import {Team} from '../../../data/ps2alerts-constants/outfitwars/team'; 14 | import OutfitwarsMetadataEmbed from '../../../data/entities/instance/outfitwars.metadata.embed'; 15 | 16 | export class UpdateInstanceOutfitWarsDto { 17 | @IsDateString() 18 | @IsNotEmpty() 19 | @IsOptional() 20 | @ApiProperty({example: '2022-03-27T01:00:10.463Z'}) 21 | timeEnded: Date; 22 | 23 | @IsObject() 24 | @IsNotEmpty() 25 | @IsOptional() 26 | @ApiProperty({ 27 | example: { 28 | blue: 55, 29 | red: 45, 30 | cutoff: 0, 31 | outOfPlay: 0, 32 | victor: Team.BLUE, 33 | perBasePercentage: 100 / 9, 34 | }, 35 | }) 36 | result: OutfitwarsTerritoryResultInterface; 37 | 38 | @IsNumber() 39 | @IsNotEmpty() 40 | @IsOptional() 41 | @ApiProperty({example: Ps2AlertsEventState.ENDED}) 42 | state: Ps2AlertsEventState; 43 | 44 | @IsObject() 45 | @IsNotEmpty() 46 | @IsOptional() 47 | @ApiProperty({ 48 | example: { 49 | teams: { 50 | blue: { 51 | id: '37509488620604883', 52 | name: 'Dignity of War', 53 | faction: 1, 54 | world: 10, 55 | leader: '8276172967445322465', 56 | tag: 'DIG', 57 | }, 58 | red: { 59 | id: '37570391403474491', 60 | name: 'Un1ty', 61 | faction: 3, 62 | world: 1, 63 | leader: '5428482802434229601', 64 | tag: 'UN17', 65 | }, 66 | }, 67 | }, 68 | }) 69 | outfitwars: OutfitwarsMetadataEmbed; 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/rest/Dto/outfitwars/UpdateRankingOutfitWarsDto.ts: -------------------------------------------------------------------------------- 1 | import {ApiProperty} from '@nestjs/swagger'; 2 | import {IsOptional} from 'class-validator'; 3 | 4 | export class UpdateRankingOutfitWarsDto { 5 | @IsOptional() 6 | @ApiProperty({example: 'outfitwars-1-10-25939', required: false}) 7 | instanceId?: string | undefined; 8 | 9 | @IsOptional() 10 | @ApiProperty({ 11 | example: 1, 12 | description: 'If fudging is needed due to bugs with data retrieval (Cobalt), updates the wins of a ranking for an outfit. If legitimate data exists in Falcon\'s API, it will overwrite this fudged data.', 13 | required: false, 14 | }) 15 | wins?: number | undefined; 16 | 17 | @IsOptional() 18 | @ApiProperty({ 19 | example: 0, 20 | description: 'Updates the losses of a ranking for an outfit.', 21 | required: false, 22 | }) 23 | losses?: number | undefined; 24 | 25 | @IsOptional() 26 | @ApiProperty({ 27 | example: 420, 28 | description: 'Updates the tiebreaker points of a ranking for an outfit.', 29 | required: false, 30 | }) 31 | tiebreakerPoints?: number | undefined; 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/aggregates/global/BaseGlobalAggregateController.ts: -------------------------------------------------------------------------------- 1 | import {Ps2AlertsEventType} from '../../../../data/ps2alerts-constants/ps2AlertsEventType'; 2 | import {Bracket} from '../../../../data/ps2alerts-constants/bracket'; 3 | 4 | export abstract class BaseGlobalAggregateController { 5 | // If OW, force bracket to be total as brackets don't make sense in OW context 6 | public correctBracket(bracket: Bracket | undefined, ps2AlertsEventType: Ps2AlertsEventType | undefined): Bracket { 7 | if (!bracket || !ps2AlertsEventType || ps2AlertsEventType === Ps2AlertsEventType.OUTFIT_WARS_AUG_2022) { 8 | return Bracket.TOTAL; 9 | } 10 | 11 | return bracket; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/aggregates/instance/rest.aggregate.instance.combat.history.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, Inject, Param, Query} from '@nestjs/common'; 2 | import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; 3 | import MongoOperationsService from '../../../../../services/mongo/mongo.operations.service'; 4 | import InstanceCombatHistoryAggregateEntity from '../../../../data/entities/aggregate/instance/instance.combat.history.aggregate.entity'; 5 | import Pagination from '../../../../../services/mongo/pagination'; 6 | import {OptionalIntPipe} from '../../../pipes/OptionalIntPipe'; 7 | import {ApiImplicitQueries} from 'nestjs-swagger-api-implicit-queries-decorator'; 8 | import {AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES} from '../../common/rest.common.queries'; 9 | import {Ps2AlertsEventTypePipe} from '../../../pipes/Ps2AlertsEventTypePipe'; 10 | import {Ps2AlertsEventType} from '../../../../data/ps2alerts-constants/ps2AlertsEventType'; 11 | 12 | @ApiTags('Instance Combat History Aggregates') 13 | @Controller('aggregates') 14 | export default class RestInstanceCombatHistoryAggregateController { 15 | constructor( 16 | @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, 17 | ) {} 18 | 19 | @Get('instance/:instance/combat-history') 20 | @ApiOperation({summary: 'Returns the InstanceCombatHistoryAggregateEntity for an instance'}) 21 | @ApiImplicitQueries(AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES) 22 | @ApiResponse({ 23 | status: 200, 24 | description: 'The InstanceCombatHistoryAggregateEntity aggregate', 25 | type: InstanceCombatHistoryAggregateEntity, 26 | isArray: true, 27 | }) 28 | async findMany( 29 | @Param('instance') instance: string, 30 | @Query('ps2AlertsEventType', Ps2AlertsEventTypePipe) ps2AlertsEventType?: Ps2AlertsEventType, 31 | @Query('sortBy') sortBy?: string, 32 | @Query('order') order?: string, 33 | @Query('page', OptionalIntPipe) page?: number, 34 | @Query('pageSize', OptionalIntPipe) pageSize?: number, 35 | ): Promise { 36 | return this.mongoOperationsService.findMany(InstanceCombatHistoryAggregateEntity, {instance, ps2AlertsEventType}, new Pagination({sortBy, order, page, pageSize}, false)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/aggregates/instance/rest.aggregate.instance.facility.control.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, Inject, Param, ParseIntPipe, Query} from '@nestjs/common'; 2 | import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; 3 | import InstanceFacilityControlAggregateEntity from '../../../../data/entities/aggregate/instance/instance.facility.control.aggregate.entity'; 4 | import MongoOperationsService from '../../../../../services/mongo/mongo.operations.service'; 5 | import {OptionalIntPipe} from '../../../pipes/OptionalIntPipe'; 6 | import {ApiImplicitQueries} from 'nestjs-swagger-api-implicit-queries-decorator'; 7 | import Pagination from '../../../../../services/mongo/pagination'; 8 | import {AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES} from '../../common/rest.common.queries'; 9 | import {Ps2AlertsEventTypePipe} from '../../../pipes/Ps2AlertsEventTypePipe'; 10 | import {Ps2AlertsEventType} from '../../../../data/ps2alerts-constants/ps2AlertsEventType'; 11 | import {PS2ALERTS_EVENT_TYPE_QUERY} from '../../common/rest.ps2AlertsEventType.query'; 12 | import {INSTANCE_IMPLICIT_QUERY} from '../../common/rest.instance.query'; 13 | 14 | @ApiTags('Instance Facility Control Aggregates') 15 | @Controller('aggregates') 16 | export default class RestInstanceFacilityControlAggregateController { 17 | constructor( 18 | @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, 19 | ) {} 20 | 21 | @Get('instance/:instance/facility') 22 | @ApiOperation({summary: 'Returns a list of InstanceFacilityControlAggregateEntity for an instance'}) 23 | @ApiImplicitQueries(AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES) 24 | @ApiResponse({ 25 | status: 200, 26 | description: 'The list of InstanceFacilityControlAggregateEntity aggregates', 27 | type: InstanceFacilityControlAggregateEntity, 28 | isArray: true, 29 | }) 30 | async findAll( 31 | @Param('instance') instance: string, 32 | @Query('ps2AlertsEventType', Ps2AlertsEventTypePipe) ps2AlertsEventType?: Ps2AlertsEventType, 33 | @Query('sortBy') sortBy?: string, 34 | @Query('order') order?: string, 35 | @Query('page', OptionalIntPipe) page?: number, 36 | @Query('pageSize', OptionalIntPipe) pageSize?: number, 37 | ): Promise { 38 | return this.mongoOperationsService.findMany(InstanceFacilityControlAggregateEntity, {instance, ps2AlertsEventType}, new Pagination({sortBy, order, page, pageSize}, false)); 39 | } 40 | 41 | @Get('instance/:instance/facility/:facility') 42 | @ApiOperation({summary: 'Returns a InstanceFacilityControlAggregateEntity aggregate for an instance and specific facility'}) 43 | @ApiImplicitQueries([INSTANCE_IMPLICIT_QUERY, PS2ALERTS_EVENT_TYPE_QUERY]) 44 | @ApiResponse({ 45 | status: 200, 46 | description: 'The InstanceFacilityControlAggregateEntity aggregate', 47 | type: InstanceFacilityControlAggregateEntity, 48 | }) 49 | async findOne( 50 | @Param('instance') instance: string, 51 | @Param('facility', ParseIntPipe) facility: number, 52 | @Query('ps2AlertsEventType', Ps2AlertsEventTypePipe) ps2AlertsEventType?: Ps2AlertsEventType, 53 | ): Promise { 54 | return await this.mongoOperationsService.findOne(InstanceFacilityControlAggregateEntity, {instance, 'facility.id': facility, ps2AlertsEventType}); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/aggregates/instance/rest.aggregate.instance.faction.combat.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, Inject, Param, Query} from '@nestjs/common'; 2 | import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; 3 | import InstanceFactionCombatAggregateEntity from '../../../../data/entities/aggregate/instance/instance.faction.combat.aggregate.entity'; 4 | import MongoOperationsService from '../../../../../services/mongo/mongo.operations.service'; 5 | import {Ps2AlertsEventTypePipe} from '../../../pipes/Ps2AlertsEventTypePipe'; 6 | import {Ps2AlertsEventType} from '../../../../data/ps2alerts-constants/ps2AlertsEventType'; 7 | import {ApiImplicitQueries} from 'nestjs-swagger-api-implicit-queries-decorator'; 8 | import {INSTANCE_IMPLICIT_QUERY} from '../../common/rest.instance.query'; 9 | import {PS2ALERTS_EVENT_TYPE_QUERY} from '../../common/rest.ps2AlertsEventType.query'; 10 | 11 | @ApiTags('Instance Faction Combat Aggregates') 12 | @Controller('aggregates') 13 | export default class RestInstanceFactionCombatAggregateController { 14 | constructor( 15 | @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, 16 | ) {} 17 | 18 | @Get('instance/:instance/faction') 19 | @ApiOperation({summary: 'Returns the InstanceFactionCombatAggregateEntity for an instance'}) 20 | @ApiImplicitQueries([INSTANCE_IMPLICIT_QUERY, PS2ALERTS_EVENT_TYPE_QUERY]) 21 | @ApiResponse({ 22 | status: 200, 23 | description: 'The InstanceFactionCombatAggregateEntity aggregate', 24 | type: InstanceFactionCombatAggregateEntity, 25 | }) 26 | async findOne( 27 | @Param('instance') instance: string, 28 | @Query('ps2AlertsEventType', Ps2AlertsEventTypePipe) ps2AlertsEventType?: Ps2AlertsEventType, 29 | ): Promise { 30 | return this.mongoOperationsService.findOne(InstanceFactionCombatAggregateEntity, {instance, ps2AlertsEventType}); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/aggregates/instance/rest.aggregate.instance.loadout.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, Inject, Param, ParseIntPipe, Query} from '@nestjs/common'; 2 | import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; 3 | import InstanceLoadoutAggregateEntity from '../../../../data/entities/aggregate/instance/instance.loadout.aggregate.entity'; 4 | import MongoOperationsService from '../../../../../services/mongo/mongo.operations.service'; 5 | import {Loadout} from '../../../../data/ps2alerts-constants/loadout'; 6 | import {OptionalIntPipe} from '../../../pipes/OptionalIntPipe'; 7 | import {ApiImplicitQueries} from 'nestjs-swagger-api-implicit-queries-decorator'; 8 | import Pagination from '../../../../../services/mongo/pagination'; 9 | import {AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES} from '../../common/rest.common.queries'; 10 | import {Ps2AlertsEventTypePipe} from '../../../pipes/Ps2AlertsEventTypePipe'; 11 | import {Ps2AlertsEventType} from '../../../../data/ps2alerts-constants/ps2AlertsEventType'; 12 | import {INSTANCE_IMPLICIT_QUERY} from '../../common/rest.instance.query'; 13 | import {PS2ALERTS_EVENT_TYPE_QUERY} from '../../common/rest.ps2AlertsEventType.query'; 14 | 15 | @ApiTags('Instance Loadout Aggregates') 16 | @Controller('aggregates') 17 | export default class RestInstanceLoadoutAggregateController { 18 | constructor( 19 | @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, 20 | ) {} 21 | 22 | @Get('instance/:instance/loadout') 23 | @ApiOperation({summary: 'Returns a list of InstanceLoadoutAggregateEntity for an instance'}) 24 | @ApiImplicitQueries(AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES) 25 | @ApiResponse({ 26 | status: 200, 27 | description: 'The list of InstanceLoadoutAggregateEntity aggregates', 28 | type: InstanceLoadoutAggregateEntity, 29 | isArray: true, 30 | }) 31 | async findAll( 32 | @Param('instance') instance: string, 33 | @Query('ps2AlertsEventType', Ps2AlertsEventTypePipe) ps2AlertsEventType?: Ps2AlertsEventType, 34 | @Query('sortBy') sortBy?: string, 35 | @Query('order') order?: string, 36 | @Query('page', OptionalIntPipe) page?: number, 37 | @Query('pageSize', OptionalIntPipe) pageSize?: number, 38 | ): Promise { 39 | return this.mongoOperationsService.findMany(InstanceLoadoutAggregateEntity, {instance, ps2AlertsEventType}, new Pagination({sortBy, order, page, pageSize}, false)); 40 | } 41 | 42 | // Note we use loadout here because loadout is a NodeJS reserved name and TS gets confused 43 | @Get('instance/:instance/loadout/:loadout') 44 | @ApiOperation({summary: 'Returns a specific loadout of InstanceLoadoutAggregateEntity aggregates within an instance'}) 45 | @ApiImplicitQueries([INSTANCE_IMPLICIT_QUERY, PS2ALERTS_EVENT_TYPE_QUERY]) 46 | @ApiResponse({ 47 | status: 200, 48 | description: 'The InstanceLoadoutAggregateEntity aggregate', 49 | type: InstanceLoadoutAggregateEntity, 50 | }) 51 | async findOne( 52 | @Param('instance') instance: string, 53 | @Param('loadout', ParseIntPipe) loadout: Loadout, 54 | @Query('ps2AlertsEventType', Ps2AlertsEventTypePipe) ps2AlertsEventType?: Ps2AlertsEventType, 55 | ): Promise { 56 | return await this.mongoOperationsService.findOne(InstanceLoadoutAggregateEntity, {instance, loadout, ps2AlertsEventType}); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/aggregates/instance/rest.aggregate.instance.outfit.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, Inject, Param, Query} from '@nestjs/common'; 2 | import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; 3 | import InstanceOutfitAggregateEntity from '../../../../data/entities/aggregate/instance/instance.outfit.aggregate.entity'; 4 | import MongoOperationsService from '../../../../../services/mongo/mongo.operations.service'; 5 | import {OptionalIntPipe} from '../../../pipes/OptionalIntPipe'; 6 | import {ApiImplicitQueries} from 'nestjs-swagger-api-implicit-queries-decorator'; 7 | import Pagination from '../../../../../services/mongo/pagination'; 8 | import {AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES} from '../../common/rest.common.queries'; 9 | import {Ps2AlertsEventTypePipe} from '../../../pipes/Ps2AlertsEventTypePipe'; 10 | import {Ps2AlertsEventType} from '../../../../data/ps2alerts-constants/ps2AlertsEventType'; 11 | import {INSTANCE_IMPLICIT_QUERY} from '../../common/rest.instance.query'; 12 | 13 | @ApiTags('Instance Outfit Aggregates') 14 | @Controller('aggregates') 15 | export default class RestInstanceOutfitAggregateController { 16 | constructor( 17 | @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, 18 | ) {} 19 | 20 | @Get('instance/:instance/outfit') 21 | @ApiOperation({summary: 'Returns a list of InstanceOutfitAggregateEntity for an instance'}) 22 | @ApiImplicitQueries(AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES) 23 | @ApiResponse({ 24 | status: 200, 25 | description: 'The list of InstanceOutfitAggregateEntity aggregates', 26 | type: InstanceOutfitAggregateEntity, 27 | isArray: true, 28 | }) 29 | async findAll( 30 | @Param('instance') instance: string, 31 | @Query('ps2AlertsEventType', Ps2AlertsEventTypePipe) ps2AlertsEventType?: Ps2AlertsEventType, 32 | @Query('sortBy') sortBy?: string, 33 | @Query('order') order?: string, 34 | @Query('page', OptionalIntPipe) page?: number, 35 | @Query('pageSize', OptionalIntPipe) pageSize?: number, 36 | ): Promise { 37 | return this.mongoOperationsService.findMany(InstanceOutfitAggregateEntity, {instance, ps2AlertsEventType}, new Pagination({sortBy, order, page, pageSize}, false)); 38 | } 39 | 40 | @Get('instance/:instance/outfit/:outfit') 41 | @ApiOperation({summary: 'Returns a InstanceOutfitAggregateEntity aggregate with given id within an instance'}) 42 | @ApiImplicitQueries([INSTANCE_IMPLICIT_QUERY]) 43 | @ApiResponse({ 44 | status: 200, 45 | description: 'The InstanceOutfitAggregateEntity aggregate', 46 | type: InstanceOutfitAggregateEntity, 47 | }) 48 | async findOne( 49 | @Param('instance') instance: string, 50 | @Param('outfit') outfit: string, 51 | ): Promise { 52 | return this.mongoOperationsService.findOne(InstanceOutfitAggregateEntity, {instance, 'outfit.id': outfit}); 53 | } 54 | 55 | @Get('instance/outfit/:outfit') 56 | @ApiOperation({summary: 'Returns a InstanceOutfitAggregateEntity aggregate for all instances'}) 57 | @ApiResponse({ 58 | status: 200, 59 | description: 'The list of InstanceOutfitAggregateEntity aggregates by outfit ID', 60 | type: InstanceOutfitAggregateEntity, 61 | isArray: true, 62 | }) 63 | async findByOutfitId( 64 | @Param('outfit') outfit: string, 65 | ): Promise { 66 | return this.mongoOperationsService.findOne(InstanceOutfitAggregateEntity, {'outfit.id': outfit}); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/aggregates/instance/rest.aggregate.instance.population.averages.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, Inject, Param, Query} from '@nestjs/common'; 2 | import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; 3 | import MongoOperationsService from '../../../../../services/mongo/mongo.operations.service'; 4 | import {OptionalIntPipe} from '../../../pipes/OptionalIntPipe'; 5 | import {ApiImplicitQueries} from 'nestjs-swagger-api-implicit-queries-decorator'; 6 | import Pagination from '../../../../../services/mongo/pagination'; 7 | import InstancePopulationAveragesAggregateEntity 8 | from '../../../../data/entities/aggregate/instance/instance.population.averages.aggregate.entity'; 9 | import {PAGINATION_IMPLICIT_QUERIES} from '../../common/rest.pagination.queries'; 10 | 11 | @ApiTags('Instance Population Average Aggregates') 12 | @Controller('aggregates') 13 | export default class RestInstancePopulationAggregateAveragesController { 14 | constructor( 15 | @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, 16 | ) {} 17 | 18 | @Get('instance/:instance/population/averages') 19 | @ApiOperation({summary: 'Returns a list of InstancePopulationAveragesAggregateEntity for an instance'}) 20 | @ApiImplicitQueries(PAGINATION_IMPLICIT_QUERIES) 21 | @ApiResponse({ 22 | status: 200, 23 | description: 'The list of InstancePopulationAveragesAggregateEntity aggregates', 24 | type: InstancePopulationAveragesAggregateEntity, 25 | isArray: true, 26 | }) 27 | async findAll( 28 | @Param('instance') instance: string, 29 | @Query('sortBy') sortBy?: string, 30 | @Query('order') order?: string, 31 | @Query('page', OptionalIntPipe) page?: number, 32 | @Query('pageSize', OptionalIntPipe) pageSize?: number, 33 | ): Promise { 34 | return this.mongoOperationsService.findMany(InstancePopulationAveragesAggregateEntity, {instance}, new Pagination({sortBy, order, page, pageSize}, false)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/aggregates/instance/rest.aggregate.instance.population.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, Inject, Param, Query} from '@nestjs/common'; 2 | import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; 3 | import InstancePopulationAggregateEntity from '../../../../data/entities/aggregate/instance/instance.population.aggregate.entity'; 4 | import MongoOperationsService from '../../../../../services/mongo/mongo.operations.service'; 5 | import {OptionalIntPipe} from '../../../pipes/OptionalIntPipe'; 6 | import {ApiImplicitQueries} from 'nestjs-swagger-api-implicit-queries-decorator'; 7 | import Pagination from '../../../../../services/mongo/pagination'; 8 | import {RedisCacheService} from '../../../../../services/cache/redis.cache.service'; 9 | import {AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES} from '../../common/rest.common.queries'; 10 | import {Ps2AlertsEventTypePipe} from '../../../pipes/Ps2AlertsEventTypePipe'; 11 | import {Ps2AlertsEventType} from '../../../../data/ps2alerts-constants/ps2AlertsEventType'; 12 | 13 | @ApiTags('Instance Population Aggregates') 14 | @Controller('aggregates') 15 | export default class RestInstancePopulationAggregateController { 16 | constructor( 17 | @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, 18 | private readonly cacheService: RedisCacheService, 19 | ) {} 20 | 21 | @Get('instance/:instance/population') 22 | @ApiOperation({summary: 'Returns a list of InstancePopulationAggregateEntity for an instance'}) 23 | @ApiImplicitQueries(AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES) 24 | @ApiResponse({ 25 | status: 200, 26 | description: 'The list of InstancePopulationAggregateEntity aggregates', 27 | type: InstancePopulationAggregateEntity, 28 | isArray: true, 29 | }) 30 | async findAll( 31 | @Param('instance') instance: string, 32 | @Query('ps2AlertsEventType', Ps2AlertsEventTypePipe) ps2AlertsEventType?: Ps2AlertsEventType, 33 | @Query('sortBy') sortBy?: string, 34 | @Query('order') order?: string, 35 | @Query('page', OptionalIntPipe) page?: number, 36 | @Query('pageSize', OptionalIntPipe) pageSize?: number, 37 | ): Promise { 38 | const pagination = new Pagination({sortBy, order, page, pageSize}, false); 39 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 40 | const key = `/instance/${instance}/population/?P:${pagination.getKey()}`; 41 | 42 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 43 | return await this.cacheService.get(key) ?? await this.cacheService.set( 44 | key, 45 | await this.mongoOperationsService.findMany(InstancePopulationAggregateEntity, {instance}, pagination), 46 | 60); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/aggregates/instance/rest.aggregate.instance.vehicle.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, Inject, Param, ParseIntPipe, Query} from '@nestjs/common'; 2 | import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; 3 | import InstanceVehicleAggregateEntity from '../../../../data/entities/aggregate/instance/instance.vehicle.aggregate.entity'; 4 | import MongoOperationsService from '../../../../../services/mongo/mongo.operations.service'; 5 | import {Vehicle} from '../../../../data/ps2alerts-constants/vehicle'; 6 | import {OptionalIntPipe} from '../../../pipes/OptionalIntPipe'; 7 | import {ApiImplicitQueries} from 'nestjs-swagger-api-implicit-queries-decorator'; 8 | import Pagination from '../../../../../services/mongo/pagination'; 9 | import {AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES} from '../../common/rest.common.queries'; 10 | import {Ps2AlertsEventTypePipe} from '../../../pipes/Ps2AlertsEventTypePipe'; 11 | import {Ps2AlertsEventType} from '../../../../data/ps2alerts-constants/ps2AlertsEventType'; 12 | 13 | @ApiTags('Instance Vehicle Aggregates') 14 | @Controller('aggregates') 15 | export default class RestInstanceVehicleAggregateController { 16 | constructor( 17 | @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, 18 | ) {} 19 | 20 | @Get('instance/:instance/vehicle') 21 | @ApiOperation({summary: 'Returns a list of InstanceVehicleAggregateEntity aggregates for an instance'}) 22 | @ApiImplicitQueries(AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES) 23 | @ApiResponse({ 24 | status: 200, 25 | description: 'The list of InstanceVehicleAggregateEntity aggregates', 26 | type: InstanceVehicleAggregateEntity, 27 | isArray: true, 28 | }) 29 | async findAll( 30 | @Param('instance') instance: string, 31 | @Query('ps2AlertsEventType', Ps2AlertsEventTypePipe) ps2AlertsEventType?: Ps2AlertsEventType, 32 | @Query('sortBy') sortBy?: string, 33 | @Query('order') order?: string, 34 | @Query('page', OptionalIntPipe) page?: number, 35 | @Query('pageSize', OptionalIntPipe) pageSize?: number): Promise { 36 | return this.mongoOperationsService.findMany(InstanceVehicleAggregateEntity, {instance, ps2AlertsEventType}, new Pagination({sortBy, order, page, pageSize}, false)); 37 | } 38 | 39 | @Get('instance/:instance/vehicle/:vehicle') 40 | @ApiOperation({summary: 'Returns a single InstanceVehicleAggregateEntity for an instance'}) 41 | @ApiResponse({ 42 | status: 200, 43 | description: 'The InstanceVehicleAggregateEntity aggregate', 44 | type: InstanceVehicleAggregateEntity, 45 | }) 46 | async findOne( 47 | @Param('instance') instance: string, 48 | @Param('vehicle', ParseIntPipe) vehicle: Vehicle, 49 | ): Promise { 50 | return await this.mongoOperationsService.findOne(InstanceVehicleAggregateEntity, {instance, vehicle}); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/aggregates/instance/rest.aggregate.instance.weapon.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, Inject, Param, ParseIntPipe, Query} from '@nestjs/common'; 2 | import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; 3 | import InstanceWeaponAggregateEntity from '../../../../data/entities/aggregate/instance/instance.weapon.aggregate.entity'; 4 | import MongoOperationsService from '../../../../../services/mongo/mongo.operations.service'; 5 | import {OptionalIntPipe} from '../../../pipes/OptionalIntPipe'; 6 | import {ApiImplicitQueries} from 'nestjs-swagger-api-implicit-queries-decorator'; 7 | import Pagination from '../../../../../services/mongo/pagination'; 8 | import {AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES} from '../../common/rest.common.queries'; 9 | import {Ps2AlertsEventTypePipe} from '../../../pipes/Ps2AlertsEventTypePipe'; 10 | import {Ps2AlertsEventType} from '../../../../data/ps2alerts-constants/ps2AlertsEventType'; 11 | 12 | @ApiTags('Instance Weapon Aggregates') 13 | @Controller('aggregates') 14 | export default class RestInstanceWeaponAggregateController { 15 | constructor( 16 | @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, 17 | ) {} 18 | 19 | @Get('instance/:instance/weapon') 20 | @ApiOperation({summary: 'Returns a list of InstanceWeaponAggregateEntity for an instance'}) 21 | @ApiImplicitQueries(AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES) 22 | @ApiResponse({ 23 | status: 200, 24 | description: 'The list of InstanceWeaponAggregateEntity aggregates', 25 | type: InstanceWeaponAggregateEntity, 26 | isArray: true, 27 | }) 28 | async findAll( 29 | @Param('instance') instance: string, 30 | @Query('ps2AlertsEventType', Ps2AlertsEventTypePipe) ps2AlertsEventType?: Ps2AlertsEventType, 31 | @Query('sortBy') sortBy?: string, 32 | @Query('order') order?: string, 33 | @Query('page', OptionalIntPipe) page?: number, 34 | @Query('pageSize', OptionalIntPipe) pageSize?: number): Promise { 35 | return this.mongoOperationsService.findMany(InstanceWeaponAggregateEntity, {instance, ps2AlertsEventType}, new Pagination({sortBy, order, page, pageSize}, false)); 36 | } 37 | 38 | @Get('instance/:instance/weapon/:weapon') 39 | @ApiOperation({summary: 'Returns a InstanceWeaponAggregateEntity aggregate with given Id (or one of each instance)'}) 40 | @ApiResponse({ 41 | status: 200, 42 | description: 'The InstanceWeaponAggregateEntity aggregate', 43 | type: InstanceWeaponAggregateEntity, 44 | }) 45 | async findOne( 46 | @Param('instance') instance: number, 47 | @Param('weapon', ParseIntPipe) weapon: number, 48 | ): Promise { 49 | return this.mongoOperationsService.findOne(InstanceWeaponAggregateEntity, {instance, 'weapon.id': weapon}); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/census/rest.census.continent.polyfill.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, Param, ParseIntPipe} from '@nestjs/common'; 2 | import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; 3 | import {RedisCacheService} from '../../../../services/cache/redis.cache.service'; 4 | import * as fs from 'fs'; 5 | import path from 'path'; 6 | 7 | @ApiTags('Census Map Data Provider') 8 | @Controller('census') 9 | export default class RestCensusContinentPolyfillController { 10 | constructor( 11 | private readonly cacheService: RedisCacheService, 12 | ) {} 13 | 14 | @Get('regions/:zone/:version') 15 | @ApiOperation({summary: 'Return a specifically formatted census replacement for continent map data'}) 16 | @ApiResponse({ 17 | status: 200, 18 | description: 'Census map_region_data polyfill alongside lattice links', 19 | }) 20 | async serveRegions( 21 | @Param('zone', ParseIntPipe) zone: number, 22 | @Param('version') version?: string, 23 | ): Promise { 24 | if (!version) { 25 | version = '1.0'; 26 | } 27 | 28 | const key = `/census/regions/${zone}/${version}`; 29 | 30 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 31 | return await this.cacheService.get(key) ?? await this.cacheService.set( 32 | key, 33 | // eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires 34 | JSON.parse(this.readFile(path.resolve(__dirname, `../../../data/ps2alerts-constants/maps/regions-${zone}-${version}.json`))), 35 | 3600); 36 | } 37 | 38 | readFile(filePath: string): string { 39 | return fs.readFileSync(filePath, {encoding: 'utf8'}); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/common/rest.bracket.query.ts: -------------------------------------------------------------------------------- 1 | export const BRACKET_IMPLICIT_QUERY = { 2 | name: 'bracket', 3 | required: false, 4 | type: String, 5 | }; 6 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/common/rest.common.queries.ts: -------------------------------------------------------------------------------- 1 | import {PAGINATION_IMPLICIT_QUERIES} from './rest.pagination.queries'; 2 | import {WORLD_IMPLICIT_QUERY} from './rest.world.query'; 3 | import {PS2ALERTS_EVENT_TYPE_QUERY} from './rest.ps2AlertsEventType.query'; 4 | import {BRACKET_IMPLICIT_QUERY} from './rest.bracket.query'; 5 | 6 | export const COMMON_IMPLICIT_QUERIES = [ 7 | WORLD_IMPLICIT_QUERY, 8 | ...PAGINATION_IMPLICIT_QUERIES, 9 | ]; 10 | 11 | export const AGGREGATE_GLOBAL_COMMON_IMPLICIT_QUERIES = [ 12 | ...COMMON_IMPLICIT_QUERIES, 13 | BRACKET_IMPLICIT_QUERY, 14 | PS2ALERTS_EVENT_TYPE_QUERY, 15 | ]; 16 | 17 | export const AGGREGATE_INSTANCE_COMMON_IMPLICIT_QUERIES = [ 18 | PS2ALERTS_EVENT_TYPE_QUERY, 19 | ...PAGINATION_IMPLICIT_QUERIES, 20 | ]; 21 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/common/rest.date.query.ts: -------------------------------------------------------------------------------- 1 | export const DATE_FROM_IMPLICIT_QUERY = { 2 | name: 'dateFrom', 3 | required: false, 4 | type: String, 5 | }; 6 | 7 | export const DATE_TO_IMPLICIT_QUERY = { 8 | name: 'dateTo', 9 | required: false, 10 | type: String, 11 | }; 12 | 13 | export const DATE_IMPLICIT_QUERIES = [ 14 | DATE_FROM_IMPLICIT_QUERY, 15 | DATE_TO_IMPLICIT_QUERY, 16 | ]; 17 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/common/rest.instance.query.ts: -------------------------------------------------------------------------------- 1 | export const INSTANCE_IMPLICIT_QUERY = { 2 | name: 'instance', 3 | required: true, 4 | type: String, 5 | }; 6 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/common/rest.outfitwars.queries.ts: -------------------------------------------------------------------------------- 1 | import {Phase} from '../../../data/ps2alerts-constants/outfitwars/phase'; 2 | 3 | export const PHASE_IMPLICIT_QUERY = { 4 | name: 'phase', 5 | required: false, 6 | type: Phase, 7 | }; 8 | 9 | export const ROUND_IMPLICIT_QUERY = { 10 | name: 'round', 11 | required: false, 12 | type: Number, 13 | }; 14 | 15 | export const OUTFITWARS_IMPLICIT_QUERIES = [ 16 | PHASE_IMPLICIT_QUERY, 17 | ROUND_IMPLICIT_QUERY, 18 | ]; 19 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/common/rest.pagination.queries.ts: -------------------------------------------------------------------------------- 1 | export const PAGINATION_IMPLICIT_QUERIES = [ 2 | { 3 | name: 'sortBy', 4 | required: false, 5 | type: String, 6 | }, 7 | { 8 | name: 'order', 9 | required: false, 10 | type: String, 11 | }, 12 | { 13 | name: 'page', 14 | required: false, 15 | type: Number, 16 | }, 17 | { 18 | name: 'pageSize', 19 | required: false, 20 | type: Number, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/common/rest.ps2AlertsEventType.query.ts: -------------------------------------------------------------------------------- 1 | export const PS2ALERTS_EVENT_TYPE_QUERY = { 2 | name: 'ps2AlertsEventType', 3 | required: false, 4 | type: Number, 5 | }; 6 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/common/rest.result.victor.query.ts: -------------------------------------------------------------------------------- 1 | export const RESULT_VICTOR_QUERY = { 2 | name: 'victor', 3 | required: false, 4 | type: String, 5 | }; 6 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/common/rest.time.started.query.ts: -------------------------------------------------------------------------------- 1 | export const TIME_STARTED_FROM_IMPLICIT_QUERY = { 2 | name: 'timeStartedFrom', 3 | required: false, 4 | type: String, 5 | }; 6 | 7 | export const TIME_STARTED_TO_IMPLICIT_QUERY = { 8 | name: 'timeStartedTo', 9 | required: false, 10 | type: String, 11 | }; 12 | 13 | export const TIME_STARTED_IMPLICIT_QUERIES = [ 14 | TIME_STARTED_FROM_IMPLICIT_QUERY, 15 | TIME_STARTED_TO_IMPLICIT_QUERY, 16 | ]; 17 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/common/rest.world.query.ts: -------------------------------------------------------------------------------- 1 | export const WORLD_IMPLICIT_QUERY = { 2 | name: 'world', 3 | required: false, 4 | type: Number, 5 | }; 6 | -------------------------------------------------------------------------------- /src/modules/rest/controllers/common/rest.zone.query.ts: -------------------------------------------------------------------------------- 1 | export const ZONE_IMPLICIT_QUERY = { 2 | name: 'zone', 3 | required: false, 4 | type: Number, 5 | }; 6 | -------------------------------------------------------------------------------- /src/modules/rest/pipes/BracketPipe.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, PipeTransform} from '@nestjs/common'; 2 | import {Bracket} from '../../data/ps2alerts-constants/bracket'; 3 | 4 | @Injectable() 5 | export class BracketPipe implements PipeTransform { 6 | transform(value: string | undefined): Bracket { 7 | if (!value || value === '') { 8 | return Bracket.TOTAL; 9 | } 10 | 11 | return parseInt(value, 10); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/rest/pipes/MandatoryIntPipe.ts: -------------------------------------------------------------------------------- 1 | import {PipeTransform, Injectable} from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class MandatoryIntPipe implements PipeTransform { 5 | transform(value: string|undefined): number { 6 | return value ? parseInt(value, 10) : 0; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/rest/pipes/OptionalBoolPipe.ts: -------------------------------------------------------------------------------- 1 | import {PipeTransform, Injectable} from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class OptionalBoolPipe implements PipeTransform { 5 | transform(value: string | undefined): boolean | undefined { 6 | return value === 'true' ?? undefined; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/rest/pipes/OptionalDatePipe.ts: -------------------------------------------------------------------------------- 1 | import {PipeTransform, Injectable} from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class OptionalDatePipe implements PipeTransform { 5 | transform(value: string | undefined): Date | undefined { 6 | return value ? new Date(parseInt(value, 10)) : undefined; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/rest/pipes/OptionalIntPipe.ts: -------------------------------------------------------------------------------- 1 | import {PipeTransform, Injectable} from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class OptionalIntPipe implements PipeTransform { 5 | transform(value: string | undefined): number|undefined { 6 | return value ? parseInt(value, 10) : undefined; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/rest/pipes/Ps2AlertsEventTypePipe.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, PipeTransform} from '@nestjs/common'; 2 | import {Ps2AlertsEventType} from '../../data/ps2alerts-constants/ps2AlertsEventType'; 3 | 4 | @Injectable() 5 | export class Ps2AlertsEventTypePipe implements PipeTransform { 6 | transform(value: string | undefined): Ps2AlertsEventType { 7 | if (!value || value === '') { 8 | return Ps2AlertsEventType.LIVE_METAGAME; 9 | } 10 | 11 | return parseInt(value, 10); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/services/cache/redis.cache.service.ts: -------------------------------------------------------------------------------- 1 | import {CACHE_MANAGER, Inject, Injectable} from '@nestjs/common'; 2 | import {Cache} from 'cache-manager'; 3 | 4 | @Injectable() 5 | export class RedisCacheService { 6 | 7 | constructor( 8 | @Inject(CACHE_MANAGER) private readonly cache: Cache, 9 | ) {} 10 | 11 | async set(key: string, data: T, ttl = 3600): Promise { 12 | await this.cache.set(key, data, {ttl}); 13 | return data; 14 | } 15 | 16 | async get(key: string): Promise { 17 | const data: T | null = await this.cache.get(key) ?? null; 18 | return data ?? null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/services/databases/mongo.config.ts: -------------------------------------------------------------------------------- 1 | import {TypeOrmModuleOptions, TypeOrmOptionsFactory} from '@nestjs/typeorm'; 2 | import {ConfigService} from '@nestjs/config'; 3 | import {Injectable} from '@nestjs/common'; 4 | import {resolve} from 'path'; 5 | 6 | @Injectable() 7 | export class MongoConfig implements TypeOrmOptionsFactory { 8 | private readonly config: ConfigService; 9 | 10 | constructor(config: ConfigService) { 11 | this.config = config; 12 | } 13 | 14 | public createTypeOrmOptions(): TypeOrmModuleOptions { 15 | const path = resolve(`${__dirname}/../../../dist`); 16 | return { 17 | ...this.config.get('database.mongo'), 18 | entities: [`${path}/**/*.entity.js`], 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/services/mongo/pagination.ts: -------------------------------------------------------------------------------- 1 | export default class Pagination { 2 | private readonly take: number | undefined; 3 | private readonly skip: number | undefined; 4 | private readonly order: {[k: string]: string} | undefined; 5 | 6 | public constructor(pageQuery: {sortBy?: string, order?: string, pageSize?: number, page?: number}, limited = false) { 7 | this.take = 100; 8 | 9 | if (pageQuery.pageSize) { 10 | if (pageQuery.pageSize < 1000) { 11 | this.take = pageQuery.pageSize; 12 | } else { 13 | this.take = 1000; 14 | } 15 | } 16 | 17 | if (!limited && !pageQuery.pageSize) { 18 | this.take = undefined; 19 | } 20 | 21 | if (pageQuery.pageSize && pageQuery.page) { 22 | this.skip = (pageQuery.page - 1) * pageQuery.pageSize; 23 | } 24 | 25 | if (pageQuery.sortBy) { 26 | this.order = { 27 | [pageQuery.sortBy]: pageQuery.order ? pageQuery.order.toUpperCase() : 'ASC', 28 | }; 29 | } 30 | } 31 | 32 | public getKey(): string { 33 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 34 | return `O:${JSON.stringify(this.order)}-T:${this.take}-S:${this.skip}`; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/services/mongo/range.ts: -------------------------------------------------------------------------------- 1 | export default class Range { 2 | private readonly field: string; 3 | private readonly from: string | Date | undefined; 4 | private readonly to: string | Date | undefined; 5 | 6 | public constructor(field: string, from: Date | undefined, to: Date | undefined = undefined) { 7 | this.field = field; 8 | this.from = from instanceof Date ? from : from; 9 | this.to = to instanceof Date ? to : to; 10 | } 11 | 12 | public build(): Record | undefined { 13 | if (this.from && this.to) { 14 | return { 15 | $gte: this.from, 16 | $lte: this.to, 17 | }; 18 | } 19 | 20 | if (this.from && !this.to) { 21 | return { 22 | $gte: this.from, 23 | }; 24 | } 25 | 26 | return undefined; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "forceConsistentCasingInFileNames": true, 6 | "esModuleInterop": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "noImplicitAny": true, 10 | "module": "CommonJS", 11 | "moduleResolution": "Node", 12 | "outDir": "dist", 13 | "rootDir": "src", 14 | "paths": { 15 | "*": [ 16 | "node_modules/*" 17 | ] 18 | }, 19 | "strict": true, 20 | "strictNullChecks": true, 21 | "strictPropertyInitialization": false, 22 | "target": "ES6", 23 | "resolveJsonModule": true, 24 | }, 25 | "include": [ 26 | "src/**/*.ts", 27 | "src/**/*.json" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /vars.local.yml.dist: -------------------------------------------------------------------------------- 1 | census_service_id: "foobar" 2 | --------------------------------------------------------------------------------