diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 6f3cb6cae..815aae13d 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,109 +1,109 @@ -name: "Continuous Integration for IBF" +name: 'Continuous Integration for IBF' on: - push: - branches: [master] - paths-ignore: - - "./package.json" - - "./COMMITLOG.md" - pull_request: - branches: [master] + push: + branches: [master] + paths-ignore: + - './package.json' + - './COMMITLOG.md' + pull_request: + branches: [master] jobs: - detect-changes: - runs-on: ubuntu-latest - - outputs: - ibf-api-service: ${{ steps.filter.outputs.ibf-api-service }} - ibf-dashboard: ${{ steps.filter.outputs.ibf-dashboard }} - - steps: - - uses: actions/checkout@v3 - - uses: dorny/paths-filter@v2 - id: filter - with: - filters: | - ibf-api-service: - - "services/API-service/**" - ibf-dashboard: - - "interfaces/IBF-dashboard/**" - - ibf-api-service: - needs: detect-changes - if: ${{ needs.detect-changes.outputs.ibf-api-service == 'true' }} - - runs-on: ubuntu-latest - + detect-changes: + runs-on: ubuntu-latest + + outputs: + ibf-api-service: ${{ steps.filter.outputs.ibf-api-service }} + ibf-dashboard: ${{ steps.filter.outputs.ibf-dashboard }} + + steps: + - uses: actions/checkout@v3 + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + ibf-api-service: + - "services/API-service/**" + ibf-dashboard: + - "interfaces/IBF-dashboard/**" + + ibf-api-service: + needs: detect-changes + if: ${{ needs.detect-changes.outputs.ibf-api-service == 'true' }} + + runs-on: ubuntu-latest + + env: + SECRET: ${{ secrets.SECRET }} + MC_API: ${{ secrets.MC_API }} + + strategy: + matrix: + node-version: [17.x] + + defaults: + run: + working-directory: 'services/API-service' + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3.4.1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci --no-audit + - run: npm run lint + - run: npm test + - run: docker build . --file Dockerfile --tag + rodekruis/ibf-api-service:$(date +%s) + + ibf-dashboard: + needs: detect-changes + if: ${{ needs.detect-changes.outputs.ibf-dashboard == 'true' }} + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x] + + defaults: + run: + working-directory: 'interfaces/IBF-dashboard' + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3.4.1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci --no-audit + - run: npm test + - run: docker build . --file Dockerfile --tag + rodekruis/ibf-dashboard:$(date +%s) + + bump-version: + needs: [ibf-api-service, ibf-dashboard] + if: | + always() && + github.event_name == 'push' + + runs-on: ubuntu-latest + + steps: + - name: Wait for previous workflow to complete + uses: softprops/turnstyle@v1 + with: + abort-after-seconds: 1800 env: - SECRET: ${{ secrets.SECRET }} - MC_API: ${{ secrets.MC_API }} - - strategy: - matrix: - node-version: [12.x] - - defaults: - run: - working-directory: "services/API-service" - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.4.1 - with: - node-version: ${{ matrix.node-version }} - - run: npm ci --no-audit - - run: npm run lint - - run: npm test - - run: docker build . --file Dockerfile --tag - rodekruis/ibf-api-service:$(date +%s) - - ibf-dashboard: - needs: detect-changes - if: ${{ needs.detect-changes.outputs.ibf-dashboard == 'true' }} - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [14.x] - - defaults: - run: - working-directory: "interfaces/IBF-dashboard" - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.4.1 - with: - node-version: ${{ matrix.node-version }} - - run: npm ci --no-audit - - run: npm test - - run: docker build . --file Dockerfile --tag - rodekruis/ibf-dashboard:$(date +%s) - - bump-version: - needs: [ibf-api-service, ibf-dashboard] - if: | - always() && - github.event_name == 'push' - - runs-on: ubuntu-latest - - steps: - - name: Wait for previous workflow to complete - uses: softprops/turnstyle@v1 - with: - abort-after-seconds: 1800 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - uses: actions/checkout@v3 - - - name: Bump version and push tag - uses: TriPSs/conventional-changelog-action@v3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - git-message: "chore(release): {version}" - release-count: 10 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/checkout@v3 + + - name: Bump version and push tag + uses: TriPSs/conventional-changelog-action@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + git-message: 'chore(release): {version}' + release-count: 10 diff --git a/.prettierrc b/.prettierrc index 283367cde..fa9699b89 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,6 +3,5 @@ "trailingComma": "all", "singleQuote": true, "printWidth": 80, - "no-parameter-properties": true, "tabWidth": 2 } diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 8e8fa7ac2..96e26a218 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,8 +1,6 @@ -version: '3.8' - services: ibf-api-service: - command: [ 'npm', 'run', 'start:dev' ] + command: ['npm', 'run', 'start:dev'] environment: - NODE_ENV=development - LOCAL_PORT_IBF_SERVICE=${LOCAL_PORT_IBF_SERVICE} @@ -16,7 +14,7 @@ services: - api-network ibf-dashboard: - entrypoint: [ 'echo', 'Service ibf-dashboard disabled' ] + entrypoint: ['echo', 'Service ibf-dashboard disabled'] ibf-geoserver: ports: diff --git a/example.env b/example.env index 9594846a1..34b343731 100644 --- a/example.env +++ b/example.env @@ -28,7 +28,7 @@ GEOSERVER_ADMIN_PASSWORD= # interfaces/IBF-dashboard NG_CONFIGURATION= -NG_API_URL= +NG_API_URL= # URL should not end with trailing slash NG_USE_SERVICE_WORKER= NG_GEOSERVER_URL= NG_IBF_SYSTEM_VERSION= diff --git a/services/API-service/.gitignore b/services/API-service/.gitignore index 3aa5681ba..2747035a3 100644 --- a/services/API-service/.gitignore +++ b/services/API-service/.gitignore @@ -1,5 +1,4 @@ # IDE / Editors -.vscode .idea *.sublime-project *.sublime-workspace diff --git a/services/API-service/.node-version b/services/API-service/.node-version index cc5875fab..b26a23938 100644 --- a/services/API-service/.node-version +++ b/services/API-service/.node-version @@ -1 +1 @@ -v10.15.3 +v17.9.1 diff --git a/services/API-service/.prettierignore b/services/API-service/.prettierignore new file mode 100644 index 000000000..10ff87a63 --- /dev/null +++ b/services/API-service/.prettierignore @@ -0,0 +1,17 @@ +# Generated files +dist +coverage +www + + +# External code +node_modules + + +# Raster-files +geoserver-volume/raster-files/* +!geoserver-volume/raster-files/README.md + + +# certificates +cert/ diff --git a/services/API-service/.prettierrc.js b/services/API-service/.prettierrc.js index a183a7df1..d20840619 100644 --- a/services/API-service/.prettierrc.js +++ b/services/API-service/.prettierrc.js @@ -3,6 +3,9 @@ module.exports = { trailingComma: 'all', singleQuote: true, printWidth: 80, - 'no-parameter-properties': true, tabWidth: 2, + plugins: ['@ianvs/prettier-plugin-sort-imports'], + importOrder: ['^@nestjs', '', '', '', '^[.]'], + importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'], + importOrderTypeScriptVersion: '5.0.0', }; diff --git a/services/API-service/README.md b/services/API-service/README.md index 2125ac05b..6a16d5f2d 100644 --- a/services/API-service/README.md +++ b/services/API-service/README.md @@ -177,3 +177,7 @@ For the rest, follow the same instructions as above to receive initial and follo - Endpoint URL: `https://ibf.510.global/api/point-data/community-notification/${countryCodeISO3}` - To test this locally you can replace `ibf.510.global` by a local ngrok address - To demo on other environments, replace by respective environment-url, e.g. `ibf-test.510.global` + +## API tests + +1. Run them by `docker exec ibf-api-service npm run test:api:all` diff --git a/services/API-service/appdatasource.ts b/services/API-service/appdatasource.ts index e29a3ca46..b2789d5ac 100644 --- a/services/API-service/appdatasource.ts +++ b/services/API-service/appdatasource.ts @@ -1,3 +1,5 @@ import { DataSource, DataSourceOptions } from 'typeorm'; + import { ORMConfig } from './ormconfig'; + export const AppDataSource = new DataSource(ORMConfig as DataSourceOptions); diff --git a/services/API-service/jest.api.config.js b/services/API-service/jest.api.config.js new file mode 100644 index 000000000..bdb761c21 --- /dev/null +++ b/services/API-service/jest.api.config.js @@ -0,0 +1,16 @@ +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + moduleFileExtensions: ['js', 'ts'], + transform: { + '^.+\\.ts?$': ['ts-jest', { tsconfig: '/test/tsconfig.json' }], + }, + rootDir: '.', + testMatch: ['/test/**/*.test.ts'], + coverageReporters: ['json', 'lcov'], + modulePathIgnorePatterns: ['/dist/'], + testTimeout: 30_000, + verbose: true, + reporters: ['default'], +}; diff --git a/services/API-service/migration/1710512991479-rename-mock-rasters.ts b/services/API-service/migration/1710512991479-rename-mock-rasters.ts index 4ce5f8a52..d77f99b38 100644 --- a/services/API-service/migration/1710512991479-rename-mock-rasters.ts +++ b/services/API-service/migration/1710512991479-rename-mock-rasters.ts @@ -1,7 +1,8 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; import * as fs from 'fs'; import * as path from 'path'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + export class RenameMockRasters1710512991479 implements MigrationInterface { public async up(_queryRunner: QueryRunner): Promise { const directoryPath = './geoserver-volume/raster-files/mock-output/'; @@ -13,8 +14,6 @@ export class RenameMockRasters1710512991479 implements MigrationInterface { if (fs.existsSync(directoryPath)) { const files = fs.readdirSync(directoryPath); - console.log('🚀 ~ RenameMockRasters1710512991479 ~ up ~ files:', files); - files.forEach((file) => { if (!file.includes('hour_MWI')) { const newFilename = file.replace( diff --git a/services/API-service/ormconfig.ts b/services/API-service/ormconfig.ts index 54a054e9e..c5830ba14 100644 --- a/services/API-service/ormconfig.ts +++ b/services/API-service/ormconfig.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; + import { DataSourceOptions } from 'typeorm'; export const ORMConfig: DataSourceOptions = { diff --git a/services/API-service/package-lock.json b/services/API-service/package-lock.json index 1cb8720ab..592770c77 100644 --- a/services/API-service/package-lock.json +++ b/services/API-service/package-lock.json @@ -19,7 +19,10 @@ "class-transformer": "^0.3.1", "class-validator": "^0.14.0", "csv-parser": "^3.0.0", + "date-fns": "^3.6.0", + "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", + "juice": "^10.0.0", "mailchimp-api-v3": "^1.15.0", "mysql": "^2.15.0", "passport": "^0.4.1", @@ -34,6 +37,7 @@ "wkt-io-ts": "^1.0.2" }, "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.3.0", "@types/express": "^4.17.14", "@types/jest": "^26.0.20", "@types/node": "16.x", @@ -68,41 +72,77 @@ "node": ">=0.10.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.12.13" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.13.tgz", - "integrity": "sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.12.13", - "@babel/helper-module-transforms": "^7.12.13", - "@babel/helpers": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13", - "convert-source-map": "^1.7.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.19", - "semver": "^5.4.1", - "source-map": "^0.5.0" + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/@babel/core/node_modules/debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -116,13 +156,10 @@ } }, "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, "bin": { "json5": "lib/cli.js" }, @@ -136,106 +173,119 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/@babel/core/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dev": true, - "bin": { - "semver": "bin/semver" + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/core/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" } }, - "node_modules/@babel/generator": { - "version": "7.12.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.15.tgz", - "integrity": "sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ==", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" + "yallist": "^3.0.2" } }, - "node_modules/@babel/generator/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, - "node_modules/@babel/helper-function-name": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", - "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, "dependencies": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-get-function-arity": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", - "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz", - "integrity": "sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz", - "integrity": "sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.13.tgz", - "integrity": "sha512-acKF7EjqOR67ASIlDTupwkKM1eUisNAjaSduo5Cz+793ikfnpe7p4Q7B7EWU2PCoSTPWsQkR7hRUWEIZPiVLGA==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-replace-supers": "^7.12.13", - "@babel/helper-simple-access": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.12.11", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13", - "lodash": "^4.17.19" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", - "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-plugin-utils": { @@ -244,68 +294,90 @@ "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==", "dev": true }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz", - "integrity": "sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg==", + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.12.13", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz", - "integrity": "sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==", + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", - "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "dev": true, - "dependencies": { - "@babel/types": "^7.12.13" + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", - "dev": true + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, "node_modules/@babel/helpers": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.13.tgz", - "integrity": "sha512-oohVzLRZ3GQEk4Cjhfs9YkJA4TdIDTObdBEZGrd6F/T0GPSnuV6l22eMcxlvcvzVIPH3VTtxbseudM1zIE+rPQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dev": true, "dependencies": { - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", - "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.12.11", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.12.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.15.tgz", - "integrity": "sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -324,9 +396,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", - "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -335,31 +407,38 @@ } }, "node_modules/@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz", - "integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.12.13", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/traverse/node_modules/debug": { @@ -381,14 +460,17 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz", - "integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@cnakazawa/watch": { @@ -608,6 +690,41 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@ianvs/prettier-plugin-sort-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", + "integrity": "sha512-OOMtUcO4J3LoL63dOKAe7bn+lSRRPeit2DqNHpx+wvBp3Grejo2PMaK4Mp1mwy8pnat64ccSgk/lBZbsAdLErw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.0", + "@babel/generator": "^7.23.6", + "@babel/parser": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "semver": "^7.5.2" + }, + "peerDependencies": { + "@vue/compiler-sfc": "2.7.x || 3.x", + "prettier": "2 || 3" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, + "node_modules/@ianvs/prettier-plugin-sort-imports/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@jest/console": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", @@ -1442,6 +1559,20 @@ "node": ">=8" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -1451,12 +1582,31 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "devOptional": true }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -2883,6 +3033,14 @@ "node": ">=6" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3040,6 +3198,11 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -3345,6 +3508,11 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -3450,7 +3618,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3489,6 +3656,38 @@ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", "dev": true }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/bs-logger": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", @@ -3623,6 +3822,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001639", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", + "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -3675,6 +3894,65 @@ "node": ">=16" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", @@ -3947,6 +4225,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -3956,8 +4242,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "node_modules/concat-stream": { "version": "1.6.2", @@ -4223,6 +4508,32 @@ "node": ">=8" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", @@ -4286,18 +4597,12 @@ } }, "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" } }, "node_modules/dayjs": { @@ -4503,6 +4808,30 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, "node_modules/domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -4512,6 +4841,33 @@ "webidl-conversions": "^4.0.2" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -4569,6 +4925,26 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.815", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.815.tgz", + "integrity": "sha512-OvpTT2ItpOXJL7IGcYakRjHCt8L5GrrN/wHCQsRB4PQa1X9fe+X9oen245mIId7s14xvArCGSTIq644yPUKKLg==", + "dev": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4591,6 +4967,17 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -4640,9 +5027,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } @@ -5649,6 +6036,33 @@ "dev": true, "optional": true }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6185,6 +6599,24 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -6852,6 +7284,87 @@ "node": ">=6" } }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", @@ -9400,6 +9913,24 @@ "verror": "1.10.0" } }, + "node_modules/juice": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-10.0.0.tgz", + "integrity": "sha512-9f68xmhGrnIi6DBkiiP3rUrQN33SEuaKu1+njX6VgMP+jwZAsnT33WIzlrWICL9matkhYu3OyrqSUP55YTIdGg==", + "dependencies": { + "cheerio": "^1.0.0-rc.12", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -9714,6 +10245,11 @@ "node": ">= 0.6" } }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -9798,7 +10334,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -10083,6 +10618,12 @@ "semver": "bin/semver" } }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "node_modules/nodemon": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", @@ -10182,6 +10723,17 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -10670,6 +11222,12 @@ "node": ">= 10.x" } }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -11100,9 +11658,9 @@ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regex-not": { "version": "1.0.2", @@ -11665,9 +12223,9 @@ "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -11838,6 +12396,14 @@ "node": ">=8" } }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "engines": { + "node": "*" + } + }, "node_modules/slug": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/slug/-/slug-4.0.2.tgz", @@ -13145,6 +13711,21 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/typeorm/node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/typeorm/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -13379,6 +13960,36 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/update-notifier": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", @@ -13542,6 +14153,14 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "engines": { + "node": ">=10" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -13604,6 +14223,134 @@ "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.8.tgz", "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==" }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -13883,38 +14630,61 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "requires": { - "@babel/highlight": "^7.12.13" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" } }, + "@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true + }, "@babel/core": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.13.tgz", - "integrity": "sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.12.13", - "@babel/helper-module-transforms": "^7.12.13", - "@babel/helpers": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13", - "convert-source-map": "^1.7.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.19", - "semver": "^5.4.1", - "source-map": "^0.5.0" + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -13925,115 +14695,110 @@ } }, "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true } } }, "@babel/generator": { - "version": "7.12.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.15.tgz", - "integrity": "sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dev": true, "requires": { - "@babel/types": "^7.12.13", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true } } }, - "@babel/helper-function-name": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", - "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" } }, - "@babel/helper-get-function-arity": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", - "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" } }, - "@babel/helper-member-expression-to-functions": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz", - "integrity": "sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ==", + "@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" } }, "@babel/helper-module-imports": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz", - "integrity": "sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-module-transforms": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.13.tgz", - "integrity": "sha512-acKF7EjqOR67ASIlDTupwkKM1eUisNAjaSduo5Cz+793ikfnpe7p4Q7B7EWU2PCoSTPWsQkR7hRUWEIZPiVLGA==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-replace-supers": "^7.12.13", - "@babel/helper-simple-access": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.12.11", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13", - "lodash": "^4.17.19" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", - "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" } }, "@babel/helper-plugin-utils": { @@ -14042,68 +14807,69 @@ "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==", "dev": true }, - "@babel/helper-replace-supers": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz", - "integrity": "sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.12.13", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13" - } - }, "@babel/helper-simple-access": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz", - "integrity": "sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-split-export-declaration": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", - "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" } }, + "@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true + }, "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "dev": true }, "@babel/helpers": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.13.tgz", - "integrity": "sha512-oohVzLRZ3GQEk4Cjhfs9YkJA4TdIDTObdBEZGrd6F/T0GPSnuV6l22eMcxlvcvzVIPH3VTtxbseudM1zIE+rPQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dev": true, "requires": { - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/highlight": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", - "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" } }, "@babel/parser": { - "version": "7.12.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.15.tgz", - "integrity": "sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true }, "@babel/plugin-syntax-object-rest-spread": { @@ -14116,39 +14882,40 @@ } }, "@babel/runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", - "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", "requires": { "regenerator-runtime": "^0.14.0" } }, "@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/traverse": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz", - "integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.12.13", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" }, "dependencies": { "debug": { @@ -14169,13 +14936,13 @@ } }, "@babel/types": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz", - "integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" } }, @@ -14333,6 +15100,28 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@ianvs/prettier-plugin-sort-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", + "integrity": "sha512-OOMtUcO4J3LoL63dOKAe7bn+lSRRPeit2DqNHpx+wvBp3Grejo2PMaK4Mp1mwy8pnat64ccSgk/lBZbsAdLErw==", + "dev": true, + "requires": { + "@babel/core": "^7.24.0", + "@babel/generator": "^7.23.6", + "@babel/parser": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "semver": "^7.5.2" + }, + "dependencies": { + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + } + } + }, "@jest/console": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", @@ -15047,18 +15836,45 @@ } } }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "devOptional": true }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "devOptional": true }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -16000,6 +16816,11 @@ } } }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==" + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -16121,6 +16942,11 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -16379,6 +17205,11 @@ "unpipe": "1.0.0" } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -16462,7 +17293,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -16500,6 +17330,18 @@ } } }, + "browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + } + }, "bs-logger": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", @@ -16606,6 +17448,12 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" }, + "caniuse-lite": { + "version": "1.0.30001639", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", + "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==", + "dev": true + }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -16646,6 +17494,52 @@ "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz", "integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==" }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "dependencies": { + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + } + } + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, "chokidar": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", @@ -16863,6 +17757,11 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -16872,8 +17771,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -17085,6 +17983,23 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", @@ -17141,12 +18056,9 @@ } }, "date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "requires": { - "@babel/runtime": "^7.21.0" - } + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==" }, "dayjs": { "version": "1.11.6", @@ -17301,6 +18213,21 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -17310,6 +18237,24 @@ "webidl-conversions": "^4.0.2" } }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -17358,6 +18303,20 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.4.815", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.815.tgz", + "integrity": "sha512-OvpTT2ItpOXJL7IGcYakRjHCt8L5GrrN/wHCQsRB4PQa1X9fe+X9oen245mIId7s14xvArCGSTIq644yPUKKLg==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -17377,6 +18336,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -17420,9 +18384,9 @@ } }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" }, "escape-goat": { "version": "2.1.1", @@ -18179,6 +19143,32 @@ "dev": true, "optional": true }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -18598,6 +19588,17 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -19122,6 +20123,62 @@ "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==" }, + "jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", @@ -21295,6 +22352,18 @@ "verror": "1.10.0" } }, + "juice": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-10.0.0.tgz", + "integrity": "sha512-9f68xmhGrnIi6DBkiiP3rUrQN33SEuaKu1+njX6VgMP+jwZAsnT33WIzlrWICL9matkhYu3OyrqSUP55YTIdGg==", + "requires": { + "cheerio": "^1.0.0-rc.12", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + } + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -21562,6 +22631,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, + "mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==" + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -21622,7 +22696,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -21876,6 +22949,12 @@ } } }, + "node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "nodemon": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", @@ -21958,6 +23037,14 @@ "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "dev": true }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -22341,6 +23428,12 @@ } } }, + "picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -22659,9 +23752,9 @@ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, "regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regex-not": { "version": "1.0.2", @@ -23120,9 +24213,9 @@ "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "semver-diff": { @@ -23264,6 +24357,11 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==" + }, "slug": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/slug/-/slug-4.0.2.tgz", @@ -24234,6 +25332,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -24405,6 +25511,16 @@ } } }, + "update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "requires": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + } + }, "update-notifier": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", @@ -24540,6 +25656,11 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -24593,6 +25714,95 @@ "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.8.tgz", "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==" }, + "web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "requires": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "dependencies": { + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "requires": { + "domelementtype": "^2.2.0" + } + } + } + }, + "domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "requires": { + "domelementtype": "^2.0.1" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "dependencies": { + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "requires": { + "domelementtype": "^2.2.0" + } + } + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==" + }, + "htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + } + } + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/services/API-service/package.json b/services/API-service/package.json index d85d4cb1b..7017a7564 100644 --- a/services/API-service/package.json +++ b/services/API-service/package.json @@ -16,6 +16,8 @@ "test": "jest --config=jest.json --detectOpenHandles --forceExit --passWithNoTests", "test:dev": "npm test -- --watchAll", "test:coverage": "npm test -- --coverage --coverageDirectory=coverage", + "test:api:all": "node --expose-gc node_modules/.bin/jest --config=jest.api.config.js --runInBand --detectOpenHandles --logHeapUsage", + "test:api:watch": "npm run test:api:all -- --watchAll", "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", "migration:generate": "npm run typeorm migration:generate -- -d ./appdatasource.ts", "migration:run": "npm run typeorm migration:run -- -d ./appdatasource.ts", @@ -37,7 +39,10 @@ "class-transformer": "^0.3.1", "class-validator": "^0.14.0", "csv-parser": "^3.0.0", + "date-fns": "^3.6.0", + "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", + "juice": "^10.0.0", "mailchimp-api-v3": "^1.15.0", "mysql": "^2.15.0", "passport": "^0.4.1", @@ -52,6 +57,7 @@ "wkt-io-ts": "^1.0.2" }, "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.3.0", "@types/express": "^4.17.14", "@types/jest": "^26.0.20", "@types/node": "16.x", diff --git a/services/API-service/src/api/admin-area-data/admin-area-data.controller.ts b/services/API-service/src/api/admin-area-data/admin-area-data.controller.ts index c6df15115..e7a1d091a 100644 --- a/services/API-service/src/api/admin-area-data/admin-area-data.controller.ts +++ b/services/API-service/src/api/admin-area-data/admin-area-data.controller.ts @@ -18,13 +18,14 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; +import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; import { AdminDataReturnDto } from '../admin-area-dynamic-data/dto/admin-data-return.dto'; import { UserRole } from '../user/user-role.enum'; import { AdminAreaDataService } from './admin-area-data.service'; import { UploadAdminAreaDataJsonDto } from './dto/upload-admin-area-data.dto'; -import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; @ApiBearerAuth() @UseGuards(RolesGuard) diff --git a/services/API-service/src/api/admin-area-data/admin-area-data.entity.ts b/services/API-service/src/api/admin-area-data/admin-area-data.entity.ts index 8e814fb22..398ab633a 100644 --- a/services/API-service/src/api/admin-area-data/admin-area-data.entity.ts +++ b/services/API-service/src/api/admin-area-data/admin-area-data.entity.ts @@ -1,11 +1,12 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, - JoinColumn, + Entity, Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { AdminLevel } from '../country/admin-level.enum'; import { CountryEntity } from '../country/country.entity'; diff --git a/services/API-service/src/api/admin-area-data/admin-area-data.module.ts b/services/API-service/src/api/admin-area-data/admin-area-data.module.ts index c2fe9c72f..94204c953 100644 --- a/services/API-service/src/api/admin-area-data/admin-area-data.module.ts +++ b/services/API-service/src/api/admin-area-data/admin-area-data.module.ts @@ -1,11 +1,12 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; import { UserModule } from '../user/user.module'; import { AdminAreaDataController } from './admin-area-data.controller'; import { AdminAreaDataEntity } from './admin-area-data.entity'; import { AdminAreaDataService } from './admin-area-data.service'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/admin-area-data/admin-area-data.service.ts b/services/API-service/src/api/admin-area-data/admin-area-data.service.ts index aaf578c41..3ca40b22c 100644 --- a/services/API-service/src/api/admin-area-data/admin-area-data.service.ts +++ b/services/API-service/src/api/admin-area-data/admin-area-data.service.ts @@ -1,15 +1,17 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + +import { validate } from 'class-validator'; import { Repository } from 'typeorm'; + +import { HelperService } from '../../shared/helper.service'; +import { AdminDataReturnDto } from '../admin-area-dynamic-data/dto/admin-data-return.dto'; +import { UpdateableStaticIndicator } from '../admin-area-dynamic-data/enum/dynamic-data-unit'; import { AdminAreaDataEntity } from './admin-area-data.entity'; import { UploadAdminAreaDataDto, UploadAdminAreaDataJsonDto, } from './dto/upload-admin-area-data.dto'; -import { validate } from 'class-validator'; -import { AdminDataReturnDto } from '../admin-area-dynamic-data/dto/admin-data-return.dto'; -import { HelperService } from '../../shared/helper.service'; -import { UpdateableStaticIndicator } from '../admin-area-dynamic-data/enum/dynamic-data-unit'; @Injectable() export class AdminAreaDataService { diff --git a/services/API-service/src/api/admin-area-data/dto/upload-admin-area-data.dto.ts b/services/API-service/src/api/admin-area-data/dto/upload-admin-area-data.dto.ts index 7d6b14912..550f86077 100644 --- a/services/API-service/src/api/admin-area-data/dto/upload-admin-area-data.dto.ts +++ b/services/API-service/src/api/admin-area-data/dto/upload-admin-area-data.dto.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsEnum, @@ -7,14 +10,13 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ManyToOne, JoinColumn } from 'typeorm'; +import { JoinColumn, ManyToOne } from 'typeorm'; + import { DynamicDataPlaceCodeDto } from '../../admin-area-dynamic-data/dto/dynamic-data-place-code.dto'; +import indicatorData from '../../admin-area-dynamic-data/dto/example/ETH/malaria/upload-potential_cases-3.json'; import { UpdateableStaticIndicator } from '../../admin-area-dynamic-data/enum/dynamic-data-unit'; import { AdminLevel } from '../../country/admin-level.enum'; import { CountryEntity } from '../../country/country.entity'; -import indicatorData from '../../admin-area-dynamic-data/dto/example/ETH/malaria/upload-potential_cases-3.json'; export class UploadAdminAreaDataDto { @ApiProperty() diff --git a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.controller.ts b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.controller.ts index d85aedc5b..689043455 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.controller.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.controller.ts @@ -1,26 +1,35 @@ -import { AdminDataReturnDto } from './dto/admin-data-return.dto'; -import { DynamicIndicator } from './enum/dynamic-data-unit'; -import { Body, Get, Param, UploadedFile } from '@nestjs/common'; -import { Controller, Post, UseGuards, UseInterceptors } from '@nestjs/common'; import { - ApiOperation, - ApiConsumes, + Body, + Controller, + Get, + Param, + Post, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { Query } from '@nestjs/common/decorators'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiBearerAuth, - ApiTags, - ApiParam, ApiBody, - ApiResponse, + ApiConsumes, + ApiOperation, + ApiParam, ApiQuery, + ApiResponse, + ApiTags, } from '@nestjs/swagger'; + +import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; -import { UploadAdminAreaDynamicDataDto } from './dto/upload-admin-area-dynamic-data.dto'; -import { AdminAreaDynamicDataService } from './admin-area-dynamic-data.service'; +import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; import { DisasterType } from '../disaster/disaster-type.enum'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { Roles } from '../../roles.decorator'; import { UserRole } from '../user/user-role.enum'; -import { Query } from '@nestjs/common/decorators'; -import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; +import { AdminAreaDynamicDataService } from './admin-area-dynamic-data.service'; +import { AdminDataReturnDto } from './dto/admin-data-return.dto'; +import { UploadAdminAreaDynamicDataDto } from './dto/upload-admin-area-dynamic-data.dto'; +import { DynamicIndicator } from './enum/dynamic-data-unit'; @ApiBearerAuth() @UseGuards(RolesGuard) diff --git a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.entity.ts b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.entity.ts index 4d1143d6e..de6f17225 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.entity.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.entity.ts @@ -1,11 +1,12 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, - JoinColumn, + Entity, Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; diff --git a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.module.ts b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.module.ts index f7b529db9..268de513f 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.module.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.module.ts @@ -1,16 +1,17 @@ -import { CountryModule } from './../country/country.module'; -import { UserModule } from '../user/user.module'; -import { AdminAreaDynamicDataService } from './admin-area-dynamic-data.service'; -import { AdminAreaDynamicDataController } from './admin-area-dynamic-data.controller'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { TriggerPerLeadTime } from '../event/trigger-per-lead-time.entity'; -import { AdminAreaDynamicDataEntity } from './admin-area-dynamic-data.entity'; -import { EventModule } from '../event/event.module'; -import { DisasterEntity } from '../disaster/disaster.entity'; -import { CountryEntity } from '../country/country.entity'; + import { HelperService } from '../../shared/helper.service'; import { AdminAreaModule } from '../admin-area/admin-area.module'; +import { CountryEntity } from '../country/country.entity'; +import { DisasterEntity } from '../disaster/disaster.entity'; +import { EventModule } from '../event/event.module'; +import { TriggerPerLeadTime } from '../event/trigger-per-lead-time.entity'; +import { UserModule } from '../user/user.module'; +import { CountryModule } from './../country/country.module'; +import { AdminAreaDynamicDataController } from './admin-area-dynamic-data.controller'; +import { AdminAreaDynamicDataEntity } from './admin-area-dynamic-data.entity'; +import { AdminAreaDynamicDataService } from './admin-area-dynamic-data.service'; @Module({ imports: [ diff --git a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts index e917a073c..6dbe706f6 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts @@ -1,21 +1,28 @@ -import { LeadTime } from './enum/lead-time.enum'; -import { DynamicDataPlaceCodeDto } from './dto/dynamic-data-place-code.dto'; +import fs from 'fs'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { DataSource, In, IsNull, MoreThanOrEqual, Repository } from 'typeorm'; -import { UploadAdminAreaDynamicDataDto } from './dto/upload-admin-area-dynamic-data.dto'; import { InjectRepository } from '@nestjs/typeorm'; -import { AdminAreaDynamicDataEntity } from './admin-area-dynamic-data.entity'; -import { DynamicIndicator } from './enum/dynamic-data-unit'; -import { AdminDataReturnDto } from './dto/admin-data-return.dto'; -import { UploadTriggerPerLeadTimeDto } from '../event/dto/upload-trigger-per-leadtime.dto'; -import { EventService } from '../event/event.service'; -import { DisasterEntity } from '../disaster/disaster.entity'; -import { DisasterType } from '../disaster/disaster-type.enum'; -import fs from 'fs'; -import { CountryEntity } from '../country/country.entity'; + +import { DataSource, In, IsNull, MoreThanOrEqual, Repository } from 'typeorm'; + +import { DisasterTypeGeoServerMapper } from '../../scripts/disaster-type-geoserver-file.mapper'; import { HelperService } from '../../shared/helper.service'; import { EventAreaService } from '../admin-area/services/event-area.service'; -import { DisasterTypeGeoServerMapper } from '../../scripts/disaster-type-geoserver-file.mapper'; +import { CountryEntity } from '../country/country.entity'; +import { DisasterType } from '../disaster/disaster-type.enum'; +import { DisasterEntity } from '../disaster/disaster.entity'; +import { UploadTriggerPerLeadTimeDto } from '../event/dto/upload-trigger-per-leadtime.dto'; +import { EventService } from '../event/event.service'; +import { AdminAreaDynamicDataEntity } from './admin-area-dynamic-data.entity'; +import { AdminDataReturnDto } from './dto/admin-data-return.dto'; +import { DynamicDataPlaceCodeDto } from './dto/dynamic-data-place-code.dto'; +import { UploadAdminAreaDynamicDataDto } from './dto/upload-admin-area-dynamic-data.dto'; +import { DynamicIndicator } from './enum/dynamic-data-unit'; +import { LeadTime } from './enum/lead-time.enum'; + +interface RasterData { + originalname: string; + buffer: Buffer; +} @Injectable() export class AdminAreaDynamicDataService { @@ -232,9 +239,9 @@ export class AdminAreaDynamicDataService { const result = await this.adminAreaDynamicDataRepo .createQueryBuilder('dynamic') .where({ - indicator: indicator, - placeCode: placeCode, - leadTime: leadTime, + indicator, + placeCode, + leadTime, eventName: eventName === 'no-name' || !eventName ? IsNull() : eventName, }) .select(['dynamic.value AS value']) @@ -244,7 +251,7 @@ export class AdminAreaDynamicDataService { } public async postRaster( - data: any, + data: RasterData, disasterType: DisasterType, ): Promise { const subfolder = diff --git a/services/API-service/src/api/admin-area-dynamic-data/dto/dynamic-data-place-code.dto.ts b/services/API-service/src/api/admin-area-dynamic-data/dto/dynamic-data-place-code.dto.ts index 003620ec3..efee267fe 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/dto/dynamic-data-place-code.dto.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/dto/dynamic-data-place-code.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; + export class DynamicDataPlaceCodeDto { @ApiProperty() @IsNotEmpty() diff --git a/services/API-service/src/api/admin-area-dynamic-data/dto/upload-admin-area-dynamic-data.dto.ts b/services/API-service/src/api/admin-area-dynamic-data/dto/upload-admin-area-dynamic-data.dto.ts index f93be9656..bd314f830 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/dto/upload-admin-area-dynamic-data.dto.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/dto/upload-admin-area-dynamic-data.dto.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsEnum, @@ -7,13 +10,12 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { Type } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; + +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { DynamicIndicator } from '../enum/dynamic-data-unit'; +import { LeadTime } from '../enum/lead-time.enum'; import { DynamicDataPlaceCodeDto } from './dynamic-data-place-code.dto'; import exposure from './example/PHL/dengue/upload-potential_cases-2.json'; -import { LeadTime } from '../enum/lead-time.enum'; -import { DynamicIndicator } from '../enum/dynamic-data-unit'; -import { DisasterType } from '../../disaster/disaster-type.enum'; export class UploadAdminAreaDynamicDataDto { @ApiProperty({ example: 'PHL' }) diff --git a/services/API-service/src/api/admin-area/admin-area.controller.ts b/services/API-service/src/api/admin-area/admin-area.controller.ts index 8146c0893..c497f97e6 100644 --- a/services/API-service/src/api/admin-area/admin-area.controller.ts +++ b/services/API-service/src/api/admin-area/admin-area.controller.ts @@ -17,13 +17,14 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; -import { GeoJson } from '../../shared/geo.model'; + +import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; -import { AdminAreaService } from './admin-area.service'; import { AggregateDataRecord } from '../../shared/data.model'; -import { AdminAreaEntity } from './admin-area.entity'; -import { Roles } from '../../roles.decorator'; +import { GeoJson } from '../../shared/geo.model'; import { UserRole } from '../user/user-role.enum'; +import { AdminAreaEntity } from './admin-area.entity'; +import { AdminAreaService } from './admin-area.service'; @ApiBearerAuth() @UseGuards(RolesGuard) @@ -69,7 +70,7 @@ export class AdminAreaController { type: [AdminAreaEntity], }) @Get('raw/:countryCodeISO3') - public async getAdminAreasRaw(@Param() params): Promise { + public async getAdminAreasRaw(@Param() params) { return await this.adminAreaService.getAdminAreasRaw(params.countryCodeISO3); } diff --git a/services/API-service/src/api/admin-area/admin-area.entity.ts b/services/API-service/src/api/admin-area/admin-area.entity.ts index f1acf5042..f4af44831 100644 --- a/services/API-service/src/api/admin-area/admin-area.entity.ts +++ b/services/API-service/src/api/admin-area/admin-area.entity.ts @@ -1,14 +1,16 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, - JoinColumn, - OneToMany, + Entity, Index, + JoinColumn, + ManyToOne, MultiPolygon, + OneToMany, + PrimaryGeneratedColumn, } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; diff --git a/services/API-service/src/api/admin-area/admin-area.module.ts b/services/API-service/src/api/admin-area/admin-area.module.ts index f5e3ca1ff..37ab73e4c 100644 --- a/services/API-service/src/api/admin-area/admin-area.module.ts +++ b/services/API-service/src/api/admin-area/admin-area.module.ts @@ -1,18 +1,19 @@ -import { CountryModule } from './../country/country.module'; +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; +import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { CountryEntity } from '../country/country.entity'; +import { DisasterEntity } from '../disaster/disaster.entity'; import { EventModule } from '../event/event.module'; import { UserModule } from '../user/user.module'; +import { CountryModule } from './../country/country.module'; import { AdminAreaController } from './admin-area.controller'; import { AdminAreaEntity } from './admin-area.entity'; import { AdminAreaService } from './admin-area.service'; -import { DisasterEntity } from '../disaster/disaster.entity'; -import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; -import { HttpModule } from '@nestjs/axios'; -import { EventAreaService } from './services/event-area.service'; import { EventAreaEntity } from './event-area.entity'; +import { EventAreaService } from './services/event-area.service'; @Module({ imports: [ diff --git a/services/API-service/src/api/admin-area/admin-area.service.ts b/services/API-service/src/api/admin-area/admin-area.service.ts index 959f18ce8..43c98c262 100644 --- a/services/API-service/src/api/admin-area/admin-area.service.ts +++ b/services/API-service/src/api/admin-area/admin-area.service.ts @@ -1,17 +1,19 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { GeoJson } from '../../shared/geo.model'; -import { HelperService } from '../../shared/helper.service'; + import { InsertResult, MoreThan, MoreThanOrEqual, Repository } from 'typeorm'; -import { AdminAreaEntity } from './admin-area.entity'; -import { EventService } from '../event/event.service'; + import { AggregateDataRecord } from '../../shared/data.model'; -import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; +import { GeoJson } from '../../shared/geo.model'; +import { HelperService } from '../../shared/helper.service'; import { AdminAreaDataEntity } from '../admin-area-data/admin-area-data.entity'; -import { DisasterType } from '../disaster/disaster-type.enum'; -import { DisasterEntity } from '../disaster/disaster.entity'; +import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { DynamicIndicator } from '../admin-area-dynamic-data/enum/dynamic-data-unit'; import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; +import { DisasterType } from '../disaster/disaster-type.enum'; +import { DisasterEntity } from '../disaster/disaster.entity'; +import { EventService } from '../event/event.service'; +import { AdminAreaEntity } from './admin-area.entity'; import { EventAreaService } from './services/event-area.service'; @Injectable() @@ -292,7 +294,7 @@ export class AdminAreaService { }); } - public async getAdminAreasRaw(countryCodeISO3): Promise { + public async getAdminAreasRaw(countryCodeISO3) { return await this.adminAreaRepository.find({ select: [ 'countryCodeISO3', diff --git a/services/API-service/src/api/admin-area/event-area.entity.ts b/services/API-service/src/api/admin-area/event-area.entity.ts index e79c87e29..085245d30 100644 --- a/services/API-service/src/api/admin-area/event-area.entity.ts +++ b/services/API-service/src/api/admin-area/event-area.entity.ts @@ -1,12 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, + Entity, JoinColumn, + ManyToOne, MultiPolygon, + PrimaryGeneratedColumn, } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; import { DisasterType } from '../disaster/disaster-type.enum'; import { DisasterEntity } from '../disaster/disaster.entity'; diff --git a/services/API-service/src/api/admin-area/services/event-area.service.ts b/services/API-service/src/api/admin-area/services/event-area.service.ts index 42f6f65ec..b292772d3 100644 --- a/services/API-service/src/api/admin-area/services/event-area.service.ts +++ b/services/API-service/src/api/admin-area/services/event-area.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, MoreThanOrEqual, InsertResult } from 'typeorm'; + +import { InsertResult, MoreThanOrEqual, Repository } from 'typeorm'; + import { AggregateDataRecord, EventSummaryCountry, diff --git a/services/API-service/src/api/country/country-disaster.entity.ts b/services/API-service/src/api/country/country-disaster.entity.ts index 8f53d8cf2..68655d202 100644 --- a/services/API-service/src/api/country/country-disaster.entity.ts +++ b/services/API-service/src/api/country/country-disaster.entity.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; + import { Column, Entity, @@ -8,6 +9,7 @@ import { ManyToOne, PrimaryGeneratedColumn, } from 'typeorm'; + import { DisasterType } from '../disaster/disaster-type.enum'; import { DisasterEntity } from '../disaster/disaster.entity'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; diff --git a/services/API-service/src/api/country/country-time-zone-mapping.ts b/services/API-service/src/api/country/country-time-zone-mapping.ts new file mode 100644 index 000000000..67cf4657c --- /dev/null +++ b/services/API-service/src/api/country/country-time-zone-mapping.ts @@ -0,0 +1,11 @@ +export const CountryTimeZoneMapping: { [key: string]: string } = { + UGA: 'Africa/Kampala', + KEN: 'Africa/Nairobi', + ETH: 'Africa/Addis_Ababa', + ZMB: 'Africa/Lusaka', + MWI: 'Africa/Blantyre', + ZWE: 'Africa/Harare', + EGY: 'Africa/Cairo', + PHL: 'Asia/Manila', + SSD: 'Africa/Juba', +}; diff --git a/services/API-service/src/api/country/country.controller.ts b/services/API-service/src/api/country/country.controller.ts index 85dec28eb..f713ea67b 100644 --- a/services/API-service/src/api/country/country.controller.ts +++ b/services/API-service/src/api/country/country.controller.ts @@ -6,12 +6,13 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; -import { NotificationInfoDto } from './dto/notification-info.dto'; import { UserRole } from '../user/user-role.enum'; import { CountryEntity } from './country.entity'; import { CountryService } from './country.service'; import { AddCountriesDto } from './dto/add-countries.dto'; +import { NotificationInfoDto } from './dto/notification-info.dto'; @ApiBearerAuth() @ApiTags('country') diff --git a/services/API-service/src/api/country/country.entity.ts b/services/API-service/src/api/country/country.entity.ts index 2b42542e6..85e4f75b1 100644 --- a/services/API-service/src/api/country/country.entity.ts +++ b/services/API-service/src/api/country/country.entity.ts @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { Column, Entity, @@ -7,11 +9,11 @@ import { OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; + import { BoundingBox } from '../../shared/geo.model'; -import { UserEntity } from '../user/user.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; import { NotificationInfoEntity } from '../notification/notifcation-info.entity'; -import { ApiProperty } from '@nestjs/swagger'; +import { UserEntity } from '../user/user.entity'; import { CountryDisasterSettingsEntity } from './country-disaster.entity'; @Entity('country') diff --git a/services/API-service/src/api/country/country.module.ts b/services/API-service/src/api/country/country.module.ts index e3e94e2fd..f0850e486 100644 --- a/services/API-service/src/api/country/country.module.ts +++ b/services/API-service/src/api/country/country.module.ts @@ -1,5 +1,7 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { DisasterEntity } from '../disaster/disaster.entity'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; import { NotificationInfoEntity } from '../notification/notifcation-info.entity'; @@ -8,7 +10,6 @@ import { CountryDisasterSettingsEntity } from './country-disaster.entity'; import { CountryController } from './country.controller'; import { CountryEntity } from './country.entity'; import { CountryService } from './country.service'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/country/country.service.ts b/services/API-service/src/api/country/country.service.ts index ffb6a04a1..8d96324e2 100644 --- a/services/API-service/src/api/country/country.service.ts +++ b/services/API-service/src/api/country/country.service.ts @@ -1,10 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { In, Repository } from 'typeorm'; + import { DisasterType } from '../disaster/disaster-type.enum'; import { DisasterEntity } from '../disaster/disaster.entity'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; -import { NotificationInfoDto } from './dto/notification-info.dto'; +import { NotificationInfoEntity } from '../notification/notifcation-info.entity'; import { AdminLevel } from './admin-level.enum'; import { CountryDisasterSettingsEntity } from './country-disaster.entity'; import { CountryEntity } from './country.entity'; @@ -13,7 +15,7 @@ import { CountryDisasterSettingsDto, CountryDto, } from './dto/add-countries.dto'; -import { NotificationInfoEntity } from '../notification/notifcation-info.entity'; +import { NotificationInfoDto } from './dto/notification-info.dto'; @Injectable() export class CountryService { diff --git a/services/API-service/src/api/country/dto/add-countries.dto.ts b/services/API-service/src/api/country/dto/add-countries.dto.ts index 52adfd06d..5ff0f9879 100644 --- a/services/API-service/src/api/country/dto/add-countries.dto.ts +++ b/services/API-service/src/api/country/dto/add-countries.dto.ts @@ -1,5 +1,7 @@ -import { IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; + +import { IsNotEmpty } from 'class-validator'; + import { BoundingBox } from '../../../shared/geo.model'; import { DisasterType } from '../../disaster/disaster-type.enum'; import { AdminLevel } from '../admin-level.enum'; diff --git a/services/API-service/src/api/country/dto/notification-info.dto.ts b/services/API-service/src/api/country/dto/notification-info.dto.ts index bd27c5d29..8374168d6 100644 --- a/services/API-service/src/api/country/dto/notification-info.dto.ts +++ b/services/API-service/src/api/country/dto/notification-info.dto.ts @@ -1,6 +1,7 @@ -import { IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + export class NotificationInfoDto { @ApiProperty() @IsString() diff --git a/services/API-service/src/api/disaster/disaster.entity.ts b/services/API-service/src/api/disaster/disaster.entity.ts index cda1131a4..b648e2a53 100644 --- a/services/API-service/src/api/disaster/disaster.entity.ts +++ b/services/API-service/src/api/disaster/disaster.entity.ts @@ -1,4 +1,5 @@ -import { CountryEntity } from './../country/country.entity'; +import { ApiProperty } from '@nestjs/swagger'; + import { Column, Entity, @@ -6,13 +7,14 @@ import { ManyToMany, PrimaryGeneratedColumn, } from 'typeorm'; -import { DisasterType } from './disaster-type.enum'; -import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime, LeadTimeUnit, } from '../admin-area-dynamic-data/enum/lead-time.enum'; import { UserEntity } from '../user/user.entity'; +import { CountryEntity } from './../country/country.entity'; +import { DisasterType } from './disaster-type.enum'; @Entity('disaster') export class DisasterEntity { diff --git a/services/API-service/src/api/eap-actions/area-of-focus.entity.ts b/services/API-service/src/api/eap-actions/area-of-focus.entity.ts index fc097d256..2e1cf58a1 100644 --- a/services/API-service/src/api/eap-actions/area-of-focus.entity.ts +++ b/services/API-service/src/api/eap-actions/area-of-focus.entity.ts @@ -1,5 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Entity, Column, OneToMany, PrimaryColumn } from 'typeorm'; + +import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm'; + import { EapActionEntity } from './eap-action.entity'; @Entity('area-of-focus') diff --git a/services/API-service/src/api/eap-actions/dto/check-eap-action.dto.ts b/services/API-service/src/api/eap-actions/dto/check-eap-action.dto.ts index 0e65837a1..52afc0877 100644 --- a/services/API-service/src/api/eap-actions/dto/check-eap-action.dto.ts +++ b/services/API-service/src/api/eap-actions/dto/check-eap-action.dto.ts @@ -1,6 +1,7 @@ -import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + export class CheckEapActionDto { @ApiProperty() @IsNotEmpty() diff --git a/services/API-service/src/api/eap-actions/dto/eap-action.dto.ts b/services/API-service/src/api/eap-actions/dto/eap-action.dto.ts index 11f9f4c63..8c8ae807c 100644 --- a/services/API-service/src/api/eap-actions/dto/eap-action.dto.ts +++ b/services/API-service/src/api/eap-actions/dto/eap-action.dto.ts @@ -1,7 +1,9 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -import { AreaOfFocusEntity } from '../area-of-focus.entity'; + +import { IsNotEmpty, IsString } from 'class-validator'; + import { DisasterType } from '../../disaster/disaster-type.enum'; +import { AreaOfFocusEntity } from '../area-of-focus.entity'; class EapActionDto { @ApiProperty({ example: 'UGA' }) diff --git a/services/API-service/src/api/eap-actions/eap-action-status.entity.ts b/services/API-service/src/api/eap-actions/eap-action-status.entity.ts index 5b7ef3a11..ac934d15d 100644 --- a/services/API-service/src/api/eap-actions/eap-action-status.entity.ts +++ b/services/API-service/src/api/eap-actions/eap-action-status.entity.ts @@ -1,11 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, + Entity, Index, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; import { UserEntity } from '../user/user.entity'; import { EapActionEntity } from './eap-action.entity'; diff --git a/services/API-service/src/api/eap-actions/eap-action.entity.ts b/services/API-service/src/api/eap-actions/eap-action.entity.ts index ab70d6749..51ac4442b 100644 --- a/services/API-service/src/api/eap-actions/eap-action.entity.ts +++ b/services/API-service/src/api/eap-actions/eap-action.entity.ts @@ -1,11 +1,12 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - OneToMany, - ManyToOne, + Entity, JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; import { AreaOfFocusEntity } from './area-of-focus.entity'; diff --git a/services/API-service/src/api/eap-actions/eap-actions.controller.ts b/services/API-service/src/api/eap-actions/eap-actions.controller.ts index d2bb7d277..155f3348a 100644 --- a/services/API-service/src/api/eap-actions/eap-actions.controller.ts +++ b/services/API-service/src/api/eap-actions/eap-actions.controller.ts @@ -1,6 +1,4 @@ -import { Controller, Post, Body, Get, UseGuards, Param } from '@nestjs/common'; -import { EapActionsService } from './eap-actions.service'; -import { UserDecorator } from '../user/user.decorator'; +import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, @@ -8,15 +6,18 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; -import { CheckEapActionDto } from './dto/check-eap-action.dto'; -import { EapActionStatusEntity } from './eap-action-status.entity'; -import { AreaOfFocusEntity } from './area-of-focus.entity'; -import { RolesGuard } from '../../roles.guard'; + import { Roles } from '../../roles.decorator'; +import { RolesGuard } from '../../roles.guard'; +import { DisasterType } from '../disaster/disaster-type.enum'; import { UserRole } from '../user/user-role.enum'; -import { EapActionEntity } from './eap-action.entity'; +import { UserDecorator } from '../user/user.decorator'; +import { AreaOfFocusEntity } from './area-of-focus.entity'; +import { CheckEapActionDto } from './dto/check-eap-action.dto'; import { AddEapActionsDto } from './dto/eap-action.dto'; -import { DisasterType } from '../disaster/disaster-type.enum'; +import { EapActionStatusEntity } from './eap-action-status.entity'; +import { EapActionEntity } from './eap-action.entity'; +import { EapAction, EapActionsService } from './eap-actions.service'; @ApiBearerAuth() @ApiTags('eap-actions') @@ -70,7 +71,7 @@ export class EapActionsController { @Post('check-external/:countryCodeISO3/:disasterType') public async checkActionExternally( @Param() params, - @Body() eapActions: any, + @Body() eapActions: EapAction[], ): Promise { return await this.eapActionsService.checkActionExternally( params.countryCodeISO3, diff --git a/services/API-service/src/api/eap-actions/eap-actions.module.ts b/services/API-service/src/api/eap-actions/eap-actions.module.ts index 74cb74192..b9e19d453 100644 --- a/services/API-service/src/api/eap-actions/eap-actions.module.ts +++ b/services/API-service/src/api/eap-actions/eap-actions.module.ts @@ -1,17 +1,18 @@ -import { CountryEntity } from './../country/country.entity'; -import { TriggerPerLeadTime } from '../event/trigger-per-lead-time.entity'; +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + +import { AdminAreaEntity } from '../admin-area/admin-area.entity'; +import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; +import { TriggerPerLeadTime } from '../event/trigger-per-lead-time.entity'; import { UserEntity } from '../user/user.entity'; import { UserModule } from '../user/user.module'; -import { EapActionEntity } from './eap-action.entity'; +import { CountryEntity } from './../country/country.entity'; +import { AreaOfFocusEntity } from './area-of-focus.entity'; import { EapActionStatusEntity } from './eap-action-status.entity'; +import { EapActionEntity } from './eap-action.entity'; import { EapActionsController } from './eap-actions.controller'; import { EapActionsService } from './eap-actions.service'; -import { AreaOfFocusEntity } from './area-of-focus.entity'; -import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; -import { AdminAreaEntity } from '../admin-area/admin-area.entity'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/eap-actions/eap-actions.service.ts b/services/API-service/src/api/eap-actions/eap-actions.service.ts index 3d44879fe..79853bd17 100644 --- a/services/API-service/src/api/eap-actions/eap-actions.service.ts +++ b/services/API-service/src/api/eap-actions/eap-actions.service.ts @@ -1,15 +1,22 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { UserEntity } from '../user/user.entity'; + import { In, IsNull, Repository } from 'typeorm'; -import { EapActionEntity } from './eap-action.entity'; -import { EapActionStatusEntity } from './eap-action-status.entity'; -import { CheckEapActionDto } from './dto/check-eap-action.dto'; -import { AreaOfFocusEntity } from './area-of-focus.entity'; -import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; + import { AdminAreaEntity } from '../admin-area/admin-area.entity'; -import { AddEapActionsDto } from './dto/eap-action.dto'; import { DisasterType } from '../disaster/disaster-type.enum'; +import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; +import { UserEntity } from '../user/user.entity'; +import { AreaOfFocusEntity } from './area-of-focus.entity'; +import { CheckEapActionDto } from './dto/check-eap-action.dto'; +import { AddEapActionsDto } from './dto/eap-action.dto'; +import { EapActionStatusEntity } from './eap-action-status.entity'; +import { EapActionEntity } from './eap-action.entity'; + +export interface EapAction { + Early_action: string; + placeCode: string; +} @Injectable() export class EapActionsService { @@ -110,9 +117,8 @@ export class EapActionsService { public async checkActionExternally( countryCodeISO3: string, disasterType: DisasterType, - eapActions, + eapActions: EapAction[], ): Promise { - console.log('eapAction: ', eapActions); const eapActionIds = eapActions['Early_action'].split(' '); const actionIds = await this.eapActionRepository.find({ where: { @@ -129,7 +135,7 @@ export class EapActionsService { const placeCode = eapActions['placeCode']; const adminArea = await this.adminAreaRepository.findOne({ select: ['id'], - where: { placeCode: placeCode }, + where: { placeCode }, }); // note: the below will not be able to distinguish between different open events (= typhoon only) @@ -223,7 +229,7 @@ export class EapActionsService { '(' + eapActionsStates.getQuery() + ')', 'status', 'action.id = status."actionCheckedId" AND status."placeCode" = :placeCode', - { placeCode: placeCode }, + { placeCode }, ) .setParameters(eapActionsStates.getParameters()) .leftJoin('action.areaOfFocus', 'area') diff --git a/services/API-service/src/api/event/dto/event-place-code.dto.ts b/services/API-service/src/api/event/dto/event-place-code.dto.ts index c3b3dd32e..f41b79a73 100644 --- a/services/API-service/src/api/event/dto/event-place-code.dto.ts +++ b/services/API-service/src/api/event/dto/event-place-code.dto.ts @@ -1,7 +1,9 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -import { DisasterType } from '../../disaster/disaster-type.enum'; + +import { IsNotEmpty, IsString } from 'class-validator'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; +import { DisasterType } from '../../disaster/disaster-type.enum'; export class AffectedAreaDto { public placeCode: string; diff --git a/services/API-service/src/api/event/dto/trigger-per-leadtime.dto.ts b/services/API-service/src/api/event/dto/trigger-per-leadtime.dto.ts index 1553d4901..1aa1fe456 100644 --- a/services/API-service/src/api/event/dto/trigger-per-leadtime.dto.ts +++ b/services/API-service/src/api/event/dto/trigger-per-leadtime.dto.ts @@ -1,5 +1,7 @@ -import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; + +import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; export class TriggerPerLeadTimeDto { diff --git a/services/API-service/src/api/event/dto/upload-trigger-per-leadtime.dto.ts b/services/API-service/src/api/event/dto/upload-trigger-per-leadtime.dto.ts index 93d99aafb..487b0a171 100644 --- a/services/API-service/src/api/event/dto/upload-trigger-per-leadtime.dto.ts +++ b/services/API-service/src/api/event/dto/upload-trigger-per-leadtime.dto.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsEnum, @@ -6,11 +9,10 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { TriggerPerLeadTimeDto } from './trigger-per-leadtime.dto'; -import triggers from './example/triggers-per-leadtime-UGA-triggered.json'; + import { DisasterType } from '../../disaster/disaster-type.enum'; +import triggers from './example/triggers-per-leadtime-UGA-triggered.json'; +import { TriggerPerLeadTimeDto } from './trigger-per-leadtime.dto'; export class UploadTriggerPerLeadTimeDto { @ApiProperty({ example: 'UGA' }) diff --git a/services/API-service/src/api/event/event-map-image.entity.ts b/services/API-service/src/api/event/event-map-image.entity.ts index 9cc6b9a15..1052b0000 100644 --- a/services/API-service/src/api/event/event-map-image.entity.ts +++ b/services/API-service/src/api/event/event-map-image.entity.ts @@ -1,11 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, Column, - PrimaryGeneratedColumn, - ManyToOne, + Entity, JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; import { DisasterType } from '../disaster/disaster-type.enum'; import { DisasterEntity } from '../disaster/disaster.entity'; @@ -16,7 +18,7 @@ export class EventMapImageEntity { public id: string; @Column({ type: 'bytea' }) - public image: any; + public image: Buffer; @ApiProperty({ example: 'SSD' }) @ManyToOne((): typeof CountryEntity => CountryEntity) diff --git a/services/API-service/src/api/event/event-place-code.entity.ts b/services/API-service/src/api/event/event-place-code.entity.ts index 69fb297d3..bef237b0a 100644 --- a/services/API-service/src/api/event/event-place-code.entity.ts +++ b/services/API-service/src/api/event/event-place-code.entity.ts @@ -1,13 +1,14 @@ import { - Entity, - Column, Check, - PrimaryGeneratedColumn, + Column, + Entity, + JoinColumn, JoinTable, - OneToMany, ManyToOne, - JoinColumn, + OneToMany, + PrimaryGeneratedColumn, } from 'typeorm'; + import { AdminAreaEntity } from '../admin-area/admin-area.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; import { EapActionStatusEntity } from '../eap-actions/eap-action-status.entity'; @@ -37,6 +38,8 @@ export class EventPlaceCodeEntity { @Column({ default: true }) public thresholdReached: boolean; + // TODO refactor this to be named issuedDate + // As far as I understand, this is the date when the event was created and not when the disaster will happen @Column({ type: 'timestamp' }) public startDate: Date; diff --git a/services/API-service/src/api/event/event.controller.ts b/services/API-service/src/api/event/event.controller.ts index 6dd6bb368..990cc1cb4 100644 --- a/services/API-service/src/api/event/event.controller.ts +++ b/services/API-service/src/api/event/event.controller.ts @@ -1,8 +1,4 @@ -import { - ActivationLogDto, - EventPlaceCodeDto, -} from './dto/event-place-code.dto'; -import { EventService } from './event.service'; +import stream from 'stream'; import { Body, Controller, @@ -17,6 +13,7 @@ import { UseGuards, UseInterceptors, } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; import { ApiBearerAuth, ApiBody, @@ -27,18 +24,23 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + +import { Response } from 'express-serve-static-core'; + +import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; -import { UploadTriggerPerLeadTimeDto } from './dto/upload-trigger-per-leadtime.dto'; import { EventSummaryCountry, TriggeredArea } from '../../shared/data.model'; -import { DateDto, TriggerPerLeadTimeExampleDto } from './dto/date.dto'; -import { Roles } from '../../roles.decorator'; -import { UserRole } from '../user/user-role.enum'; -import { UserDecorator } from '../user/user.decorator'; -import { FileInterceptor } from '@nestjs/platform-express'; -import stream from 'stream'; -import { Response } from 'express-serve-static-core'; import { IMAGE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; import { SendNotificationDto } from '../notification/dto/send-notification.dto'; +import { UserRole } from '../user/user-role.enum'; +import { UserDecorator } from '../user/user.decorator'; +import { DateDto, TriggerPerLeadTimeExampleDto } from './dto/date.dto'; +import { + ActivationLogDto, + EventPlaceCodeDto, +} from './dto/event-place-code.dto'; +import { UploadTriggerPerLeadTimeDto } from './dto/upload-trigger-per-leadtime.dto'; +import { EventService } from './event.service'; @ApiBearerAuth() @ApiTags('event') @@ -263,7 +265,8 @@ export class EventController { ); } const bufferStream = new stream.PassThrough(); - bufferStream.end(Buffer.from(blob, 'binary')); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bufferStream.end(Buffer.from(blob as any, 'binary')); response.writeHead(HttpStatus.OK, { 'Content-Type': 'image/png', }); diff --git a/services/API-service/src/api/event/event.module.ts b/services/API-service/src/api/event/event.module.ts index 52ab2aeb9..633c42cc8 100644 --- a/services/API-service/src/api/event/event.module.ts +++ b/services/API-service/src/api/event/event.module.ts @@ -1,20 +1,21 @@ -import { EapActionsModule } from './../eap-actions/eap-actions.module'; -import { CountryModule } from './../country/country.module'; -import { EventPlaceCodeEntity } from './event-place-code.entity'; -import { UserModule } from './../user/user.module'; import { Module } from '@nestjs/common'; -import { EventController } from './event.controller'; -import { EventService } from './event.service'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { TriggerPerLeadTime } from './trigger-per-lead-time.entity'; + +import { HelperService } from '../../shared/helper.service'; import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { AdminAreaEntity } from '../admin-area/admin-area.entity'; +import { CountryEntity } from '../country/country.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; -import { HelperService } from '../../shared/helper.service'; +import { TyphoonTrackModule } from '../typhoon-track/typhoon-track.module'; import { UserEntity } from '../user/user.entity'; +import { CountryModule } from './../country/country.module'; +import { EapActionsModule } from './../eap-actions/eap-actions.module'; +import { UserModule } from './../user/user.module'; import { EventMapImageEntity } from './event-map-image.entity'; -import { TyphoonTrackModule } from '../typhoon-track/typhoon-track.module'; -import { CountryEntity } from '../country/country.entity'; +import { EventPlaceCodeEntity } from './event-place-code.entity'; +import { EventController } from './event.controller'; +import { EventService } from './event.service'; +import { TriggerPerLeadTime } from './trigger-per-lead-time.entity'; @Module({ imports: [ diff --git a/services/API-service/src/api/event/event.service.ts b/services/API-service/src/api/event/event.service.ts index 998c5d682..c016e676b 100644 --- a/services/API-service/src/api/event/event.service.ts +++ b/services/API-service/src/api/event/event.service.ts @@ -1,42 +1,45 @@ -import { EapActionsService } from './../eap-actions/eap-actions.service'; -import { AdminAreaDynamicDataEntity } from './../admin-area-dynamic-data/admin-area-dynamic-data.entity'; -import { EventPlaceCodeEntity } from './event-place-code.entity'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { subDays } from 'date-fns'; import { - ActivationLogDto, - AffectedAreaDto, - EventPlaceCodeDto, -} from './dto/event-place-code.dto'; -import { + DataSource, + In, + IsNull, LessThan, + MoreThan, MoreThanOrEqual, Repository, - In, - MoreThan, - IsNull, - DataSource, + SelectQueryBuilder, } from 'typeorm'; -import { InjectRepository } from '@nestjs/typeorm'; -import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; -import { UploadTriggerPerLeadTimeDto } from './dto/upload-trigger-per-leadtime.dto'; -import { TriggerPerLeadTime } from './trigger-per-lead-time.entity'; import { DisasterSpecificProperties, EventSummaryCountry, TriggeredArea, } from '../../shared/data.model'; +import { HelperService } from '../../shared/helper.service'; +import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; import { AdminAreaEntity } from '../admin-area/admin-area.entity'; -import { DateDto } from './dto/date.dto'; -import { TriggerPerLeadTimeDto } from './dto/trigger-per-leadtime.dto'; +import { CountryDisasterSettingsEntity } from '../country/country-disaster.entity'; +import { CountryEntity } from '../country/country.entity'; import { DisasterType } from '../disaster/disaster-type.enum'; import { DisasterEntity } from '../disaster/disaster.entity'; -import { HelperService } from '../../shared/helper.service'; +import { TyphoonTrackService } from '../typhoon-track/typhoon-track.service'; import { UserEntity } from '../user/user.entity'; +import { AdminAreaDynamicDataEntity } from './../admin-area-dynamic-data/admin-area-dynamic-data.entity'; +import { EapActionsService } from './../eap-actions/eap-actions.service'; +import { DateDto } from './dto/date.dto'; +import { + ActivationLogDto, + AffectedAreaDto, + EventPlaceCodeDto, +} from './dto/event-place-code.dto'; +import { TriggerPerLeadTimeDto } from './dto/trigger-per-leadtime.dto'; +import { UploadTriggerPerLeadTimeDto } from './dto/upload-trigger-per-leadtime.dto'; import { EventMapImageEntity } from './event-map-image.entity'; -import { TyphoonTrackService } from '../typhoon-track/typhoon-track.service'; -import { CountryEntity } from '../country/country.entity'; -import { CountryDisasterSettingsEntity } from '../country/country-disaster.entity'; +import { EventPlaceCodeEntity } from './event-place-code.entity'; +import { TriggerPerLeadTime } from './trigger-per-lead-time.entity'; @Injectable() export class EventService { @@ -63,42 +66,72 @@ export class EventService { private dataSource: DataSource, private typhoonTrackService: TyphoonTrackService, ) {} - public async getEventSummary( countryCodeISO3: string, disasterType: DisasterType, ): Promise { const recentDate = await this.getRecentDate(countryCodeISO3, disasterType); - const eventSummary = await this.eventPlaceCodeRepo - .createQueryBuilder('event') - .select(['area."countryCodeISO3"', 'event."eventName"']) - .leftJoin('event.adminArea', 'area') - .groupBy('area."countryCodeISO3"') - .addGroupBy('event."eventName"') - .addSelect([ - 'to_char(MIN("startDate") , \'yyyy-mm-dd\') AS "startDate"', - 'to_char(MAX("endDate") , \'yyyy-mm-dd\') AS "endDate"', - 'MAX(event."thresholdReached"::int)::boolean AS "thresholdReached"', - 'count(event."adminAreaId")::int AS "affectedAreas"', - 'MAX(event."triggerValue")::float AS "triggerValue"', - 'sum(event."actionsValue")::int AS "actionsValueSum"', - ]) - .where({ - closed: false, - endDate: MoreThanOrEqual(recentDate.date), - disasterType: disasterType, - }) - .andWhere('area."countryCodeISO3" = :countryCodeISO3', { - countryCodeISO3: countryCodeISO3, - }) - .getRawMany(); + const eventSummaryQueryBuilder = this.createEventSummaryQueryBuilder( + countryCodeISO3, + ).andWhere({ + closed: false, + endDate: MoreThanOrEqual(recentDate.date), + disasterType: disasterType, + }); + return this.queryAndMapEventSummary( + eventSummaryQueryBuilder, + countryCodeISO3, + disasterType, + ); + } - const disasterSettings = await this.getCountryDisasterSettings( + public async getEventsSummaryTriggerFinishedMail( + countryCodeISO3: string, + disasterType: DisasterType, + ): Promise { + const adminAreaIds = await this.getCountryAdminAreaIds(countryCodeISO3); + + const sixDaysAgo = subDays(new Date(), 6); + const eventSummaryQueryBuilder = this.createEventSummaryQueryBuilder( + countryCodeISO3, + ) + .andWhere('event.endDate > :endDate', { endDate: sixDaysAgo }) + .andWhere('event.adminArea IN (:...adminAreaIds)', { adminAreaIds }) + .andWhere('event.disasterType = :disasterType', { disasterType }) + .andWhere('event.closed = :closed', { closed: true }); + + return this.queryAndMapEventSummary( + eventSummaryQueryBuilder, countryCodeISO3, disasterType, ); + } - for await (const event of eventSummary) { + private async queryAndMapEventSummary( + qb: SelectQueryBuilder, + countryCodeISO3: string, + disasterType: DisasterType, + ): Promise { + const rawEventSummary = await qb.getRawMany(); + const eventSummary = await this.populateEventsDetails( + rawEventSummary, + countryCodeISO3, + disasterType, + ); + return eventSummary; + } + + private async populateEventsDetails( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rawEvents: any[], + countryCodeISO3: string, + disasterType: DisasterType, + ): Promise { + const disasterSettings = await this.getCountryDisasterSettings( + countryCodeISO3, + disasterType, + ); + for (const event of rawEvents) { event.firstLeadTime = await this.getFirstLeadTime( countryCodeISO3, disasterType, @@ -118,14 +151,44 @@ export class EventService { event.eventName, ); } - if (disasterSettings.eapAlertClasses) { + if (disasterType === DisasterType.Floods) { + // REFACTOR: either make eapAlertClass a requirement across all hazard + // types or reimplement such that eapAlertClass is not needed in the + // backend (it is a VIEW of the DATA in the dashboard and email) event.disasterSpecificProperties = await this.getEventEapAlertClass( disasterSettings, event.triggerValue, ); } } - return eventSummary; + return rawEvents; + } + + private createEventSummaryQueryBuilder( + countryCodeISO3: string, + ): SelectQueryBuilder { + return this.eventPlaceCodeRepo + .createQueryBuilder('event') + .select([ + 'area."countryCodeISO3"', + 'event."eventName"', + 'event."triggerValue"', + ]) + .leftJoin('event.adminArea', 'area') + .groupBy('area."countryCodeISO3"') + .addGroupBy('event."eventName"') + .addGroupBy('event."triggerValue"') + .addSelect([ + 'to_char(MIN("startDate") , \'yyyy-mm-dd\') AS "startDate"', + 'to_char(MAX("endDate") , \'yyyy-mm-dd\') AS "endDate"', + 'MAX(event."thresholdReached"::int)::boolean AS "thresholdReached"', + 'count(event."adminAreaId")::int AS "affectedAreas"', + 'MAX(event."triggerValue")::float AS "triggerValue"', + 'sum(event."actionsValue")::int AS "actionsValueSum"', + ]) + .andWhere('area."countryCodeISO3" = :countryCodeISO3', { + countryCodeISO3: countryCodeISO3, + }); } public async getRecentDate( @@ -203,7 +266,7 @@ export class EventService { ); const deleteFilters = { adminArea: In(countryAdminAreaIds), - disasterType: disasterType, + disasterType, startDate: MoreThanOrEqual( this.helperService.getUploadCutoffMoment(disasterType, date), ), @@ -221,7 +284,7 @@ export class EventService { return ( await this.disasterTypeRepository.findOne({ select: ['triggerUnit'], - where: { disasterType: disasterType }, + where: { disasterType }, }) ).triggerUnit; } @@ -232,7 +295,7 @@ export class EventService { ) { return ( await this.countryRepository.findOne({ - where: { countryCodeISO3: countryCodeISO3 }, + where: { countryCodeISO3 }, relations: ['countryDisasterSettings'], }) ).countryDisasterSettings.find((d) => d.disasterType === disasterType); @@ -255,11 +318,11 @@ export class EventService { ).defaultAdminLevel; const whereFiltersDynamicData = { - indicator: triggerUnit, + indicator: triggerUnit, // REFACTOR: trigger unit and indicator should not be used interchangeably value: MoreThan(0), - adminLevel: adminLevel, - disasterType: disasterType, - countryCodeISO3: countryCodeISO3, + adminLevel, + disasterType, + countryCodeISO3, timestamp: MoreThanOrEqual( this.helperService.getUploadCutoffMoment( disasterType, @@ -509,14 +572,14 @@ export class EventService { disasterType, ); const whereFilters = { - countryCodeISO3: countryCodeISO3, + countryCodeISO3, timestamp: MoreThanOrEqual( this.helperService.getUploadCutoffMoment( disasterType, lastTriggeredDate.timestamp, ), ), - disasterType: disasterType, + disasterType, }; if (eventName) { whereFilters['eventName'] = eventName; @@ -596,7 +659,7 @@ export class EventService { return ( await this.disasterTypeRepository.findOne({ select: ['actionsUnit'], - where: { disasterType: disasterType }, + where: { disasterType }, }) ).actionsUnit; } @@ -651,16 +714,16 @@ export class EventService { ); const whereFilters = { - indicator: triggerUnit, + indicator: triggerUnit, // REFACTOR: trigger unit and indicator should not be used interchangeably timestamp: MoreThanOrEqual( this.helperService.getUploadCutoffMoment( disasterType, lastTriggeredDate.timestamp, ), ), - countryCodeISO3: countryCodeISO3, - adminLevel: adminLevel, - disasterType: disasterType, + countryCodeISO3, + adminLevel, + disasterType, eventName: eventName || IsNull(), }; @@ -685,16 +748,16 @@ export class EventService { const whereOptions = { placeCode: In(triggerPlaceCodesArray), - indicator: actionUnit, + indicator: actionUnit, // REFACTOR: action unit and indicator should not be used interchangeably timestamp: MoreThanOrEqual( this.helperService.getUploadCutoffMoment( disasterType, lastTriggeredDate.timestamp, ), ), - countryCodeISO3: countryCodeISO3, - adminLevel: adminLevel, - disasterType: disasterType, + countryCodeISO3, + adminLevel, + disasterType, }; if (eventName) { whereFilters['eventName'] = eventName; @@ -711,7 +774,7 @@ export class EventService { for (const area of affectedAreas) { area.triggerValue = triggeredPlaceCodes.find( - (p) => p.placeCode === area.placeCode, + ({ placeCode }) => placeCode === area.placeCode, ).triggerValue; } @@ -737,7 +800,7 @@ export class EventService { where: { closed: false, adminArea: In(countryAdminAreaIds), - disasterType: disasterType, + disasterType, eventName: eventName || IsNull(), }, relations: ['adminArea'], @@ -782,17 +845,14 @@ export class EventService { private async updateEvents( eventPlaceCodeIds: string[], - aboveThreshold: boolean, + thresholdReached: boolean, endDate: Date, ) { if (eventPlaceCodeIds.length) { await this.eventPlaceCodeRepo .createQueryBuilder() .update() - .set({ - thresholdReached: aboveThreshold, - endDate: endDate, - }) + .set({ thresholdReached, endDate }) .where({ eventPlaceCodeId: In(eventPlaceCodeIds) }) .execute(); } @@ -853,7 +913,7 @@ export class EventService { where: { closed: false, adminArea: In(countryAdminAreaIds), - disasterType: disasterType, + disasterType, eventName: eventName || IsNull(), }, relations: ['adminArea'], @@ -897,27 +957,26 @@ export class EventService { countryCodeISO3, disasterType, ); - const whereFilters = { - endDate: LessThan(uploadDate.timestamp), // If the area was not prolongued earlier, then the endDate is not updated and is therefore less than the uploadDate + const where = { + endDate: LessThan(uploadDate.timestamp), // If the area was not prolonged earlier, then the endDate is not updated and is therefore less than the uploadDate adminArea: In(countryAdminAreaIds), - disasterType: disasterType, + disasterType, closed: false, }; - const expiredEventAreas = await this.eventPlaceCodeRepo.find({ - where: whereFilters, - }); + const expiredEventAreas = await this.eventPlaceCodeRepo.find({ where }); - //Below threshold events can be removed from this table after closing + // Below threshold events can be removed from this table after closing + // Below threshold events are warnings an not triggered. I do not know why they are removed here const belowThresholdEvents = expiredEventAreas.filter( - (a) => !a.thresholdReached, + ({ thresholdReached }) => !thresholdReached, ); await this.eventPlaceCodeRepo.remove(belowThresholdEvents); //For the other ones update 'closed = true' const aboveThresholdEvents = expiredEventAreas.filter( - (a) => a.thresholdReached, + ({ thresholdReached }) => thresholdReached, ); - for await (const area of aboveThresholdEvents) { + for (const area of aboveThresholdEvents) { area.closed = true; } await this.eventPlaceCodeRepo.save(aboveThresholdEvents); @@ -927,7 +986,7 @@ export class EventService { countryCodeISO3: string, disasterType: DisasterType, eventName: string, - imageFileBlob, + imageFileBlob: { buffer: Buffer }, ): Promise { let eventMapImageEntity = await this.eventMapImageRepository.findOne({ where: { @@ -954,11 +1013,11 @@ export class EventService { countryCodeISO3: string, disasterType: DisasterType, eventName: string, - ): Promise { + ): Promise { const eventMapImageEntity = await this.eventMapImageRepository.findOne({ where: { - countryCodeISO3: countryCodeISO3, - disasterType: disasterType, + countryCodeISO3, + disasterType, eventName: eventName === 'no-name' || !eventName ? IsNull() : eventName, }, }); diff --git a/services/API-service/src/api/event/trigger-per-lead-time.entity.ts b/services/API-service/src/api/event/trigger-per-lead-time.entity.ts index ef3eeee88..778baaf76 100644 --- a/services/API-service/src/api/event/trigger-per-lead-time.entity.ts +++ b/services/API-service/src/api/event/trigger-per-lead-time.entity.ts @@ -1,10 +1,11 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, + Entity, JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; import { CountryEntity } from '../country/country.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; diff --git a/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts b/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts index fc3e96d6e..7c7bceed0 100644 --- a/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts +++ b/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { IsIn, IsNotEmpty, @@ -5,7 +7,8 @@ import { IsOptional, IsString, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; + +import { EapAlertClassKeyEnum } from '../../../shared/data.model'; export class GlofasStationForecastDto { @ApiProperty({ example: 'G1374' }) @@ -20,7 +23,7 @@ export class GlofasStationForecastDto { @ApiProperty({ example: 1 }) @IsNotEmpty() - @IsIn(['no', 'min', 'med', 'max']) + @IsIn(Object.values(EapAlertClassKeyEnum)) public eapAlertClass: string; @ApiProperty({ example: 10 }) diff --git a/services/API-service/src/api/glofas-station/dto/upload-station.dto.ts b/services/API-service/src/api/glofas-station/dto/upload-station.dto.ts index 4e296ad43..41ec3ef93 100644 --- a/services/API-service/src/api/glofas-station/dto/upload-station.dto.ts +++ b/services/API-service/src/api/glofas-station/dto/upload-station.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class UploadStationDto { @ApiProperty({ example: 'G1374' }) @IsNotEmpty() diff --git a/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts b/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts index 55f0f244a..6229d388d 100644 --- a/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts +++ b/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, @@ -5,11 +8,10 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; -import { Type } from 'class-transformer'; -import { GlofasStationForecastDto } from './station-forecast.dto'; import stations from '../../point-data/dto/example/glofas-stations/glofas-stations-UGA-triggered.json'; +import { GlofasStationForecastDto } from './station-forecast.dto'; export class UploadTriggerPerStationDto { @ApiProperty({ example: 'UGA' }) diff --git a/services/API-service/src/api/glofas-station/glofas-station.controller.ts b/services/API-service/src/api/glofas-station/glofas-station.controller.ts index e6baa24a3..6a54db489 100644 --- a/services/API-service/src/api/glofas-station/glofas-station.controller.ts +++ b/services/API-service/src/api/glofas-station/glofas-station.controller.ts @@ -6,6 +6,7 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; import { UserRole } from '../user/user-role.enum'; @@ -34,7 +35,7 @@ export class GlofasStationController { description: 'Glofas station locations and attributes for given country.', }) @Get(':countryCodeISO3') - public async getStationsByCountry(@Param() params): Promise { + public async getStationsByCountry(@Param() params) { return await this.glofasStationService.getStationsByCountry( params.countryCodeISO3, ); diff --git a/services/API-service/src/api/glofas-station/glofas-station.module.ts b/services/API-service/src/api/glofas-station/glofas-station.module.ts index b4e05210f..3b5c76086 100644 --- a/services/API-service/src/api/glofas-station/glofas-station.module.ts +++ b/services/API-service/src/api/glofas-station/glofas-station.module.ts @@ -1,15 +1,16 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { AdminAreaEntity } from '../admin-area/admin-area.entity'; import { CountryEntity } from '../country/country.entity'; import { EventModule } from '../event/event.module'; +import { PointDataModule } from '../point-data/point-data.module'; import { UserModule } from '../user/user.module'; import { GlofasStationController } from './glofas-station.controller'; import { GlofasStationService } from './glofas-station.service'; -import { HttpModule } from '@nestjs/axios'; -import { PointDataModule } from '../point-data/point-data.module'; @Module({ imports: [ diff --git a/services/API-service/src/api/glofas-station/glofas-station.service.ts b/services/API-service/src/api/glofas-station/glofas-station.service.ts index 5489cf7d3..bfd264f0b 100644 --- a/services/API-service/src/api/glofas-station/glofas-station.service.ts +++ b/services/API-service/src/api/glofas-station/glofas-station.service.ts @@ -1,12 +1,14 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { Repository } from 'typeorm'; -import { UploadTriggerPerStationDto } from './dto/upload-trigger-per-station'; -import { DisasterType } from '../disaster/disaster-type.enum'; + import { CountryEntity } from '../country/country.entity'; -import { PointDataService } from '../point-data/point-data.service'; +import { DisasterType } from '../disaster/disaster-type.enum'; import { UploadDynamicPointDataDto } from '../point-data/dto/upload-asset-exposure-status.dto'; import { PointDataEnum } from '../point-data/point-data.entity'; +import { PointDataService } from '../point-data/point-data.service'; +import { UploadTriggerPerStationDto } from './dto/upload-trigger-per-station'; @Injectable() export class GlofasStationService { @@ -15,7 +17,7 @@ export class GlofasStationService { public constructor(private readonly pointDataService: PointDataService) {} - public async getStationsByCountry(countryCodeISO3: string): Promise { + public async getStationsByCountry(countryCodeISO3: string) { const stations = await this.pointDataService.getPointDataByCountry( PointDataEnum.glofasStations, countryCodeISO3, diff --git a/services/API-service/src/api/lead-time/lead-time.entity.ts b/services/API-service/src/api/lead-time/lead-time.entity.ts index f374b78ce..a14e29b1b 100644 --- a/services/API-service/src/api/lead-time/lead-time.entity.ts +++ b/services/API-service/src/api/lead-time/lead-time.entity.ts @@ -1,5 +1,7 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm'; +import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'; + import { CountryDisasterSettingsEntity } from '../country/country-disaster.entity'; + @Entity('lead-time') export class LeadTimeEntity { @PrimaryGeneratedColumn('uuid') diff --git a/services/API-service/src/api/lines-data/dto/upload-asset-exposure-status.dto.ts b/services/API-service/src/api/lines-data/dto/upload-asset-exposure-status.dto.ts index 6d6b352fc..dd809b37b 100644 --- a/services/API-service/src/api/lines-data/dto/upload-asset-exposure-status.dto.ts +++ b/services/API-service/src/api/lines-data/dto/upload-asset-exposure-status.dto.ts @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { IsArray, IsEnum, @@ -5,7 +7,7 @@ import { IsOptional, IsString, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; import { DisasterType } from '../../disaster/disaster-type.enum'; import { LinesDataEnum } from '../lines-data.entity'; diff --git a/services/API-service/src/api/lines-data/dto/upload-buildings.dto.ts b/services/API-service/src/api/lines-data/dto/upload-buildings.dto.ts index d9252c7d1..5d6522451 100644 --- a/services/API-service/src/api/lines-data/dto/upload-buildings.dto.ts +++ b/services/API-service/src/api/lines-data/dto/upload-buildings.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; + export class BuildingDto { @ApiProperty({ example: 1234 }) public fid: number = undefined; diff --git a/services/API-service/src/api/lines-data/dto/upload-roads.dto.ts b/services/API-service/src/api/lines-data/dto/upload-roads.dto.ts index 2c320678c..864510b6a 100644 --- a/services/API-service/src/api/lines-data/dto/upload-roads.dto.ts +++ b/services/API-service/src/api/lines-data/dto/upload-roads.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class RoadDto { @ApiProperty({ example: 'highway' }) @IsString() diff --git a/services/API-service/src/api/lines-data/lines-data-dynamic-status.entity.ts b/services/API-service/src/api/lines-data/lines-data-dynamic-status.entity.ts index 7b50870ca..2317dbed4 100644 --- a/services/API-service/src/api/lines-data/lines-data-dynamic-status.entity.ts +++ b/services/API-service/src/api/lines-data/lines-data-dynamic-status.entity.ts @@ -1,12 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, + Entity, + Index, JoinColumn, ManyToOne, - Index, + PrimaryGeneratedColumn, } from 'typeorm'; + import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; import { LinesDataEntity } from './lines-data.entity'; diff --git a/services/API-service/src/api/lines-data/lines-data-views.entity.ts b/services/API-service/src/api/lines-data/lines-data-views.entity.ts index 7f7bbf506..7b82e0a23 100644 --- a/services/API-service/src/api/lines-data/lines-data-views.entity.ts +++ b/services/API-service/src/api/lines-data/lines-data-views.entity.ts @@ -1,7 +1,8 @@ import { ViewEntity } from 'typeorm'; -import { LinesDataEntity, LinesDataEnum } from './lines-data.entity'; -import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; + import { AppDataSource } from '../../../appdatasource'; +import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; +import { LinesDataEntity, LinesDataEnum } from './lines-data.entity'; const getViewQuery = (type: LinesDataEnum) => { return () => diff --git a/services/API-service/src/api/lines-data/lines-data.controller.ts b/services/API-service/src/api/lines-data/lines-data.controller.ts index e9e2e5bed..bba97c730 100644 --- a/services/API-service/src/api/lines-data/lines-data.controller.ts +++ b/services/API-service/src/api/lines-data/lines-data.controller.ts @@ -17,12 +17,13 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; +import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; import { UserRole } from '../user/user-role.enum'; -import { LinesDataService } from './lines-data.service'; import { UploadLinesExposureStatusDto } from './dto/upload-asset-exposure-status.dto'; -import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; +import { LinesDataService } from './lines-data.service'; @ApiBearerAuth() @ApiTags('lines-data') diff --git a/services/API-service/src/api/lines-data/lines-data.entity.ts b/services/API-service/src/api/lines-data/lines-data.entity.ts index 416386ce2..66ffbf8ea 100644 --- a/services/API-service/src/api/lines-data/lines-data.entity.ts +++ b/services/API-service/src/api/lines-data/lines-data.entity.ts @@ -1,9 +1,9 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - Index, + Entity, Geometry, + Index, + PrimaryGeneratedColumn, } from 'typeorm'; export enum LinesDataEnum { diff --git a/services/API-service/src/api/lines-data/lines-data.module.ts b/services/API-service/src/api/lines-data/lines-data.module.ts index a16a0e0ec..36c72f55a 100644 --- a/services/API-service/src/api/lines-data/lines-data.module.ts +++ b/services/API-service/src/api/lines-data/lines-data.module.ts @@ -1,12 +1,13 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; import { UserModule } from '../user/user.module'; +import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; import { LinesDataController } from './lines-data.controller'; import { LinesDataEntity } from './lines-data.entity'; import { LinesDataService } from './lines-data.service'; -import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/lines-data/lines-data.service.ts b/services/API-service/src/api/lines-data/lines-data.service.ts index 864e46c1d..d468d0720 100644 --- a/services/API-service/src/api/lines-data/lines-data.service.ts +++ b/services/API-service/src/api/lines-data/lines-data.service.ts @@ -1,12 +1,14 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { HelperService } from '../../shared/helper.service'; + import { MoreThanOrEqual, Repository } from 'typeorm'; -import { LinesDataEntity, LinesDataEnum } from './lines-data.entity'; -import { RoadDto } from './dto/upload-roads.dto'; + +import { HelperService } from '../../shared/helper.service'; import { UploadLinesExposureStatusDto } from './dto/upload-asset-exposure-status.dto'; -import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; import { BuildingDto } from './dto/upload-buildings.dto'; +import { RoadDto } from './dto/upload-roads.dto'; +import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; +import { LinesDataEntity, LinesDataEnum } from './lines-data.entity'; @Injectable() export class LinesDataService { @@ -17,7 +19,7 @@ export class LinesDataService { public constructor(private readonly helperService: HelperService) {} - private getDtoPerLinesDataCategory(linesDataCategory: LinesDataEnum): any { + private getDtoPerLinesDataCategory(linesDataCategory: LinesDataEnum) { switch (linesDataCategory) { case LinesDataEnum.roads: return new RoadDto(); @@ -34,6 +36,7 @@ export class LinesDataService { public async uploadJson( linesDataCategory: LinesDataEnum, countryCodeISO3: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any validatedObjArray: any, deleteExisting = true, ) { diff --git a/services/API-service/src/api/metadata/dto/add-indicators.dto.ts b/services/API-service/src/api/metadata/dto/add-indicators.dto.ts index f975c0b9e..e0029e2d2 100644 --- a/services/API-service/src/api/metadata/dto/add-indicators.dto.ts +++ b/services/API-service/src/api/metadata/dto/add-indicators.dto.ts @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { IsBoolean, IsIn, @@ -5,7 +7,6 @@ import { IsNumber, IsString, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; export class IndicatorDto { @ApiProperty({ diff --git a/services/API-service/src/api/metadata/dto/add-layers.dto.ts b/services/API-service/src/api/metadata/dto/add-layers.dto.ts index 72d048578..0abac94a5 100644 --- a/services/API-service/src/api/metadata/dto/add-layers.dto.ts +++ b/services/API-service/src/api/metadata/dto/add-layers.dto.ts @@ -1,5 +1,7 @@ -import { IsBoolean, IsEnum, IsIn, IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; + +import { IsBoolean, IsEnum, IsIn, IsNotEmpty, IsString } from 'class-validator'; + import { DisasterType } from '../../disaster/disaster-type.enum'; export class LayerDto { diff --git a/services/API-service/src/api/metadata/indicator-metadata.entity.ts b/services/API-service/src/api/metadata/indicator-metadata.entity.ts index 413cea53d..ce8ab7512 100644 --- a/services/API-service/src/api/metadata/indicator-metadata.entity.ts +++ b/services/API-service/src/api/metadata/indicator-metadata.entity.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity('indicator-metadata') export class IndicatorMetadataEntity { diff --git a/services/API-service/src/api/metadata/layer-metadata.entity.ts b/services/API-service/src/api/metadata/layer-metadata.entity.ts index 1f2251e01..4c021f993 100644 --- a/services/API-service/src/api/metadata/layer-metadata.entity.ts +++ b/services/API-service/src/api/metadata/layer-metadata.entity.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; + import { IsIn } from 'class-validator'; -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity('layer-metadata') export class LayerMetadataEntity { diff --git a/services/API-service/src/api/metadata/metadata.controller.ts b/services/API-service/src/api/metadata/metadata.controller.ts index d97333542..42fb49c9d 100644 --- a/services/API-service/src/api/metadata/metadata.controller.ts +++ b/services/API-service/src/api/metadata/metadata.controller.ts @@ -6,6 +6,7 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; import { UserRole } from '../user/user-role.enum'; diff --git a/services/API-service/src/api/metadata/metadata.module.ts b/services/API-service/src/api/metadata/metadata.module.ts index 1d9cfcaad..bba7858ad 100644 --- a/services/API-service/src/api/metadata/metadata.module.ts +++ b/services/API-service/src/api/metadata/metadata.module.ts @@ -1,15 +1,16 @@ -import { CountryModule } from './../country/country.module'; +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + +import { HelperService } from '../../shared/helper.service'; +import { DisasterEntity } from '../disaster/disaster.entity'; +import { EventModule } from '../event/event.module'; import { UserModule } from '../user/user.module'; -import { MetadataController } from './metadata.controller'; +import { CountryModule } from './../country/country.module'; import { IndicatorMetadataEntity } from './indicator-metadata.entity'; -import { MetadataService } from './metadata.service'; import { LayerMetadataEntity } from './layer-metadata.entity'; -import { HelperService } from '../../shared/helper.service'; -import { EventModule } from '../event/event.module'; -import { DisasterEntity } from '../disaster/disaster.entity'; -import { HttpModule } from '@nestjs/axios'; +import { MetadataController } from './metadata.controller'; +import { MetadataService } from './metadata.service'; @Module({ imports: [ diff --git a/services/API-service/src/api/metadata/metadata.service.ts b/services/API-service/src/api/metadata/metadata.service.ts index 574f4fb11..536928b3f 100644 --- a/services/API-service/src/api/metadata/metadata.service.ts +++ b/services/API-service/src/api/metadata/metadata.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { Repository } from 'typeorm'; + import { DisasterType } from '../disaster/disaster-type.enum'; import { AddIndicatorsDto, IndicatorDto } from './dto/add-indicators.dto'; import { AddLayersDto, LayerDto } from './dto/add-layers.dto'; diff --git a/services/API-service/src/api/notification/dto/admin-area-notification-info.dto.ts b/services/API-service/src/api/notification/dto/admin-area-notification-info.dto.ts new file mode 100644 index 000000000..c5afda2a9 --- /dev/null +++ b/services/API-service/src/api/notification/dto/admin-area-notification-info.dto.ts @@ -0,0 +1,4 @@ +export class AdminAreaLabel { + singular: string; + plural: string; +} diff --git a/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts new file mode 100644 index 000000000..44bc7b412 --- /dev/null +++ b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts @@ -0,0 +1,17 @@ +import { CountryEntity } from '../../country/country.entity'; +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; +import { AdminAreaLabel } from './admin-area-notification-info.dto'; +import { NotificationDataPerEventDto } from './notification-date-per-event.dto'; + +export class ContentEventEmail { + public disasterType: DisasterType; + public disasterTypeLabel: string; + public indicatorMetadata: IndicatorMetadataEntity; + public linkEapSop: string; + public dataPerEvent: NotificationDataPerEventDto[]; + public mapImageData: unknown[]; + public defaultAdminLevel: number; + public defaultAdminAreaLabel: AdminAreaLabel; + public country: CountryEntity; // Ensure that is has the following relations 'disasterTypes', 'notificationInfo','countryDisasterSettings','countryDisasterSettings.activeLeadTimes', +} diff --git a/services/API-service/src/api/notification/dto/notification-api-test-response.dto.ts b/services/API-service/src/api/notification/dto/notification-api-test-response.dto.ts new file mode 100644 index 000000000..773d8595f --- /dev/null +++ b/services/API-service/src/api/notification/dto/notification-api-test-response.dto.ts @@ -0,0 +1,9 @@ +export class NotificationApiTestResponseDto { + activeEvents: NotificationApiTestResponseChannelDto; + finishedEvents: NotificationApiTestResponseChannelDto; +} + +export class NotificationApiTestResponseChannelDto { + email: string; + whatsapp: string; +} diff --git a/services/API-service/src/api/notification/dto/notification-date-per-event.dto.ts b/services/API-service/src/api/notification/dto/notification-date-per-event.dto.ts new file mode 100644 index 000000000..373beeabc --- /dev/null +++ b/services/API-service/src/api/notification/dto/notification-date-per-event.dto.ts @@ -0,0 +1,47 @@ +import { EapAlertClass, TriggeredArea } from '../../../shared/data.model'; +import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; + +export class NotificationDataPerEventDto { + triggerStatusLabel: TriggerStatusLabelEnum; + eventName: string; + disasterSpecificCopy: DisasterSpecificCopy; + + /** + * The day that the event starts. + */ + firstLeadTime: LeadTime; + + /** + * The day that the event triggers. This could be different from firstLeadTimeString. + * For example, a flood could transition from a warning (a chance of a small flood) + * to an EAP trigger (a larger chance of a bigger flood). + */ + firstTriggerLeadTime: LeadTime; + + firstLeadTimeString: string; + firstTriggerLeadTimeString: string; + + triggeredAreas: TriggeredArea[]; + + /** + * The number of areas where the event triggers. + */ + nrOfTriggeredAreas: number; + + totalAffectedOfIndicator: number; + mapImage?: Buffer; + issuedDate: Date; + eapAlertClass: EapAlertClass; +} + +export enum TriggerStatusLabelEnum { + Trigger = 'Trigger', + Warning = 'Warning', +} + +export class DisasterSpecificCopy { + eventStatus: string; + extraInfo: string; + leadTimeString?: string; + timestamp?: string; +} diff --git a/services/API-service/src/api/notification/dto/send-notification.dto.ts b/services/API-service/src/api/notification/dto/send-notification.dto.ts index 98431e8d2..2109c565a 100644 --- a/services/API-service/src/api/notification/dto/send-notification.dto.ts +++ b/services/API-service/src/api/notification/dto/send-notification.dto.ts @@ -1,7 +1,9 @@ -import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -import { DisasterType } from '../../disaster/disaster-type.enum'; + +import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + import countries from '../../../scripts/json/countries.json'; +import { DisasterType } from '../../disaster/disaster-type.enum'; export class SendNotificationDto { @ApiProperty({ example: countries.map((c) => c.countryCodeISO3).join(' | ') }) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts new file mode 100644 index 000000000..86eb669cc --- /dev/null +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -0,0 +1,504 @@ +import * as fs from 'fs'; +import { Injectable } from '@nestjs/common'; + +import * as ejs from 'ejs'; +import * as juice from 'juice'; + +import { + EapAlertClassKeyEnum, + EventSummaryCountry, +} from '../../../shared/data.model'; +import { CountryTimeZoneMapping } from '../../country/country-time-zone-mapping'; +import { CountryEntity } from '../../country/country.entity'; +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { ContentEventEmail } from '../dto/content-trigger-email.dto'; +import { + NotificationDataPerEventDto, + TriggerStatusLabelEnum, +} from '../dto/notification-date-per-event.dto'; + +const emailFolder = './src/api/notification/email'; +const emailTemplateFolder = `${emailFolder}/html`; +const emailIconFolder = `${emailFolder}/icons`; +const emailLogoFolder = `${emailFolder}/logos`; + +@Injectable() +export class EmailTemplateService { + public async createHtmlForTriggerEmail( + emailContent: ContentEventEmail, + date: Date, + ): Promise { + const replaceKeyValues = this.createReplaceKeyValuesTrigger( + emailContent, + date, + ); + return this.formatEmail(replaceKeyValues); + } + + public async createHtmlForTriggerFinishedEmail( + country: CountryEntity, + disasterType: DisasterType, + finishedEvents: EventSummaryCountry[], + disasterTypeLabel: string, + _date: Date, // I am not sure if this is needed and for what it was used before + ): Promise { + const replaceKeyValues = this.createReplaceKeyValuesTriggerFinished( + country, + disasterType, + finishedEvents, + disasterTypeLabel, + ); + return await this.formatEmail(replaceKeyValues); + } + + private createReplaceKeyValuesTrigger( + emailContent: ContentEventEmail, + _date: Date, + ): Record { + const country = emailContent.country; + const disasterType = emailContent.disasterType; + + const keyValueReplaceObject = { + emailBody: this.readHtmlFile('trigger-notification.html'), + headerEventOverview: this.getHeaderEventStarted(emailContent), + notificationActions: this.getNotificationActionsHtml( + country, + emailContent.linkEapSop, + ), + tablesStacked: this.getTablesForEvents(emailContent), + eventListBody: this.getEventListBody(emailContent), + imgLogo: country.notificationInfo.logo[disasterType], + triggerStatement: country.notificationInfo.triggerStatement[disasterType], + mapImagePart: this.getMapImageHtml(emailContent), + linkDashboard: process.env.DASHBOARD_URL, + socialMediaLink: country.notificationInfo.linkSocialMediaUrl, + socialMediaType: country.notificationInfo.linkSocialMediaType, + disasterType: emailContent.disasterTypeLabel, + footer: this.getFooterHtml(country.countryName), + }; + return keyValueReplaceObject; + } + + private createReplaceKeyValuesTriggerFinished( + country: CountryEntity, + disasterType: DisasterType, + events: EventSummaryCountry[], + disasterTypeLabel: string, + ): Record { + const keyValueReplaceObject = { + emailBody: this.readHtmlFile('trigger-finished.html'), + headerEventOverview: '', + eventOverview: this.getEventsFinishedOverview( + country, + events, + disasterTypeLabel, + ), + imgLogo: country.notificationInfo.logo[disasterType], + linkDashboard: process.env.DASHBOARD_URL, + socialMediaPart: this.getSocialMediaHtml(country), + socialMediaLink: country.notificationInfo.linkSocialMediaUrl, + socialMediaType: country.notificationInfo.linkSocialMediaType, + disasterType: disasterTypeLabel, + footer: this.getFooterHtml(country.countryName), + }; + return keyValueReplaceObject; + } + + private getEventsFinishedOverview( + country: CountryEntity, + events: EventSummaryCountry[], + disasterTypeLabel: string, + ): string { + const template = this.readHtmlFile('event-finished.html'); + return events + .map((event) => + ejs.render(template, { + disasterTypeLabel, + eventName: event.eventName, + issuedDate: this.dateObjectToDateTimeString( + new Date(event.startDate), + country.countryCodeISO3, + ), + timezone: CountryTimeZoneMapping[country.countryCodeISO3], + }), + ) + .join(''); + } + + private getHeaderEventStarted(emailContent: ContentEventEmail): string { + let headerEventOverview = this.readHtmlFile('header.html'); + headerEventOverview = ejs.render(headerEventOverview, { + sentOnDate: this.getCurrentDateTimeString( + emailContent.country.countryCodeISO3, + ), + disasterLabel: emailContent.disasterTypeLabel, + nrOfEvents: emailContent.dataPerEvent.length, + timezone: CountryTimeZoneMapping[emailContent.country.countryCodeISO3], + }); + return headerEventOverview; + } + + private getNotificationActionsHtml( + country: CountryEntity, + linkEapSop: string, + ): string { + const socialMediaLinkHtml = this.getSocialMediaHtml(country); + + let html = this.readHtmlFile('notification-actions.html'); + const data = { + linkDashboard: process.env.DASHBOARD_URL, + linkEapSop: linkEapSop, + socialMediaPart: socialMediaLinkHtml, + }; + html = ejs.render(html, data); + return html; + } + + private getSocialMediaHtml(country: CountryEntity) { + return country.notificationInfo.linkSocialMediaType + ? this.readHtmlFile('social-media-link.html') + : ''; + } + + private getMapImageHtml(emailContent: ContentEventEmail) { + return emailContent.dataPerEvent + .filter((event) => event.mapImage) + .map((event) => { + const eventHtmlTemplate = this.readHtmlFile('map-image.html'); + const replacements = { + mapImgSrc: this.getMapImgSrc( + emailContent.country.countryCodeISO3, + emailContent.disasterType, + event.eventName, + ), + mapImgDescription: this.getMapImageDescription( + emailContent.disasterType, + ), + eventName: event.eventName ? `(for ${event.eventName})` : '', + }; + return ejs.render(eventHtmlTemplate, replacements); + }) + .join(''); + } + + private getMapImgSrc( + countryCodeISO3: string, + disasterType: DisasterType, + eventName: string, + ) { + return `${ + process.env.NG_API_URL + }/event/event-map-image/${countryCodeISO3}/${disasterType}/${ + eventName || 'no-name' + }`; + } + + private getMapImageDescription(disasterType: DisasterType): string { + const descriptions = { + [DisasterType.Floods]: + 'The triggered areas are outlined in purple. The potential flood extent is shown in red.
', + }; + + return descriptions[disasterType] || ''; + } + + private async formatEmail( + emailKeyValueReplaceObject: Record, + ): Promise { + // TODO REFACTOR: Apply styles in a separate file also for the base.html + const template = this.readHtmlFile('base.html'); + const styles = this.readHtmlFile('styles.ejs'); + const templateWithStyle = styles + template; + + let emailHtml = templateWithStyle; + let previousHtml = null; + + // This loop is needed to handle nested EJS tags. It repeatedly renders the template + // until there are no more EJS tags left to render. This is necessary because EJS + // doesn't render nested tags in one pass. + while (emailHtml !== previousHtml) { + previousHtml = emailHtml; + emailHtml = ejs.render(previousHtml, emailKeyValueReplaceObject); + } + // Inline the CSS + const inlinedHtml = await new Promise((resolve, reject) => { + juice.juiceResources(emailHtml, { webResources: {} }, (err, html) => { + if (err) { + console.error('Error inlining CSS: ', err); + reject(err); + } else { + resolve(html); + } + }); + }); + + return inlinedHtml as string; + } + + private getTablesForEvents(emailContent: ContentEventEmail): string { + const adminAreaLabelsParent = + emailContent.country.adminRegionLabels[ + String(emailContent.defaultAdminLevel - 1) + ]; + return emailContent.dataPerEvent + .map((event) => { + const data = { + hazard: emailContent.disasterTypeLabel, + triggerStatusLabel: event.triggerStatusLabel, + eventName: event.eventName, + defaultAdminAreaLabelSingular: + emailContent.defaultAdminAreaLabel.singular, + defaultAdminAreaLabelPlural: + emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), + defaultAdminAreaLabelParent: adminAreaLabelsParent.singular, + indicatorLabel: emailContent.indicatorMetadata.label, + triangleIcon: this.getTriangleIcon( + event.eapAlertClass?.key, + event.triggerStatusLabel, + ), + tableRows: this.getTablesRows(event), + color: this.getIbfHexColor( + event.eapAlertClass?.color, + event.triggerStatusLabel, + ), + severityLabel: this.getEventSeverityLabel(event.eapAlertClass?.key), + }; + + const templateFileName = 'table-event.html'; + const template = this.readHtmlFile(templateFileName); + + const result = ejs.render(template, data); + return result; + }) + .join(''); + } + + private getEventSeverityLabel( + eapAlertClassKey: EapAlertClassKeyEnum, + ): string { + const severityLabels = { + [EapAlertClassKeyEnum.med]: 'Medium', + [EapAlertClassKeyEnum.min]: 'Low', + }; + + return severityLabels[eapAlertClassKey] || ''; + } + + private getTablesRows(event: NotificationDataPerEventDto) { + return event.triggeredAreas + .map((area) => { + const tableRowHtmlFileName = + TriggerStatusLabelEnum.Trigger === event.triggerStatusLabel + ? 'table-trigger-row.html' + : 'table-warning-row.html'; + const areaTemplate = this.readHtmlFile(tableRowHtmlFileName); + const areaData = { + affectedOfIndicator: area.actionsValue, + adminBoundary: area.displayName ? area.displayName : area.name, + higherAdminBoundary: area.nameParent, + }; + + return ejs.render(areaTemplate, areaData); + }) + .join(''); + } + + private getEventListBody(emailContent: ContentEventEmail): string { + return emailContent.dataPerEvent + .map((event) => { + const data = { + // Event details + eventName: event.eventName, + hazard: emailContent.disasterTypeLabel, + triggerStatusLabel: event.triggerStatusLabel, + issuedDate: this.dateObjectToDateTimeString( + event.issuedDate, + emailContent.country.countryCodeISO3, + ), + timezone: + CountryTimeZoneMapping[emailContent.country.countryCodeISO3], + + // Lead time details + firstLeadTimeString: event.firstLeadTimeString, + firstTriggerLeadTimeString: event.firstTriggerLeadTimeString, + firstLeadTimeQuantity: event.firstLeadTime.replace('-', ' '), + firstTriggerLeadTimeQuantity: event.firstTriggerLeadTime + ? event.firstTriggerLeadTime.replace('-', ' ') + : '', + + // Area details + nrOfTriggeredAreas: event.nrOfTriggeredAreas, + defaultAdminAreaLabel: + emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), + + // Indicator details + indicatorLabel: emailContent.indicatorMetadata.label, + totalAffectedOfIndicator: event.totalAffectedOfIndicator, + indicatorUnit: emailContent.indicatorMetadata.unit, + totalAffected: this.getTotalAffectedHtml( + event, + emailContent.indicatorMetadata.label.toLowerCase(), + ), + + // EAP details + triangleIcon: this.getTriangleIcon( + event.eapAlertClass?.key, + event.triggerStatusLabel, + ), + leadTime: event.firstLeadTime.replace('-', ' '), + disasterIssuedLabel: this.getDisasterIssuedLabel( + event.eapAlertClass?.label, + event.triggerStatusLabel, + ), + color: this.getIbfHexColor( + event.eapAlertClass?.color, + event.triggerStatusLabel, + ), + advisory: this.getAdvisoryHtml( + event.triggerStatusLabel, + emailContent.linkEapSop, + ), + }; + + const templateFileName = 'body-event.html'; + const template = this.readHtmlFile(templateFileName); + return ejs.render(template, data); + }) + .join(''); + } + + private getDisasterIssuedLabel( + eapLabel: string, + triggerStatusLabel: TriggerStatusLabelEnum, + ) { + return eapLabel || triggerStatusLabel; + } + + private getAdvisoryHtml( + triggerStatusLabel: TriggerStatusLabelEnum, + eapLink: string, + ) { + const fileName = + triggerStatusLabel === TriggerStatusLabelEnum.Trigger + ? 'advisory-trigger.html' + : 'advisory-warning.html'; + const advisoryHtml = this.readHtmlFile(fileName); + return ejs.render(advisoryHtml, { eapLink }); + } + + private getTotalAffectedHtml( + event: NotificationDataPerEventDto, + indicatorUnit: string, + ): string { + const fileName = + event.triggerStatusLabel === TriggerStatusLabelEnum.Warning + ? 'body-total-affected-warning.html' + : 'body-total-affected-trigger.html'; + const htmlTemplate = this.readHtmlFile(fileName); + return ejs.render(htmlTemplate, { + totalAffectedOfIndicator: event.totalAffectedOfIndicator, + indicatorUnit: indicatorUnit, + }); + } + + private getIbfHexColor( + color: string, + triggerStatusLabel: TriggerStatusLabelEnum, + ): string { + const ibfOrange = '#aa6009'; + const ibfYellow = '#7d6906'; + const ibfRed = '#8a0f32'; + + // Color defined in the EAP Alert Class. This is only used for flood events + // For other events, the color is defined in the disaster settings + // So we decide it based on the trigger status label + + if (color) { + // TODO: Define in a place where FrontEnd and Backend can share this + switch (color) { + case 'ibf-orange': + return ibfOrange; + case 'ibf-yellow': + return ibfYellow; + default: + return ibfRed; + } + } + return triggerStatusLabel === TriggerStatusLabelEnum.Trigger + ? ibfRed + : ibfOrange; + } + + private getFooterHtml(countryName: string): string { + const footerHtml = this.readHtmlFile('footer.html'); + const ibfLogo = this.getLogoImageAsDataURL(); + return ejs.render(footerHtml, { + ibfLogo: ibfLogo, + countryName: countryName, + }); + } + + private getCurrentDateTimeString(countryCodeISO3: string): string { + const date = new Date(); + return this.dateObjectToDateTimeString(date, countryCodeISO3); + } + + private dateObjectToDateTimeString( + date: Date, + countryCodeISO3: string, + ): string { + const timeZone = CountryTimeZoneMapping[countryCodeISO3]; + const options: Intl.DateTimeFormatOptions = { + weekday: 'long', + day: '2-digit', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZone: timeZone, + }; + return date.toLocaleString('default', options); + } + + private getTriangleIcon( + eapAlertClassKey: EapAlertClassKeyEnum, + triggerStatusLabel: TriggerStatusLabelEnum, + ) { + const fileNameMap = { + [EapAlertClassKeyEnum.med]: 'warning-medium.png', + [EapAlertClassKeyEnum.min]: 'warning-low.png', + default: 'trigger.png', + }; + + let fileName = eapAlertClassKey + ? fileNameMap[eapAlertClassKey] + : fileNameMap.default; + if ( + !eapAlertClassKey && + triggerStatusLabel !== TriggerStatusLabelEnum.Trigger + ) { + fileName = 'warning-medium.png'; + } + + const filePath = `${emailIconFolder}/${fileName}`; + return this.getPngImageAsDataURL(filePath); + } + + private getLogoImageAsDataURL() { + const filePath = `${emailLogoFolder}/logo-IBF.png`; + return this.getPngImageAsDataURL(filePath); + } + + private getPngImageAsDataURL(relativePath: string) { + const imageBuffer = fs.readFileSync(relativePath); + const imageDataURL = `data:image/png;base64,${imageBuffer.toString( + 'base64', + )}`; + + return imageDataURL; + } + + private readHtmlFile(fileName: string): string { + return fs.readFileSync(`${emailTemplateFolder}/${fileName}`, 'utf8'); + } +} diff --git a/services/API-service/src/api/notification/email/email.service.ts b/services/API-service/src/api/notification/email/email.service.ts index e88a2caf0..013008a5d 100644 --- a/services/API-service/src/api/notification/email/email.service.ts +++ b/services/API-service/src/api/notification/email/email.service.ts @@ -1,34 +1,23 @@ -import { AdminAreaDynamicDataService } from './../../admin-area-dynamic-data/admin-area-dynamic-data.service'; -import { LeadTimeEntity } from './../../lead-time/lead-time.entity'; -import { CountryEntity } from './../../country/country.entity'; import { Injectable } from '@nestjs/common'; -import { EventService } from '../../event/event.service'; -import fs from 'fs'; + import Mailchimp from 'mailchimp-api-v3'; -import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; -import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; -import { DynamicIndicator } from '../../admin-area-dynamic-data/enum/dynamic-data-unit'; -import { DisasterType } from '../../disaster/disaster-type.enum'; + import { EventSummaryCountry } from '../../../shared/data.model'; +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { CountryEntity } from './../../country/country.entity'; import { NotificationContentService } from './../notification-content/notification-content.service'; - -class ReplaceKeyValue { - replaceKey: string; - replaceValue: string; -} +import { EmailTemplateService } from './email-template.service'; @Injectable() export class EmailService { - private placeholderToday = '(TODAY)'; private fromEmail = process.env.SUPPORT_EMAIL_ADDRESS; private fromEmailName = 'IBF portal'; private mailchimp = new Mailchimp(process.env.MC_API); public constructor( - private readonly eventService: EventService, - private readonly adminAreaDynamicDataService: AdminAreaDynamicDataService, private readonly notificationContentService: NotificationContentService, + private readonly emailTemplateService: EmailTemplateService, ) {} private async getSegmentId( @@ -54,15 +43,24 @@ export class EmailService { country: CountryEntity, disasterType: DisasterType, activeEvents: EventSummaryCountry[], + isApiTest: boolean, date?: Date, - ): Promise { - const replaceKeyValues = await this.createReplaceKeyValuesTrigger( - country, - disasterType, - activeEvents, - date ? new Date(date) : new Date(), + ): Promise { + date = date ? new Date(date) : new Date(); + + const emailContent = + await this.notificationContentService.getContentTriggerNotification( + country, + disasterType, + activeEvents, + ); + const emailHtml = await this.emailTemplateService.createHtmlForTriggerEmail( + emailContent, + date, ); - const emailHtml = this.formatEmail(replaceKeyValues); + if (isApiTest) { + return emailHtml; + } const emailSubject = `IBF ${( await this.notificationContentService.getDisasterTypeLabel(disasterType) ).toLowerCase()} notification`; @@ -77,19 +75,25 @@ export class EmailService { public async sendTriggerFinishedEmail( country: CountryEntity, disasterType: DisasterType, - finishedEvent: EventSummaryCountry, + finishedEvents: EventSummaryCountry[], + isApiTest: boolean, date?: Date, - ): Promise { - const replaceKeyValues = await this.createReplaceKeyValuesTriggerFinished( - country, - disasterType, - finishedEvent, - date ? new Date(date) : new Date(), - ); - const emailHtml = this.formatEmail(replaceKeyValues); - const emailSubject = `IBF ${( - await this.notificationContentService.getDisasterTypeLabel(disasterType) - ).toLowerCase()} trigger is now below threshold`; + ): Promise { + const disasterTypeLabel = + await this.notificationContentService.getDisasterTypeLabel(disasterType); + const emailHtml = + await this.emailTemplateService.createHtmlForTriggerFinishedEmail( + country, + disasterType, + finishedEvents, + disasterTypeLabel, + date ? new Date(date) : new Date(), + ); + + if (isApiTest) { + return emailHtml; + } + const emailSubject = `IBF ${disasterTypeLabel.toLowerCase()} trigger is now below threshold`; this.sendEmail( emailSubject, emailHtml, @@ -134,604 +138,4 @@ export class EmailService { ); await this.mailchimp.post(`/campaigns/${createResult.id}/actions/send`); } - - private async createReplaceKeyValuesTrigger( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - date: Date, - ): Promise { - const keyValueReplaceList = [ - { - replaceKey: '(EMAIL-BODY)', - replaceValue: this.getEmailBody(false), - }, - { - replaceKey: '(HEADER-EVENT-OVERVIEW)', - replaceValue: await this.getHeaderEventOverview( - country, - disasterType, - events, - date, - ), - }, - { - replaceKey: '(SOCIAL-MEDIA-PART)', - replaceValue: this.getSocialMediaHtml(country), - }, - { - replaceKey: '(TABLES-stacked)', - replaceValue: await this.getTriggerOverviewTables( - country, - disasterType, - events, - date, - ), - }, - { - replaceKey: this.placeholderToday, - replaceValue: date.toLocaleDateString('default', { - day: '2-digit', - month: 'short', - year: 'numeric', - }), - }, - { - replaceKey: '(EVENT-LIST-BODY)', - replaceValue: ( - await this.getLeadTimeList(country, disasterType, events, date) - )['leadTimeListLong'], - }, - { - replaceKey: '(IMG-LOGO)', - replaceValue: country.notificationInfo.logo[disasterType], - }, - { - replaceKey: '(TRIGGER-STATEMENT)', - replaceValue: country.notificationInfo.triggerStatement[disasterType], - }, - { - replaceKey: '(MAP-IMAGE-PART)', - replaceValue: await this.getMapImageHtml(country, disasterType, events), - }, - { - replaceKey: '(LINK-DASHBOARD)', - replaceValue: process.env.DASHBOARD_URL, - }, - { - replaceKey: '(LINK-EAP-SOP)', - replaceValue: country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink, - }, - { - replaceKey: '(SOCIAL-MEDIA-LINK)', - replaceValue: country.notificationInfo.linkSocialMediaUrl, - }, - { - replaceKey: '(SOCIAL-MEDIA-TYPE)', - replaceValue: country.notificationInfo.linkSocialMediaType, - }, - { - replaceKey: '(ADMIN-AREA-PLURAL)', - replaceValue: - country.adminRegionLabels[ - String( - country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel, - ) - ].plural.toLowerCase(), - }, - { - replaceKey: '(ADMIN-AREA-SINGULAR)', - replaceValue: - country.adminRegionLabels[ - String( - country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel, - ) - ].singular.toLowerCase(), - }, - { - replaceKey: '(DISASTER-TYPE)', - replaceValue: - await this.notificationContentService.getDisasterTypeLabel( - disasterType, - ), - }, - { - replaceKey: '(VIDEO-PDF-LINKS)', - replaceValue: this.getVideoPdfLinks( - country.notificationInfo.linkVideo, - country.notificationInfo.linkPdf, - ), - }, - { - replaceKey: '(EXPOSURE-UNIT)', - replaceValue: ( - await this.notificationContentService.getActionUnit(disasterType) - ).label.toLocaleLowerCase(), - }, - ]; - return keyValueReplaceList; - } - - private async createReplaceKeyValuesTriggerFinished( - country: CountryEntity, - disasterType: DisasterType, - event: EventSummaryCountry, - date: Date, - ): Promise { - const keyValueReplaceList = [ - { - replaceKey: '(EMAIL-BODY)', - replaceValue: this.getEmailBody(true), - }, - { - replaceKey: '(HEADER-EVENT-OVERVIEW)', - replaceValue: '', - }, - { - replaceKey: '(IMG-LOGO)', - replaceValue: country.notificationInfo.logo[disasterType], - }, - { - replaceKey: '(START-DATE)', - replaceValue: event.startDate, - }, - { - replaceKey: '(LINK-DASHBOARD)', - replaceValue: process.env.DASHBOARD_URL, - }, - { - replaceKey: '(SOCIAL-MEDIA-PART)', - replaceValue: this.getSocialMediaHtml(country), - }, - { - replaceKey: '(LINK-EAP-SOP)', - replaceValue: country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink, - }, - { - replaceKey: '(SOCIAL-MEDIA-LINK)', - replaceValue: country.notificationInfo.linkSocialMediaUrl, - }, - { - replaceKey: '(SOCIAL-MEDIA-TYPE)', - replaceValue: country.notificationInfo.linkSocialMediaType, - }, - { - replaceKey: '(VIDEO-PDF-LINKS)', - replaceValue: this.getVideoPdfLinks( - country.notificationInfo.linkVideo, - country.notificationInfo.linkPdf, - ), - }, - { - replaceKey: '(DISASTER-TYPE)', - replaceValue: - await this.notificationContentService.getDisasterTypeLabel( - disasterType, - ), - }, - { - replaceKey: this.placeholderToday, - replaceValue: date.toLocaleDateString('default', { - day: '2-digit', - month: 'short', - year: 'numeric', - }), - }, - ]; - return keyValueReplaceList; - } - - private getEmailBody(triggerFinished: boolean): string { - if (triggerFinished) { - return fs.readFileSync( - './src/api/notification/email/html/trigger-finished.html', - 'utf8', - ); - } else { - return fs.readFileSync( - './src/api/notification/email/html/trigger-notification.html', - 'utf8', - ); - } - } - - private async getHeaderEventOverview( - country: CountryEntity, - disasterType: DisasterType, - activeEvents: EventSummaryCountry[], - date?: Date, - ): Promise { - const leadTimeListShort = ( - await this.getLeadTimeList( - country, - disasterType, - this.sortEventsByLeadTime(activeEvents), - date, - ) - )['leadTimeListShort']; - return fs - .readFileSync( - './src/api/notification/email/html/header-event-overview.html', - 'utf8', - ) - .replace('(EVENT-LIST-HEADER)', leadTimeListShort); - } - - private sortEventsByLeadTime( - arr: EventSummaryCountry[], - ): EventSummaryCountry[] { - const leadTimeValue = (leadTime: LeadTime): number => - Number(leadTime.split('-')[0]); - - return arr.sort((a, b) => { - if (leadTimeValue(a.firstLeadTime) < leadTimeValue(b.firstLeadTime)) { - return -1; - } - if (leadTimeValue(a.firstLeadTime) > leadTimeValue(b.firstLeadTime)) { - return 1; - } - - return 0; - }); - } - - private getVideoPdfLinks(videoLink: string, pdfLink: string) { - const linkVideoHTML = ` - video`; - - const linkPdfHTML = `PDF`; - let videoStr = ''; - if (videoLink) { - videoStr = ' ' + linkVideoHTML; - } - let pdfStr = ''; - if (pdfLink) { - pdfStr = ' ' + linkPdfHTML; - } - let orStr = ''; - if (videoStr && pdfStr) { - orStr = ' or'; - } - if (videoStr || pdfStr) { - return `See instructions for the IBF-portal in${videoStr}${orStr}${pdfStr}.`; - } - } - - private async getLeadTimeList( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - date?: Date, - ): Promise { - const triggeredLeadTimes = - await this.notificationContentService.getLeadTimesAcrossEvents( - country.countryCodeISO3, - disasterType, - events, - ); - - let leadTimeListShort = ''; - let leadTimeListLong = ''; - for (const leadTime of country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).activeLeadTimes) { - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - for await (const event of events) { - // for each event .. - const triggeredLeadTimes = - await this.eventService.getTriggerPerLeadtime( - country.countryCodeISO3, - disasterType, - event.eventName, - ); - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - const leadTimeListEvent = - await this.notificationContentService.getLeadTimeListEvent( - country, - event, - disasterType, - leadTime.leadTimeName as LeadTime, - date, - ); - - // We are hack-misusing 'extraInfo' being filled as a proxy for typhoonNoLandfallYet-boolean - leadTimeListShort = `${leadTimeListShort}${leadTimeListEvent.short}`; - leadTimeListLong = `${leadTimeListLong}${leadTimeListEvent.long}`; - } - } - } - } - return { leadTimeListShort, leadTimeListLong }; - } - - private async getTriggerOverviewTables( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - date: Date, - ): Promise { - const triggeredLeadTimes = - await this.notificationContentService.getLeadTimesAcrossEvents( - country.countryCodeISO3, - disasterType, - events, - ); - - let leadTimeTables = ''; - for (const leadTime of country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).activeLeadTimes) { - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - for await (const event of events) { - // for each event .. - const triggeredLeadTimes = - await this.eventService.getTriggerPerLeadtime( - country.countryCodeISO3, - disasterType, - event.eventName, - ); - - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - // .. find the right leadtime - const tableForLeadTime = await this.getTableForLeadTime( - country, - disasterType, - leadTime, - event, - date, - ); - leadTimeTables = leadTimeTables + tableForLeadTime; - } - } - } - } - return leadTimeTables; - } - - private getSocialMediaHtml(country: CountryEntity): string { - if (country.notificationInfo.linkSocialMediaType) { - return fs.readFileSync( - './src/api/notification/email/html/social-media-link.html', - 'utf8', - ); - } else { - return ''; - } - } - - private async getMapImageHtml( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - ): Promise { - let html = ''; - for await (const event of events) { - const mapImage = await this.eventService.getEventMapImage( - country.countryCodeISO3, - disasterType, - event.eventName || 'no-name', - ); - if (mapImage) { - let eventHtml = fs.readFileSync( - './src/api/notification/email/html/map-image.html', - 'utf8', - ); - eventHtml = eventHtml - .replace( - '(MAP-IMG-SRC)', - this.getMapImgSrc( - country.countryCodeISO3, - disasterType, - event.eventName, - ), - ) - .replace( - '(MAP-IMG-DESCRIPTION)', - this.getMapImageDescription(disasterType), - ); - eventHtml = eventHtml.replace( - '(EVENT-NAME)', - event.eventName ? ` for '${event.eventName}'` : '', - ); - html += eventHtml; - } - } - return html; - } - - private getMapImgSrc( - countryCodeISO3: string, - disasterType: DisasterType, - eventName: string, - ): string { - const src = `${ - process.env.NG_API_URL - }/event/event-map-image/${countryCodeISO3}/${disasterType}/${ - eventName || 'no-name' - }`; - - return src; - } - - private getMapImageDescription(disasterType: DisasterType): string { - switch (disasterType) { - case DisasterType.Floods: - return 'The triggered areas are outlined in purple. The potential flood extent is shown in red.
'; - default: - return ''; - } - } - - private async getTableForLeadTime( - country: CountryEntity, - disasterType: DisasterType, - leadTime: LeadTimeEntity, - event: EventSummaryCountry, - date: Date, - ): Promise { - const adminLevel = country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel; - const adminAreaLabels = country.adminRegionLabels[String(adminLevel)]; - const adminAreaLabelsParent = - country.adminRegionLabels[String(adminLevel - 1)]; - - const actionsUnit = await this.notificationContentService.getActionUnit( - disasterType, - ); - - const tableForLeadTimeStart = `
- ${ - ( - await this.notificationContentService.getLeadTimeListEvent( - country, - event, - disasterType, - leadTime.leadTimeName as LeadTime, - date, - ) - ).short - } -
- - - - - - - - - - -
`; - const tableForLeadTimeMiddle = await this.getAreaTables( - country, - disasterType, - leadTime, - event.eventName, - actionsUnit, - ); - const tableForLeadTimeEnd = '
This table lists the potentially exposed ${adminAreaLabels.plural.toLowerCase()} in order of ${actionsUnit.label.toLowerCase()}:
Alert class${adminAreaLabels.singular}${ - adminAreaLabelsParent ? ' (' + adminAreaLabelsParent.singular + ')' : '' - }Predicted ${actionsUnit.label}


'; - const tableForLeadTime = - tableForLeadTimeStart + tableForLeadTimeMiddle + tableForLeadTimeEnd; - return tableForLeadTime; - } - - private async getAreaTables( - country: CountryEntity, - disasterType: DisasterType, - leadTime: LeadTimeEntity, - eventName: string, - actionsUnit: IndicatorMetadataEntity, - ): Promise { - const triggeredAreas = await this.eventService.getTriggeredAreas( - country.countryCodeISO3, - disasterType, - country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel, - leadTime.leadTimeName, - eventName, - ); - triggeredAreas.sort((a, b) => (a.triggerValue > b.triggerValue ? -1 : 1)); - const disaster = await this.notificationContentService.getDisaster( - disasterType, - ); - const hasEap = this.hasEap(disasterType); - let areaTableString = ''; - for (const area of triggeredAreas) { - const actionsUnitValue = - await this.adminAreaDynamicDataService.getDynamicAdminAreaDataPerPcode( - disaster.actionsUnit as DynamicIndicator, - area.placeCode, - leadTime.leadTimeName as LeadTime, - eventName, - ); - - const areaTable = ` - - ${this.mapTriggerValueToAlertClass( - area.triggerValue, - hasEap, - )} - ${area.name}${ - area.nameParent ? ' (' + area.nameParent + ')' : '' - } - ${this.notificationContentService.formatActionUnitValue( - actionsUnitValue, - actionsUnit, - )} - `; - areaTableString = areaTableString + areaTable; - } - return areaTableString; - } - - // TODO merge this with the front-end instance of this to some generic place in back-end - public hasEap(disasterType: DisasterType): boolean { - const eapDisasterTypes = [ - DisasterType.Floods, - DisasterType.Drought, - DisasterType.Typhoon, - DisasterType.FlashFloods, - ]; - return eapDisasterTypes.includes(disasterType); - } - - private mapTriggerValueToAlertClass( - triggerValue: number, - hasEap: boolean, - ): string { - if (triggerValue === 1) { - return hasEap ? 'Trigger issued' : 'Alert issued'; - } else if (triggerValue === 0.7) { - return 'Medium warning issued'; - } else if (triggerValue === 0.3) { - return 'Low warning issued'; - } - } - - private formatEmail(emailKeyValueReplaceList: ReplaceKeyValue[]): string { - let emailHtml = fs.readFileSync( - './src/api/notification/email/html/base.html', - 'utf8', - ); - for (const entry of emailKeyValueReplaceList) { - emailHtml = emailHtml.split(entry.replaceKey).join(entry.replaceValue); - } - return emailHtml; - } } diff --git a/services/API-service/src/api/notification/email/html/advisory-trigger.html b/services/API-service/src/api/notification/email/html/advisory-trigger.html new file mode 100644 index 000000000..d9e896dda --- /dev/null +++ b/services/API-service/src/api/notification/email/html/advisory-trigger.html @@ -0,0 +1,7 @@ +Activate + + Early Action Protocol + diff --git a/services/API-service/src/api/notification/email/html/advisory-warning.html b/services/API-service/src/api/notification/email/html/advisory-warning.html new file mode 100644 index 000000000..90204931d --- /dev/null +++ b/services/API-service/src/api/notification/email/html/advisory-warning.html @@ -0,0 +1 @@ +Inform all potentially exposed districts diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index a73656531..2ef820fc4 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -25,11 +25,6 @@ text-align: center; } - .notification-sub-title-left { - color: white; - text-align: center; - } - .notification-content { color: #000000; line-height: 1.5; @@ -55,21 +50,6 @@ padding: 10px 0; } - .notification-actions-table td:first-child { - width: 220px; - } - - .notification-action-button { - height: 40px; - width: 200px; - background-color: #6200ee; - color: #ffffff; - font-weight: bold; - border: 0px; - border-radius: 40px; - cursor: pointer; - } - p { margin: 10px 0; padding: 0; @@ -394,7 +374,7 @@ #templateBody { /*@editable*/ - background-color: white; + background-color: #f4f5f8; /*@editable*/ background-image: none; /*@editable*/ @@ -561,7 +541,7 @@ @media only screen and (min-width: 768px) { .templateContainer { - width: 600px !important; + width: 800px !important; } } @@ -882,7 +862,7 @@ >

- (DISASTER-TYPE) IBF Notification + <%= disasterType %> IBF Notification

- (HEADER-EVENT-OVERVIEW) + <%- headerEventOverview %> @@ -922,7 +902,7 @@

- (EMAIL-BODY) + <%- emailBody %> + + triangleIcon + <%= hazard %> <%= eventName %> + + + <% if (firstTriggerLeadTimeString) { %> + + <%= hazard %> expected to start on <%= firstLeadTimeString %>, <%= + firstLeadTimeQuantity %>s from now. + + + <%= disasterIssuedLabel %>: + + expected to reach threshold on <%= firstTriggerLeadTimeString %>, <%= + firstTriggerLeadTimeQuantity %>s from now. + + + <% } else { %> + <%= disasterIssuedLabel %>: + + expected on <%= firstLeadTimeString %>, <%= firstLeadTimeQuantity %>s from + now. + + + <% } %> + + Expected exposed <%= defaultAdminAreaLabel %>: + + + <%= nrOfTriggeredAreas %> (see list below) + + + <%= indicatorLabel %>: + <%- totalAffected %> + Advisory: + + <%- advisory %> + + +
+ + This <%= triggerStatusLabel %> was issued by the IBF portal on <%= + issuedDate %> (<%= timezone %>) + + +
+ diff --git a/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html new file mode 100644 index 000000000..e5f37e53d --- /dev/null +++ b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html @@ -0,0 +1,4 @@ + + <%= totalAffectedOfIndicator %> <%= indicatorUnit %> + + diff --git a/services/API-service/src/api/notification/email/html/body-total-affected-warning.html b/services/API-service/src/api/notification/email/html/body-total-affected-warning.html new file mode 100644 index 000000000..6572e5a8e --- /dev/null +++ b/services/API-service/src/api/notification/email/html/body-total-affected-warning.html @@ -0,0 +1 @@ +Information regarding <%= indicatorUnit %> not available for warnings
diff --git a/services/API-service/src/api/notification/email/html/event-finished.html b/services/API-service/src/api/notification/email/html/event-finished.html new file mode 100644 index 000000000..188cb295a --- /dev/null +++ b/services/API-service/src/api/notification/email/html/event-finished.html @@ -0,0 +1,15 @@ +
+ + <%= disasterTypeLabel %>: <%= eventName %> is now below threshold + + + + The events actions will continue to show on the IBF portal and can still be + managed. + + + + This warning was issued by the IBF portal on <%= issuedDate %> (<%= timezone + %>) + +
diff --git a/services/API-service/src/api/notification/email/html/footer.html b/services/API-service/src/api/notification/email/html/footer.html new file mode 100644 index 000000000..c742a474e --- /dev/null +++ b/services/API-service/src/api/notification/email/html/footer.html @@ -0,0 +1,20 @@ + + + + + +
+ + +
+ Impact-Based Forecasting Portal (IBF) was co-developed by Netherlands + Red Cross 510 the together with the <%= countryName %> Red Cross + National Society. For questions contact us at ibf-support@510.global +
+
diff --git a/services/API-service/src/api/notification/email/html/header-event-overview.html b/services/API-service/src/api/notification/email/html/header-event-overview.html deleted file mode 100644 index db14059be..000000000 --- a/services/API-service/src/api/notification/email/html/header-event-overview.html +++ /dev/null @@ -1,5 +0,0 @@ - - -

(EVENT-LIST-HEADER)

- - diff --git a/services/API-service/src/api/notification/email/html/header.html b/services/API-service/src/api/notification/email/html/header.html new file mode 100644 index 000000000..2fcee9bc5 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/header.html @@ -0,0 +1,16 @@ + + + + + + +
+

+ <%= nrOfEvents %> <%= disasterLabel %> alerts +

+ + IBF alert send on <%= sentOnDate %> (<%= timezone %>) + +
+ + diff --git a/services/API-service/src/api/notification/email/html/map-image.html b/services/API-service/src/api/notification/email/html/map-image.html index 876034e1e..4af2b1715 100644 --- a/services/API-service/src/api/notification/email/html/map-image.html +++ b/services/API-service/src/api/notification/email/html/map-image.html @@ -1,6 +1,9 @@
-Map of the triggered area(EVENT-NAME): (click 'Download pictures' if it -does not show) +Map of the triggered area <%- eventName %>
-(MAP-IMG-DESCRIPTION) - +<%- mapImgDescription %> +click 'Download pictures' if it does not show diff --git a/services/API-service/src/api/notification/email/html/notification-actions.html b/services/API-service/src/api/notification/email/html/notification-actions.html new file mode 100644 index 000000000..be5074f41 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/notification-actions.html @@ -0,0 +1,66 @@ + + + + + + + <%- socialMediaPart %> + + + + + +
+ + + + +
+ Go to the IBF-portal +
+
+ + + + +
+ Find more information about the potentially exposed areas, view + the map and manage anticipatory actions. +
+
+ + + + +
+ About trigger +
+
+ + + + +
+ Read about the trigger methodology and the anticipatory actions. +
+
diff --git a/services/API-service/src/api/notification/email/html/social-media-link.html b/services/API-service/src/api/notification/email/html/social-media-link.html index b36c482e4..61a111b12 100644 --- a/services/API-service/src/api/notification/email/html/social-media-link.html +++ b/services/API-service/src/api/notification/email/html/social-media-link.html @@ -9,7 +9,7 @@ Join (SOCIAL-MEDIA-TYPE) groupJoin <%- socialMediaType %> group diff --git a/services/API-service/src/api/notification/email/html/styles.ejs b/services/API-service/src/api/notification/email/html/styles.ejs new file mode 100644 index 000000000..74a3f2b0f --- /dev/null +++ b/services/API-service/src/api/notification/email/html/styles.ejs @@ -0,0 +1,144 @@ + diff --git a/services/API-service/src/api/notification/email/html/table-event.html b/services/API-service/src/api/notification/email/html/table-event.html new file mode 100644 index 000000000..9e178323b --- /dev/null +++ b/services/API-service/src/api/notification/email/html/table-event.html @@ -0,0 +1,56 @@ + + + + +
+
+
+ Warning icon + <%= severityLabel %> <%= triggerStatusLabel %> <%= hazard %>: <%= + eventName %> +
+
+
+ Expected exposed <%= defaultAdminAreaLabelPlural %><% if + (triggerStatusLabel === 'Trigger') { %> in order of <%= + indicatorLabel.toLowerCase() %><% } %>: +
+
+
+ + + <% if (triggerStatusLabel === 'Trigger') { %> + + <% } %> + + + <%- tableRows %> +
+ <%= indicatorLabel %> + + <%= defaultAdminAreaLabelSingular %> (<%= + defaultAdminAreaLabelParent %>) +
+
+ <% if (triggerStatusLabel === 'Warning') { %> +
+ Please note: Information regarding <%= indicatorLabel %> not available + for medium warning level. +
+ <% } %> +
+
+
diff --git a/services/API-service/src/api/notification/email/html/table-trigger-row.html b/services/API-service/src/api/notification/email/html/table-trigger-row.html new file mode 100644 index 000000000..5dfe41cd0 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/table-trigger-row.html @@ -0,0 +1,8 @@ + + + <%= affectedOfIndicator %> + + + <%= adminBoundary %> (<%= higherAdminBoundary %>) + + diff --git a/services/API-service/src/api/notification/email/html/table-warning-row.html b/services/API-service/src/api/notification/email/html/table-warning-row.html new file mode 100644 index 000000000..b551f1d03 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/table-warning-row.html @@ -0,0 +1,5 @@ + + + <%= adminBoundary %> (<%= higherAdminBoundary %>) + + diff --git a/services/API-service/src/api/notification/email/html/trigger-finished.html b/services/API-service/src/api/notification/email/html/trigger-finished.html index 0c0ac46a9..fbd5ff3ea 100644 --- a/services/API-service/src/api/notification/email/html/trigger-finished.html +++ b/services/API-service/src/api/notification/email/html/trigger-finished.html @@ -83,134 +83,11 @@ id="Editable_Content_Areas" >
- Dear reader, +
Dear Reader,


- The trigger notification formerly activated on - (START-DATE) is now - below threshold.

- The event will close in the IBF-portal when the - forecast stays below threshold for 7 days in a - row. Until that time, anticipatory actions can - still be managed in the IBF-portal. - - - - - - - - - - - (SOCIAL-MEDIA-PART) - -
- - - - -
- Go to IBF-portal -
-
- - - - -
- 1. Manage the anticipatory actions - in the IBF-portal for 1 more week. - (VIDEO-PDF-LINKS) -
-
- - - - -
- About trigger -
-
- - - - -
- 2. Read about the trigger - methodology and the anticipatory - actions. -
-
+ + <%- eventOverview %> <%- notificationActions %> + <%- footer %>
diff --git a/services/API-service/src/api/notification/email/html/trigger-notification.html b/services/API-service/src/api/notification/email/html/trigger-notification.html index ecb5beb5f..0570573bf 100644 --- a/services/API-service/src/api/notification/email/html/trigger-notification.html +++ b/services/API-service/src/api/notification/email/html/trigger-notification.html @@ -83,121 +83,17 @@ id="Editable_Content_Areas" >
- Dear Reader,

- (EVENT-LIST-BODY) - - - - - - - (SOCIAL-MEDIA-PART) - - - - - -
- - - - -
- Go to the IBF-portal -
-
- - - - -
- Find more info on the potentially - exposed areas on a map and manage - anticipatory actions in the - IBF-portal. (VIDEO-PDF-LINKS) -
-
- - - - -
- About trigger -
-
- - - - -
- Read about the trigger methodology - and the anticipatory actions. -
-
+
Dear Reader,
+

+ <%- eventListBody %> <%- notificationActions %>
Trigger Statement: - (TRIGGER-STATEMENT) + <%- triggerStatement %>
- (MAP-IMAGE-PART) + <%- mapImagePart %>

- (TABLES-stacked) + <%- tablesStacked %> <%- footer %>
diff --git a/services/API-service/src/api/notification/email/icons/trigger.png b/services/API-service/src/api/notification/email/icons/trigger.png new file mode 100644 index 000000000..a590e56d3 Binary files /dev/null and b/services/API-service/src/api/notification/email/icons/trigger.png differ diff --git a/services/API-service/src/api/notification/email/icons/warning-low.png b/services/API-service/src/api/notification/email/icons/warning-low.png new file mode 100644 index 000000000..7f2973019 Binary files /dev/null and b/services/API-service/src/api/notification/email/icons/warning-low.png differ diff --git a/services/API-service/src/api/notification/email/icons/warning-medium.png b/services/API-service/src/api/notification/email/icons/warning-medium.png new file mode 100644 index 000000000..93dc07ab4 Binary files /dev/null and b/services/API-service/src/api/notification/email/icons/warning-medium.png differ diff --git a/services/API-service/src/api/notification/email/logos/logo-IBF.png b/services/API-service/src/api/notification/email/logos/logo-IBF.png new file mode 100644 index 000000000..b006cbaca Binary files /dev/null and b/services/API-service/src/api/notification/email/logos/logo-IBF.png differ diff --git a/services/API-service/src/api/notification/helpers/format-action-unit-value.helper.ts b/services/API-service/src/api/notification/helpers/format-action-unit-value.helper.ts new file mode 100644 index 000000000..ff365fd72 --- /dev/null +++ b/services/API-service/src/api/notification/helpers/format-action-unit-value.helper.ts @@ -0,0 +1,16 @@ +export function formatActionUnitValue( + value: number, + numberFormat: string, +): string { + if (value === null) { + return null; + } else if (numberFormat === 'perc') { + return Math.round(value * 100).toLocaleString() + '%'; + } else if (numberFormat === 'decimal2') { + return (Math.round(value * 100) / 100).toLocaleString(); + } else if (numberFormat === 'decimal0') { + return Math.round(value).toLocaleString(); + } else { + return Math.round(value).toLocaleString(); + } +} diff --git a/services/API-service/src/api/notification/lookup/lookup.module.ts b/services/API-service/src/api/notification/lookup/lookup.module.ts index c69fd6c58..62fcab6e7 100644 --- a/services/API-service/src/api/notification/lookup/lookup.module.ts +++ b/services/API-service/src/api/notification/lookup/lookup.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; + import { LookupService } from './lookup.service'; @Module({ diff --git a/services/API-service/src/api/notification/lookup/lookup.service.ts b/services/API-service/src/api/notification/lookup/lookup.service.ts index a50a79955..6426c8c06 100644 --- a/services/API-service/src/api/notification/lookup/lookup.service.ts +++ b/services/API-service/src/api/notification/lookup/lookup.service.ts @@ -1,4 +1,5 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + import { twilioClient } from '../whatsapp/twilio.client'; @Injectable() diff --git a/services/API-service/src/api/notification/notification-content/notification-content.module.ts b/services/API-service/src/api/notification/notification-content/notification-content.module.ts index 6369045e5..585dc94fc 100644 --- a/services/API-service/src/api/notification/notification-content/notification-content.module.ts +++ b/services/API-service/src/api/notification/notification-content/notification-content.module.ts @@ -1,5 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + +import { HelperService } from '../../../shared/helper.service'; import { AdminAreaDataModule } from '../../admin-area-data/admin-area-data.module'; import { AdminAreaDynamicDataModule } from '../../admin-area-dynamic-data/admin-area-dynamic-data.module'; import { AdminAreaModule } from '../../admin-area/admin-area.module'; @@ -8,7 +10,6 @@ import { DisasterEntity } from '../../disaster/disaster.entity'; import { EventModule } from '../../event/event.module'; import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; import { NotificationContentService } from './notification-content.service'; -import { HelperService } from '../../../shared/helper.service'; @Module({ imports: [ diff --git a/services/API-service/src/api/notification/notification-content/notification-content.service.ts b/services/API-service/src/api/notification/notification-content/notification-content.service.ts index 888262ce5..e46c1524b 100644 --- a/services/API-service/src/api/notification/notification-content/notification-content.service.ts +++ b/services/API-service/src/api/notification/notification-content/notification-content.service.ts @@ -1,18 +1,22 @@ -import { AdminAreaDynamicDataService } from '../../admin-area-dynamic-data/admin-area-dynamic-data.service'; -import { CountryEntity } from '../../country/country.entity'; import { Injectable } from '@nestjs/common'; -import { EventService } from '../../event/event.service'; import { InjectRepository } from '@nestjs/typeorm'; + import { Repository } from 'typeorm'; -import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; + +import { EventSummaryCountry, TriggeredArea } from '../../../shared/data.model'; +import { HelperService } from '../../../shared/helper.service'; import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; -import { DynamicIndicator } from '../../admin-area-dynamic-data/enum/dynamic-data-unit'; +import { CountryEntity } from '../../country/country.entity'; import { DisasterType } from '../../disaster/disaster-type.enum'; import { DisasterEntity } from '../../disaster/disaster.entity'; -import { EventSummaryCountry } from '../../../shared/data.model'; -import { AdminAreaDataService } from '../../admin-area-data/admin-area-data.service'; -import { AdminAreaService } from '../../admin-area/admin-area.service'; -import { HelperService } from '../../../shared/helper.service'; +import { EventService } from '../../event/event.service'; +import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; +import { AdminAreaLabel } from '../dto/admin-area-notification-info.dto'; +import { ContentEventEmail } from '../dto/content-trigger-email.dto'; +import { + NotificationDataPerEventDto, + TriggerStatusLabelEnum, +} from '../dto/notification-date-per-event.dto'; @Injectable() export class NotificationContentService { @@ -25,92 +29,39 @@ export class NotificationContentService { public constructor( private readonly eventService: EventService, - private readonly adminAreaDynamicDataService: AdminAreaDynamicDataService, - private readonly adminAreaDataService: AdminAreaDataService, - private readonly adminAreaService: AdminAreaService, private readonly helperService: HelperService, ) {} - public async getTotalAffectedPerLeadTime( + public async getContentTriggerNotification( country: CountryEntity, disasterType: DisasterType, - leadTime: LeadTime, - eventName: string, - ) { - const actionUnit = await this.indicatorRepository.findOne({ - where: { name: (await this.getDisaster(disasterType)).actionsUnit }, - }); - const adminLevel = country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel; - - let actionUnitValues = - await this.adminAreaDynamicDataService.getAdminAreaDynamicData( - country.countryCodeISO3, - String(adminLevel), - actionUnit.name as DynamicIndicator, - disasterType, - leadTime, - eventName, - ); - - // Filter on only the areas that are also shown in dashboard, to get same aggregate metric - const placeCodesToShow = await this.adminAreaService.getPlaceCodes( - country.countryCodeISO3, + activeEvents: EventSummaryCountry[], + ): Promise { + const content = new ContentEventEmail(); + content.disasterType = disasterType; + content.disasterTypeLabel = await this.getDisasterTypeLabel(disasterType); + content.dataPerEvent = await this.getNotificationDataForEvents( + activeEvents, + country, disasterType, - leadTime, - adminLevel, - eventName, ); - actionUnitValues = actionUnitValues.filter((row) => - placeCodesToShow.includes(row.placeCode), + content.defaultAdminLevel = this.getDefaultAdminLevel( + country, + disasterType, ); - if (!actionUnit.weightedAvg) { - // If no weightedAvg, then return early with simple sum - return actionUnitValues.reduce( - (sum, current) => sum + Number(current.value), - 0, - ); - } else { - const weighingIndicator = actionUnit.weightVar; - const weighingIndicatorValues = - await this.adminAreaDataService.getAdminAreaData( - country.countryCodeISO3, - String(adminLevel), - weighingIndicator as DynamicIndicator, - ); - weighingIndicatorValues.forEach((row) => { - row['weight'] = row.value; - delete row.value; - }); - - const mergedValues = []; - for (let i = 0; i < actionUnitValues.length; i++) { - mergedValues.push({ - ...actionUnitValues[i], - ...weighingIndicatorValues.find( - (itmInner) => itmInner.placeCode === actionUnitValues[i].placeCode, - ), - }); - } - - const sumofWeighedValues = mergedValues.reduce( - (sum, current) => - sum + - (current.weight ? Number(current.weight) : 0) * Number(current.value), - 0, - ); - const sumOfWeights = mergedValues.reduce( - (sum, current) => sum + (current.weight ? Number(current.weight) : 0), - 0, - ); - return sumofWeighedValues / sumOfWeights; - } + content.country = country; + content.indicatorMetadata = await this.getIndicatorMetadata(disasterType); + content.linkEapSop = this.getLinkEapSop(country, disasterType); + content.defaultAdminAreaLabel = this.getDefaultAdminAreaLabel( + country, + content.defaultAdminLevel, + ); + return content; } public async getCountryNotificationInfo( - countryCodeISO3, + countryCodeISO3: string, ): Promise { const findOneOptions = { countryCodeISO3: countryCodeISO3, @@ -136,7 +87,32 @@ export class NotificationContentService { }); } - public firstCharOfWordsToUpper(input: string): string { + private getDefaultAdminLevel( + country: CountryEntity, + disasterType: DisasterType, + ): number { + return country.countryDisasterSettings.find( + (s) => s.disasterType === disasterType, + ).defaultAdminLevel; + } + + private getLinkEapSop( + country: CountryEntity, + disasterType: DisasterType, + ): string { + return country.countryDisasterSettings.find( + (s) => s.disasterType === disasterType, + ).eapLink; + } + + private getDefaultAdminAreaLabel( + country: CountryEntity, + adminAreaDefaultLevel: number, + ): AdminAreaLabel { + return country.adminRegionLabels[String(adminAreaDefaultLevel)]; + } + + private firstCharOfWordsToUpper(input: string): string { return input .toLowerCase() .split(' ') @@ -144,24 +120,7 @@ export class NotificationContentService { .join(' '); } - public async getLeadTimesAcrossEvents( - countryCodeISO3: string, - disasterType: DisasterType, - events: EventSummaryCountry[], - ) { - let triggeredLeadTimes; - for await (const event of events) { - const newLeadTimes = await this.eventService.getTriggerPerLeadtime( - countryCodeISO3, - disasterType, - event.eventName, - ); - triggeredLeadTimes = { ...triggeredLeadTimes, ...newLeadTimes }; - } - return triggeredLeadTimes; - } - - public async getActionUnit( + public async getIndicatorMetadata( disasterType: DisasterType, ): Promise { return await this.indicatorRepository.findOne({ @@ -178,23 +137,135 @@ export class NotificationContentService { : (await this.getDisaster(disasterType)).label.toLowerCase(); } - public formatActionUnitValue( - value: number, - actionUnit: IndicatorMetadataEntity, - ) { - if (value === null) { - return null; - } else if (actionUnit.numberFormatMap === 'perc') { - return Math.round(value * 100).toLocaleString() + '%'; - } else if (actionUnit.numberFormatMap === 'decimal2') { - return (Math.round(value * 100) / 100).toLocaleString(); - } else if (actionUnit.numberFormatMap === 'decimal0') { - return Math.round(value).toLocaleString(); + private async getNotificationDataForEvents( + activeEvents: EventSummaryCountry[], + country: CountryEntity, + disasterType: DisasterType, + ): Promise { + const sortedEvents = this.sortEventsByLeadTime(activeEvents); + const headerEventsRows = []; + for await (const event of sortedEvents) { + headerEventsRows.push( + await this.getNotificationDataForEvent(event, country, disasterType), + ); + } + return headerEventsRows; + } + + private async getNotificationDataForEvent( + event: EventSummaryCountry, + country: CountryEntity, + disasterType: DisasterType, + ): Promise { + const data = new NotificationDataPerEventDto(); + data.triggerStatusLabel = event.thresholdReached + ? TriggerStatusLabelEnum.Trigger + : TriggerStatusLabelEnum.Warning; + + data.eventName = await this.getFormattedEventName(event, disasterType); + data.disasterSpecificCopy = await this.getDisasterSpecificCopy( + disasterType, + event.firstLeadTime, + event, + ); + data.firstLeadTime = event.firstLeadTime; + data.firstTriggerLeadTime = event.firstTriggerLeadTime; + data.triggeredAreas = await this.getSortedTriggeredAreas( + country, + disasterType, + event, + ); + data.nrOfTriggeredAreas = await this.getNrOfTriggeredAreas( + data.triggeredAreas, + data.triggerStatusLabel, + disasterType, + ); + // This looks weird, but as far as I understand the startDate of the event is the day it was first issued + data.issuedDate = new Date(event.startDate); + data.firstLeadTimeString = await this.getFirstLeadTimeString( + event, + event.countryCodeISO3, + disasterType, + ); + data.firstTriggerLeadTimeString = await this.getFirstTriggerLeadTimeString( + event, + event.countryCodeISO3, + disasterType, + ); + + data.totalAffectedOfIndicator = this.getTotalAffectedPerEvent( + data.triggeredAreas, + ); + data.mapImage = await this.eventService.getEventMapImage( + country.countryCodeISO3, + disasterType, + event.eventName || 'no-name', + ); + data.eapAlertClass = event.disasterSpecificProperties?.eapAlertClass; + return data; + } + + private async getNrOfTriggeredAreas( + triggeredAreas: TriggeredArea[], + statusLabel: TriggerStatusLabelEnum, + disasterType: DisasterType, + ): Promise { + // This filters out the areas that are affected by the event but do not have any affect action units + // Affected action units are for example people_affected, houses_affected, etc (differs per disaster type) + // We are not sure why this is done, but it is done in the original code + // For warning flood events this is not done, because there are no flood extens for warning events so we do not know any actions values + if ( + disasterType === DisasterType.Floods && + statusLabel === TriggerStatusLabelEnum.Warning + ) { + return triggeredAreas.length; } else { - return Math.round(value).toLocaleString(); + const triggeredAreasWithoutActionValue = triggeredAreas.filter( + (a) => a.actionsValue > 0, + ); + return triggeredAreasWithoutActionValue.length; } } + private async getSortedTriggeredAreas( + country: CountryEntity, + disasterType: DisasterType, + event: EventSummaryCountry, + ): Promise { + const defaultAdminLevel = this.getDefaultAdminLevel(country, disasterType); + const triggeredAreas = await this.eventService.getTriggeredAreas( + country.countryCodeISO3, + disasterType, + defaultAdminLevel, + event.firstLeadTime, + event.eventName, + ); + triggeredAreas.sort((a, b) => (a.triggerValue > b.triggerValue ? -1 : 1)); + return triggeredAreas; + } + + private sortEventsByLeadTime( + arr: EventSummaryCountry[], + ): EventSummaryCountry[] { + const leadTimeValue = (leadTime: LeadTime): number => + Number(leadTime.split('-')[0]); + + return arr.sort((a, b) => { + if (leadTimeValue(a.firstLeadTime) < leadTimeValue(b.firstLeadTime)) { + return -1; + } + if (leadTimeValue(a.firstLeadTime) > leadTimeValue(b.firstLeadTime)) { + return 1; + } + + return 0; + }); + } + + private getTotalAffectedPerEvent(adminAreas: TriggeredArea[]) { + return adminAreas.reduce((acc, cur) => acc + cur.actionsValue, 0); + } + private async getFirstLeadTimeDate( value: number, unit: string, @@ -214,7 +285,7 @@ export class NotificationContentService { }; const dayOption: Intl.DateTimeFormatOptions = - unit === 'month' ? {} : { day: '2-digit' }; + unit === 'month' ? {} : { day: '2-digit', weekday: 'long' }; return new Date(getNewDate[unit]).toLocaleDateString('default', { ...dayOption, @@ -223,94 +294,53 @@ export class NotificationContentService { }); } - public async getLeadTimeListEvent( - country: CountryEntity, + public async getFirstLeadTimeString( event: EventSummaryCountry, + countryCodeISO3: string, disasterType: DisasterType, - leadTime: LeadTime, - date: Date, - ) { - const [leadTimeValue, leadTimeUnit] = leadTime.split('-'); - const eventName = await this.getFormattedEventName(event, disasterType); - const triggerStatus = event.thresholdReached ? 'Trigger' : 'Warning'; - const dateTimePreposition = leadTimeUnit === 'month' ? 'in' : 'on'; - const dateAndTime = await this.getFirstLeadTimeDate( - Number(leadTimeValue), - leadTimeUnit, - country.countryCodeISO3, + date?: Date, + ): Promise { + return this.getEventTimeString( + event.firstLeadTime, + countryCodeISO3, disasterType, date, ); - const disasterSpecificCopy = await this.getDisasterSpecificCopy( - disasterType, - leadTime, - event, - ); - const leadTimeFromNow = `${leadTimeValue} ${leadTimeUnit}s`; - - const leadTimeString = disasterSpecificCopy.leadTimeString - ? disasterSpecificCopy.leadTimeString - : leadTimeFromNow; - - const timestamp = disasterSpecificCopy.timestamp - ? ` at ${disasterSpecificCopy.timestamp}` - : ''; - - const triggeredAreas = await this.eventService.getTriggeredAreas( - country.countryCodeISO3, - disasterType, - country.countryDisasterSettings.find( - (d) => d.disasterType === disasterType, - ).defaultAdminLevel, - event.firstLeadTime, - event.eventName, - ); - const nrTriggeredAreas = triggeredAreas.filter( - (a) => a.actionsValue > 0, - ).length; + } - return { - short: `${triggerStatus} for ${eventName}: ${ - disasterSpecificCopy.extraInfo || leadTime === LeadTime.hour0 - ? leadTimeString - : `${dateAndTime}${timestamp}` - }
`, - long: `A ${triggerStatus.toLowerCase()} for ${eventName} is issued. -

- ${disasterSpecificCopy.eventStatus || 'It is forecasted: '}${ - disasterSpecificCopy.extraInfo || leadTime === LeadTime.hour0 - ? '' - : ` ${dateTimePreposition} ${dateAndTime}${timestamp}` - }. ${disasterSpecificCopy.extraInfo} -

- There are ${nrTriggeredAreas} potentially exposed (ADMIN-AREA-PLURAL). They are listed below in order of (EXPOSURE-UNIT). -

- This ${triggerStatus.toLowerCase()} was issued by IBF on ${await this.getFirstLeadTimeDate( - 0, - leadTimeUnit, - country.countryCodeISO3, + public async getFirstTriggerLeadTimeString( + event: EventSummaryCountry, + countryCodeISO3: string, + disasterType: DisasterType, + date?: Date, + ): Promise { + if (event.firstTriggerLeadTime) { + return this.getEventTimeString( + event.firstTriggerLeadTime, + countryCodeISO3, disasterType, - new Date(event.startDate), - )}. -

`, - }; + date, + ); + } else { + return null; + } } - public async getStartTimeEvent( - event: EventSummaryCountry, + private async getEventTimeString( + leadTime: LeadTime, countryCodeISO3: string, disasterType: DisasterType, date?: Date, - ) { + ): Promise { const startDateFirstEvent = await this.getFirstLeadTimeDate( - Number(event.firstLeadTime.split('-')[0]), - event.firstLeadTime.split('-')[1], + Number(leadTime.split('-')[0]), + leadTime.split('-')[1], countryCodeISO3, disasterType, date, ); const startTimeFirstEvent = await this.getLeadTimeTimestamp( - event.firstLeadTime, + leadTime, countryCodeISO3, disasterType, ); diff --git a/services/API-service/src/api/notification/notification.controller.ts b/services/API-service/src/api/notification/notification.controller.ts index 77513fbdc..65b1d12a6 100644 --- a/services/API-service/src/api/notification/notification.controller.ts +++ b/services/API-service/src/api/notification/notification.controller.ts @@ -1,8 +1,9 @@ -import { NotificationService } from './notification.service'; import { Body, Controller, + ParseBoolPipe, Post, + Query, UseGuards, UseInterceptors, } from '@nestjs/common'; @@ -10,13 +11,17 @@ import { ApiBearerAuth, ApiConsumes, ApiOperation, + ApiQuery, ApiResponse, ApiTags, } from '@nestjs/swagger'; -import { RolesGuard } from '../../roles.guard'; -import { SendNotificationDto } from './dto/send-notification.dto'; + import { Roles } from '../../roles.decorator'; +import { RolesGuard } from '../../roles.guard'; import { UserRole } from '../user/user-role.enum'; +import { NotificationApiTestResponseDto } from './dto/notification-api-test-response.dto'; +import { SendNotificationDto } from './dto/send-notification.dto'; +import { NotificationService } from './notification.service'; @ApiBearerAuth() @UseGuards(RolesGuard) @@ -38,15 +43,30 @@ export class NotificationController { description: 'Notification request sent (actual e-mails/whatsapps sent only if there is an active event)', }) + @ApiQuery({ + name: 'isApiTest', + required: false, + type: 'boolean', + description: + 'If true, only returns the notification content without sending it', + }) @Post('send') @ApiConsumes() @UseInterceptors() - public async exposure( + public async send( @Body() sendNotification: SendNotificationDto, - ): Promise { - await this.notificationService.send( + @Query( + 'isApiTest', + new ParseBoolPipe({ + optional: true, + }), + ) + isApiTest: boolean, + ): Promise { + return await this.notificationService.send( sendNotification.countryCodeISO3, sendNotification.disasterType, + isApiTest, sendNotification.date, ); } diff --git a/services/API-service/src/api/notification/notification.module.ts b/services/API-service/src/api/notification/notification.module.ts index 0ba4d4a90..833c61d7b 100644 --- a/services/API-service/src/api/notification/notification.module.ts +++ b/services/API-service/src/api/notification/notification.module.ts @@ -1,16 +1,18 @@ -import { AdminAreaDynamicDataModule } from './../admin-area-dynamic-data/admin-area-dynamic-data.module'; -import { NotificationInfoEntity } from './notifcation-info.entity'; -import { EventModule } from './../event/event.module'; import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { IndicatorMetadataEntity } from '../metadata/indicator-metadata.entity'; +import { TyphoonTrackModule } from '../typhoon-track/typhoon-track.module'; import { UserModule } from '../user/user.module'; +import { AdminAreaDynamicDataModule } from './../admin-area-dynamic-data/admin-area-dynamic-data.module'; +import { EventModule } from './../event/event.module'; +import { EmailTemplateService } from './email/email-template.service'; +import { EmailService } from './email/email.service'; +import { NotificationInfoEntity } from './notifcation-info.entity'; +import { NotificationContentModule } from './notification-content/notification-content.module'; import { NotificationController } from './notification.controller'; import { NotificationService } from './notification.service'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { IndicatorMetadataEntity } from '../metadata/indicator-metadata.entity'; import { WhatsappModule } from './whatsapp/whatsapp.module'; -import { NotificationContentModule } from './notification-content/notification-content.module'; -import { EmailService } from './email/email.service'; -import { TyphoonTrackModule } from '../typhoon-track/typhoon-track.module'; @Module({ imports: [ @@ -23,6 +25,6 @@ import { TyphoonTrackModule } from '../typhoon-track/typhoon-track.module'; TyphoonTrackModule, ], controllers: [NotificationController], - providers: [NotificationService, EmailService], + providers: [NotificationService, EmailService, EmailTemplateService], }) export class NotificationModule {} diff --git a/services/API-service/src/api/notification/notification.service.ts b/services/API-service/src/api/notification/notification.service.ts index 1352a256f..514a03a19 100644 --- a/services/API-service/src/api/notification/notification.service.ts +++ b/services/API-service/src/api/notification/notification.service.ts @@ -1,12 +1,17 @@ import { Injectable } from '@nestjs/common'; -import { EventService } from '../event/event.service'; -import { DisasterType } from '../disaster/disaster-type.enum'; -import { WhatsappService } from './whatsapp/whatsapp.service'; -import { NotificationContentService } from './notification-content/notification-content.service'; -import { EmailService } from './email/email.service'; -import { TyphoonTrackService } from '../typhoon-track/typhoon-track.service'; + import { EventSummaryCountry } from '../../shared/data.model'; import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; +import { DisasterType } from '../disaster/disaster-type.enum'; +import { EventService } from '../event/event.service'; +import { TyphoonTrackService } from '../typhoon-track/typhoon-track.service'; +import { + NotificationApiTestResponseChannelDto, + NotificationApiTestResponseDto, +} from './dto/notification-api-test-response.dto'; +import { EmailService } from './email/email.service'; +import { NotificationContentService } from './notification-content/notification-content.service'; +import { WhatsappService } from './whatsapp/whatsapp.service'; @Injectable() export class NotificationService { @@ -21,96 +26,137 @@ export class NotificationService { public async send( countryCodeISO3: string, disasterType: DisasterType, + isApiTest: boolean, date?: Date, - ): Promise { + ): Promise { + const apiTestResponse = new NotificationApiTestResponseDto(); + const apiTestReponseActive = await this.sendNotiFicationsActiveEvents( + disasterType, + countryCodeISO3, + isApiTest, + date, + ); + if (isApiTest && apiTestReponseActive) { + apiTestResponse.activeEvents = apiTestReponseActive; + } + + if (disasterType === DisasterType.Floods) { + // Sending finished events is now for floods only + const apiTestReponseFinished = await this.sendNotificationsFinishedEvents( + countryCodeISO3, + disasterType, + isApiTest, + date, + ); + if (isApiTest && apiTestReponseFinished) { + apiTestResponse.finishedEvents = apiTestReponseFinished; + } + } + // REFACTOR: First close finished events. This is ideally done through separate endpoint called at end of pipeline, but that would require all pipelines to be updated. // Instead, making use of this endpoint which is already called at the end of every pipeline await this.eventService.closeEventsAutomatic(countryCodeISO3, disasterType); + if (isApiTest) { + return apiTestResponse; + } + } + + private async sendNotiFicationsActiveEvents( + disasterType: DisasterType, + countryCodeISO3: string, + isApiTest: boolean, + date?: Date, + ): Promise { + const apiTestReponseActive = new NotificationApiTestResponseChannelDto(); + const events = await this.eventService.getEventSummary( countryCodeISO3, disasterType, ); - - const activeEvents: EventSummaryCountry[] = []; - let finishedEvent: EventSummaryCountry; // This is now for floods only, so can only be 1 event, so not an array + const activeNotifiableEvents: EventSummaryCountry[] = []; for await (const event of events) { if ( - await this.getNotifiableActiveEvent( - event, - disasterType, - countryCodeISO3, - ) + await this.isNotifiableActiveEvent(event, disasterType, countryCodeISO3) ) { - activeEvents.push(event); - } else if (this.getFinishedEvent(event, disasterType, date)) { - finishedEvent = event; + activeNotifiableEvents.push(event); } } - if (activeEvents.length) { + + if (activeNotifiableEvents.length) { const country = await this.notificationContentService.getCountryNotificationInfo( countryCodeISO3, ); - this.emailService.sendTriggerEmail( + const messageForApiTest = await this.emailService.sendTriggerEmail( country, disasterType, - activeEvents, + activeNotifiableEvents, + isApiTest, date, ); - + if (isApiTest && messageForApiTest) { + apiTestReponseActive.email = messageForApiTest; + } if (country.notificationInfo.useWhatsapp[disasterType]) { this.whatsappService.sendTriggerWhatsapp( country, - activeEvents, + activeNotifiableEvents, disasterType, ); } } + if (isApiTest) { + return apiTestReponseActive; + } + } + + private async sendNotificationsFinishedEvents( + countryCodeISO3: string, + disasterType: DisasterType, + isApiTest: boolean, + date?: Date, + ): Promise { + const apiTestReponseFinished = new NotificationApiTestResponseChannelDto(); + const finishedNotifiableEvents = + await this.eventService.getEventsSummaryTriggerFinishedMail( + countryCodeISO3, + disasterType, + ); - if (finishedEvent) { + if (finishedNotifiableEvents.length > 0) { const country = await this.notificationContentService.getCountryNotificationInfo( countryCodeISO3, ); - this.emailService.sendTriggerFinishedEmail( + const emailFinished = await this.emailService.sendTriggerFinishedEmail( country, disasterType, - finishedEvent, + finishedNotifiableEvents, + isApiTest, date, ); + if (isApiTest && emailFinished) { + apiTestReponseFinished.email = emailFinished; + } if (country.notificationInfo.useWhatsapp[disasterType]) { - this.whatsappService.sendTriggerFinishedWhatsapp( - country, - finishedEvent, - disasterType, - ); + for (const event of finishedNotifiableEvents) { + await this.whatsappService.sendTriggerFinishedWhatsapp( + country, + event, + disasterType, + ); + } } - } - } - - private getFinishedEvent( - event: EventSummaryCountry, - disasterType: DisasterType, - uploadDate?: Date, - ) { - // For now only do this for floods - if (disasterType === DisasterType.Floods) { - const date = uploadDate ? new Date(uploadDate) : new Date(); - const yesterdayActiveDate = new Date(date.setDate(date.getDate() + 6)); // determine yesterday still active events by endDate lying (7 - 1) days in the future - if ( - new Date(event.endDate) >= - new Date(yesterdayActiveDate.setHours(0, 0, 0, 0)) - ) { - return true; + if (isApiTest) { + return apiTestReponseFinished; } } - return false; } - private async getNotifiableActiveEvent( + private async isNotifiableActiveEvent( event: EventSummaryCountry, disasterType: DisasterType, countryCodeISO3: string, diff --git a/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts b/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts index 7f6ea7ea5..7985f19c0 100644 --- a/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts +++ b/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts @@ -1,6 +1,8 @@ import { HttpStatus, Injectable, NestMiddleware } from '@nestjs/common'; import { HttpException } from '@nestjs/common/exceptions/http.exception'; + import { NextFunction, Request, Response } from 'express'; + import { DEBUG, EXTERNAL_API } from '../../../config'; import { twilio } from './twilio.client'; @@ -8,11 +10,7 @@ import { twilio } from './twilio.client'; export class AuthMiddlewareTwilio implements NestMiddleware { public constructor() {} - public async use( - req: Request, - res: Response, - next: NextFunction, - ): Promise { + public async use(req: Request, res: Response, next: NextFunction) { const twilioSignature = req.headers['x-twilio-signature']; if (DEBUG) { diff --git a/services/API-service/src/api/notification/whatsapp/twilio.dto.ts b/services/API-service/src/api/notification/whatsapp/twilio.dto.ts index 3b6f1bcb1..5e3acc318 100644 --- a/services/API-service/src/api/notification/whatsapp/twilio.dto.ts +++ b/services/API-service/src/api/notification/whatsapp/twilio.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; + import { IsOptional, IsString } from 'class-validator'; export enum TwilioStatus { diff --git a/services/API-service/src/api/notification/whatsapp/whatsapp.controller.ts b/services/API-service/src/api/notification/whatsapp/whatsapp.controller.ts index 79db69d44..a067fb05e 100644 --- a/services/API-service/src/api/notification/whatsapp/whatsapp.controller.ts +++ b/services/API-service/src/api/notification/whatsapp/whatsapp.controller.ts @@ -1,5 +1,6 @@ import { Body, Controller, Post } from '@nestjs/common'; import { ApiConsumes, ApiTags } from '@nestjs/swagger'; + import { SendTestWhatsappDto, TwilioIncomingCallbackDto, diff --git a/services/API-service/src/api/notification/whatsapp/whatsapp.module.ts b/services/API-service/src/api/notification/whatsapp/whatsapp.module.ts index e79653ba0..653537e2e 100644 --- a/services/API-service/src/api/notification/whatsapp/whatsapp.module.ts +++ b/services/API-service/src/api/notification/whatsapp/whatsapp.module.ts @@ -5,6 +5,7 @@ import { RequestMethod, } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { API_PATHS } from '../../../config'; import { CountryEntity } from '../../country/country.entity'; import { EventMapImageEntity } from '../../event/event-map-image.entity'; diff --git a/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts b/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts index 675bb16f9..681ff1699 100644 --- a/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts +++ b/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts @@ -1,6 +1,8 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { IsNull, Not, Repository } from 'typeorm'; + import { EXTERNAL_API } from '../../../config'; import { EventSummaryCountry } from '../../../shared/data.model'; import { CountryEntity } from '../../country/country.entity'; @@ -8,6 +10,7 @@ import { DisasterType } from '../../disaster/disaster-type.enum'; import { EventMapImageEntity } from '../../event/event-map-image.entity'; import { EventService } from '../../event/event.service'; import { UserEntity } from '../../user/user.entity'; +import { formatActionUnitValue } from '../helpers/format-action-unit-value.helper'; import { LookupService } from '../lookup/lookup.service'; import { NotificationContentService } from '../notification-content/notification-content.service'; import { twilioClient } from './twilio.client'; @@ -48,7 +51,7 @@ export class WhatsappService { message: string, recipientPhoneNr: string, mediaUrl?: string, - ): Promise { + ) { const payload = { body: message, messagingServiceSid: process.env.TWILIO_MESSAGING_SID, @@ -87,7 +90,7 @@ export class WhatsappService { ? 'trigger' : 'warning'; const startTimeEvent = - await this.notificationContentService.getStartTimeEvent( + await this.notificationContentService.getFirstLeadTimeString( activeEvents[0], country.countryCodeISO3, disasterType, @@ -105,7 +108,7 @@ export class WhatsappService { ]; const startTimeFirstEvent = - await this.notificationContentService.getStartTimeEvent( + await this.notificationContentService.getFirstLeadTimeString( activeEvents[0], country.countryCodeISO3, disasterType, @@ -255,8 +258,7 @@ export class WhatsappService { events, disasterType.disasterType, ); - await this.sendWhatsapp(noTriggerMessage, fromNumber); - return; + return await this.sendWhatsapp(noTriggerMessage, fromNumber); } for (const event of sortedEvents) { @@ -369,22 +371,21 @@ export class WhatsappService { const adminAreaLabel = country.adminRegionLabels[String(adminLevel)]['plural'].toLowerCase(); - const actionUnit = await this.notificationContentService.getActionUnit( - disasterType, - ); + const indicatorMetadata = + await this.notificationContentService.getIndicatorMetadata(disasterType); let areaList = ''; for (const area of triggeredAreas) { const row = `- *${area.name}${ area.nameParent ? ' (' + area.nameParent + ')' : '' - } - ${this.notificationContentService.formatActionUnitValue( + } - ${formatActionUnitValue( area.actionsValue, - actionUnit, + indicatorMetadata.numberFormatMap, )}*\n`; areaList += row; } const startTimeEvent = - await this.notificationContentService.getStartTimeEvent( + await this.notificationContentService.getFirstLeadTimeString( event, country.countryCodeISO3, disasterType, diff --git a/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts b/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts index 031f95b1b..5f88e3e68 100644 --- a/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsEnum, @@ -6,11 +9,10 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; import { DisasterType } from '../../disaster/disaster-type.enum'; import { PointDataEnum } from '../point-data.entity'; -import { Type } from 'class-transformer'; export class UploadAssetExposureStatusDto { @ApiProperty({ example: ['123', '234'] }) diff --git a/services/API-service/src/api/point-data/dto/upload-community-notifications.dto.ts b/services/API-service/src/api/point-data/dto/upload-community-notifications.dto.ts index 4be79b600..d330f62de 100644 --- a/services/API-service/src/api/point-data/dto/upload-community-notifications.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-community-notifications.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class CommunityNotificationDto { @ApiProperty({ example: 'nameVolunteer' }) @IsNotEmpty() diff --git a/services/API-service/src/api/point-data/dto/upload-dam-sites.dto.ts b/services/API-service/src/api/point-data/dto/upload-dam-sites.dto.ts index 884515095..d8880551c 100644 --- a/services/API-service/src/api/point-data/dto/upload-dam-sites.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-dam-sites.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class DamSiteDto { @ApiProperty({ example: 'name' }) @IsNotEmpty() diff --git a/services/API-service/src/api/point-data/dto/upload-evacuation-centers.dto.ts b/services/API-service/src/api/point-data/dto/upload-evacuation-centers.dto.ts index 570099bdf..ae7cf3e0c 100644 --- a/services/API-service/src/api/point-data/dto/upload-evacuation-centers.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-evacuation-centers.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; + import { IsNotEmpty, IsString } from 'class-validator'; export class EvacuationCenterDto { diff --git a/services/API-service/src/api/point-data/dto/upload-gauge.dto.ts b/services/API-service/src/api/point-data/dto/upload-gauge.dto.ts index b64eb1c74..5f39ba5a9 100644 --- a/services/API-service/src/api/point-data/dto/upload-gauge.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-gauge.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + export class GaugeDto { @ApiProperty({ example: 'name' }) @IsString() diff --git a/services/API-service/src/api/point-data/dto/upload-glofas-station.dto.ts b/services/API-service/src/api/point-data/dto/upload-glofas-station.dto.ts index 5df1ce898..337fe39ca 100644 --- a/services/API-service/src/api/point-data/dto/upload-glofas-station.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-glofas-station.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + export class GlofasStationDto { @ApiProperty({ example: 'G5100' }) @IsString() diff --git a/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts b/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts index 0d109f291..45bf22447 100644 --- a/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; + import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class HealthSiteDto { diff --git a/services/API-service/src/api/point-data/dto/upload-red-cross-branch.dto.ts b/services/API-service/src/api/point-data/dto/upload-red-cross-branch.dto.ts index 4b9c1b2f8..cd160a223 100644 --- a/services/API-service/src/api/point-data/dto/upload-red-cross-branch.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-red-cross-branch.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + export class RedCrossBranchDto { @ApiProperty({ example: 'branch name' }) @IsNotEmpty() diff --git a/services/API-service/src/api/point-data/dto/upload-schools.dto.ts b/services/API-service/src/api/point-data/dto/upload-schools.dto.ts index e2b1419ce..d1e1cd08b 100644 --- a/services/API-service/src/api/point-data/dto/upload-schools.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-schools.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class SchoolDto { @ApiProperty({ example: 'name' }) @IsString() diff --git a/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts b/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts index a98238e5a..d42a501cb 100644 --- a/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class WaterpointDto { @ApiProperty({ example: 'name' }) @IsString() diff --git a/services/API-service/src/api/point-data/dynamic-point-data.entity.ts b/services/API-service/src/api/point-data/dynamic-point-data.entity.ts index b8b228b96..6b56bcde9 100644 --- a/services/API-service/src/api/point-data/dynamic-point-data.entity.ts +++ b/services/API-service/src/api/point-data/dynamic-point-data.entity.ts @@ -1,13 +1,14 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, - JoinColumn, + Entity, Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; -import { PointDataEntity } from './point-data.entity'; + import { LeadTimeEntity } from '../lead-time/lead-time.entity'; +import { PointDataEntity } from './point-data.entity'; @Entity('dynamic-point-data') export class DynamicPointDataEntity { diff --git a/services/API-service/src/api/point-data/point-data.controller.ts b/services/API-service/src/api/point-data/point-data.controller.ts index aa7e0b42b..884734e4d 100644 --- a/services/API-service/src/api/point-data/point-data.controller.ts +++ b/services/API-service/src/api/point-data/point-data.controller.ts @@ -21,16 +21,17 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; +import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; import { GeoJson } from '../../shared/geo.model'; import { UserRole } from '../user/user-role.enum'; -import { PointDataService } from './point-data.service'; import { UploadAssetExposureStatusDto, UploadDynamicPointDataDto, } from './dto/upload-asset-exposure-status.dto'; -import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; +import { CommunityNotification, PointDataService } from './point-data.service'; @ApiBearerAuth() @ApiTags('point-data') @@ -101,7 +102,7 @@ export class PointDataController { @Post('community-notification/:countryCodeISO3') public async uploadCommunityNotification( @Param() params, - @Body() communityNotification: any, + @Body() communityNotification: CommunityNotification, ): Promise { return await this.pointDataService.uploadCommunityNotification( params.countryCodeISO3, diff --git a/services/API-service/src/api/point-data/point-data.entity.ts b/services/API-service/src/api/point-data/point-data.entity.ts index 27c87058c..cf0bb940c 100644 --- a/services/API-service/src/api/point-data/point-data.entity.ts +++ b/services/API-service/src/api/point-data/point-data.entity.ts @@ -1,4 +1,5 @@ -import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; + import { DynamicPointDataEntity } from './dynamic-point-data.entity'; export enum PointDataEnum { diff --git a/services/API-service/src/api/point-data/point-data.module.ts b/services/API-service/src/api/point-data/point-data.module.ts index d615a9994..eb97f1758 100644 --- a/services/API-service/src/api/point-data/point-data.module.ts +++ b/services/API-service/src/api/point-data/point-data.module.ts @@ -1,13 +1,14 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; import { WhatsappModule } from '../notification/whatsapp/whatsapp.module'; import { UserModule } from '../user/user.module'; +import { DynamicPointDataEntity } from './dynamic-point-data.entity'; import { PointDataController } from './point-data.controller'; import { PointDataEntity } from './point-data.entity'; import { PointDataService } from './point-data.service'; -import { HttpModule } from '@nestjs/axios'; -import { DynamicPointDataEntity } from './dynamic-point-data.entity'; @Module({ imports: [ diff --git a/services/API-service/src/api/point-data/point-data.service.ts b/services/API-service/src/api/point-data/point-data.service.ts index ae14ec8a7..d00d8fd32 100644 --- a/services/API-service/src/api/point-data/point-data.service.ts +++ b/services/API-service/src/api/point-data/point-data.service.ts @@ -1,26 +1,38 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { validate } from 'class-validator'; +import { IsNull, MoreThanOrEqual, Repository } from 'typeorm'; + import { GeoJson } from '../../shared/geo.model'; import { HelperService } from '../../shared/helper.service'; -import { IsNull, MoreThanOrEqual, Repository } from 'typeorm'; -import { EvacuationCenterDto } from './dto/upload-evacuation-centers.dto'; -import { PointDataEntity, PointDataEnum } from './point-data.entity'; -import { DamSiteDto } from './dto/upload-dam-sites.dto'; -import { HealthSiteDto } from './dto/upload-health-sites.dto'; -import { RedCrossBranchDto } from './dto/upload-red-cross-branch.dto'; -import { CommunityNotificationDto } from './dto/upload-community-notifications.dto'; +import { DisasterType } from '../disaster/disaster-type.enum'; import { WhatsappService } from '../notification/whatsapp/whatsapp.service'; -import { SchoolDto } from './dto/upload-schools.dto'; -import { WaterpointDto } from './dto/upload-waterpoint.dto'; import { UploadAssetExposureStatusDto, UploadDynamicPointDataDto, } from './dto/upload-asset-exposure-status.dto'; -import { DisasterType } from '../disaster/disaster-type.enum'; +import { CommunityNotificationDto } from './dto/upload-community-notifications.dto'; +import { DamSiteDto } from './dto/upload-dam-sites.dto'; +import { EvacuationCenterDto } from './dto/upload-evacuation-centers.dto'; import { GaugeDto } from './dto/upload-gauge.dto'; -import { DynamicPointDataEntity } from './dynamic-point-data.entity'; import { GlofasStationDto } from './dto/upload-glofas-station.dto'; +import { HealthSiteDto } from './dto/upload-health-sites.dto'; +import { RedCrossBranchDto } from './dto/upload-red-cross-branch.dto'; +import { SchoolDto } from './dto/upload-schools.dto'; +import { WaterpointDto } from './dto/upload-waterpoint.dto'; +import { DynamicPointDataEntity } from './dynamic-point-data.entity'; +import { PointDataEntity, PointDataEnum } from './point-data.entity'; + +export interface CommunityNotification { + nameVolunteer: string; + nameVillage: string; + disasterType: string; + description: string; + end: Date; + _attachments: [{ download_url: string }]; + _geolocation: [number, number]; +} @Injectable() export class PointDataService { @@ -89,7 +101,7 @@ export class PointDataService { return this.helperService.toGeojson(pointData); } - private getDtoPerPointDataCategory(pointDataCategory: PointDataEnum): any { + private getDtoPerPointDataCategory(pointDataCategory: PointDataEnum) { switch (pointDataCategory) { case PointDataEnum.dams: return new DamSiteDto(); @@ -120,6 +132,7 @@ export class PointDataService { public async uploadJson( pointDataCategory: PointDataEnum, countryCodeISO3: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any validatedObjArray: any, deleteExisting = true, ) { @@ -170,7 +183,7 @@ export class PointDataService { csvArray, ): Promise { const errors = []; - const validatatedArray = []; + const validatedArray = []; for (const [i, row] of csvArray.entries()) { const dto = this.getDtoPerPointDataCategory(pointDataCategory); for (const attribute in dto) { @@ -185,12 +198,12 @@ export class PointDataService { const errorObj = { lineNumber: i + 1, validationError: result }; errors.push(errorObj); } - validatatedArray.push(dto); + validatedArray.push(dto); } if (errors.length > 0) { throw new HttpException(errors, HttpStatus.BAD_REQUEST); } - return validatatedArray; + return validatedArray; } public async dismissCommunityNotification(pointDataId: string) { @@ -209,7 +222,7 @@ export class PointDataService { public async uploadCommunityNotification( countryCodeISO3: string, - communityNotification: any, + communityNotification: CommunityNotification, ): Promise { const notification = new CommunityNotificationDto(); notification.nameVolunteer = communityNotification['nameVolunteer']; diff --git a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.controller.ts b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.controller.ts index aaf39d3fc..a26a065f2 100644 --- a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.controller.ts +++ b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.controller.ts @@ -6,6 +6,7 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { RolesGuard } from '../../roles.guard'; import { RainfallTriggersEntity } from './rainfall-triggers.entity'; import { RainfallTriggersService } from './rainfall-triggers.service'; diff --git a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.entity.ts b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.entity.ts index 4a37bbc1d..a65c862a8 100644 --- a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.entity.ts +++ b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.entity.ts @@ -1,11 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, + Entity, JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; import { CountryEntity } from '../country/country.entity'; diff --git a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.module.ts b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.module.ts index 8417dd78e..ab54500e2 100644 --- a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.module.ts +++ b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.module.ts @@ -1,10 +1,11 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { UserModule } from '../user/user.module'; import { RainfallTriggersController } from './rainfall-triggers.controller'; import { RainfallTriggersEntity } from './rainfall-triggers.entity'; import { RainfallTriggersService } from './rainfall-triggers.service'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.service.ts b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.service.ts index 49caa6e4a..532d08dec 100644 --- a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.service.ts +++ b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { Repository } from 'typeorm'; + import { RainfallTriggersEntity } from './rainfall-triggers.entity'; @Injectable() diff --git a/services/API-service/src/api/typhoon-track/dto/trackpoint-details.ts b/services/API-service/src/api/typhoon-track/dto/trackpoint-details.ts index cd6519f34..d1ea98640 100644 --- a/services/API-service/src/api/typhoon-track/dto/trackpoint-details.ts +++ b/services/API-service/src/api/typhoon-track/dto/trackpoint-details.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsBoolean, IsDate, @@ -5,8 +8,6 @@ import { IsNotEmpty, IsNumber, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; export enum TyphoonCategory { TD = 'TD', diff --git a/services/API-service/src/api/typhoon-track/dto/upload-typhoon-track.ts b/services/API-service/src/api/typhoon-track/dto/upload-typhoon-track.ts index 2534316c9..2b0bda241 100644 --- a/services/API-service/src/api/typhoon-track/dto/upload-typhoon-track.ts +++ b/services/API-service/src/api/typhoon-track/dto/upload-typhoon-track.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, @@ -5,9 +8,8 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; -import { Type } from 'class-transformer'; import { TrackpointDetailsDto } from './trackpoint-details'; export class UploadTyphoonTrackDto { diff --git a/services/API-service/src/api/typhoon-track/typhoon-track.controller.ts b/services/API-service/src/api/typhoon-track/typhoon-track.controller.ts index ed02b04c3..93382493a 100644 --- a/services/API-service/src/api/typhoon-track/typhoon-track.controller.ts +++ b/services/API-service/src/api/typhoon-track/typhoon-track.controller.ts @@ -15,6 +15,7 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; import { GeoJson } from '../../shared/geo.model'; diff --git a/services/API-service/src/api/typhoon-track/typhoon-track.entity.ts b/services/API-service/src/api/typhoon-track/typhoon-track.entity.ts index a8511b8ac..34180fe60 100644 --- a/services/API-service/src/api/typhoon-track/typhoon-track.entity.ts +++ b/services/API-service/src/api/typhoon-track/typhoon-track.entity.ts @@ -1,11 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, + Entity, JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { GeoJson } from '../../shared/geo.model'; import { CountryEntity } from '../country/country.entity'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; diff --git a/services/API-service/src/api/typhoon-track/typhoon-track.module.ts b/services/API-service/src/api/typhoon-track/typhoon-track.module.ts index ed01ec32c..d21bf08a1 100644 --- a/services/API-service/src/api/typhoon-track/typhoon-track.module.ts +++ b/services/API-service/src/api/typhoon-track/typhoon-track.module.ts @@ -1,12 +1,13 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; import { TriggerPerLeadTime } from '../event/trigger-per-lead-time.entity'; import { UserModule } from '../user/user.module'; import { TyphoonTrackController } from './typhoon-track.controller'; import { TyphoonTrackEntity } from './typhoon-track.entity'; import { TyphoonTrackService } from './typhoon-track.service'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/typhoon-track/typhoon-track.service.ts b/services/API-service/src/api/typhoon-track/typhoon-track.service.ts index f44eff507..cd244a4f4 100644 --- a/services/API-service/src/api/typhoon-track/typhoon-track.service.ts +++ b/services/API-service/src/api/typhoon-track/typhoon-track.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { InsertResult, MoreThanOrEqual, Repository } from 'typeorm'; + import { DisasterSpecificProperties } from '../../shared/data.model'; import { GeoJson } from '../../shared/geo.model'; import { HelperService } from '../../shared/helper.service'; diff --git a/services/API-service/src/api/user/dto/create-user.dto.ts b/services/API-service/src/api/user/dto/create-user.dto.ts index 658e79d3c..cdc70f851 100644 --- a/services/API-service/src/api/user/dto/create-user.dto.ts +++ b/services/API-service/src/api/user/dto/create-user.dto.ts @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { ArrayNotEmpty, IsArray, @@ -9,11 +11,11 @@ import { IsString, MinLength, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { UserRole } from '../user-role.enum'; -import { UserStatus } from '../user-status.enum'; + import countries from '../../../scripts/json/countries.json'; import disasterTypes from '../../../scripts/json/disasters.json'; +import { UserRole } from '../user-role.enum'; +import { UserStatus } from '../user-status.enum'; const userRoleArray = Object.values(UserRole).map((item) => String(item)); diff --git a/services/API-service/src/api/user/dto/delete-user.dto.ts b/services/API-service/src/api/user/dto/delete-user.dto.ts index 156605bc9..6607f952f 100644 --- a/services/API-service/src/api/user/dto/delete-user.dto.ts +++ b/services/API-service/src/api/user/dto/delete-user.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; + export class DeleteUserDto { @ApiProperty() @IsNotEmpty() diff --git a/services/API-service/src/api/user/dto/login-user.dto.ts b/services/API-service/src/api/user/dto/login-user.dto.ts index 389bf55ce..2e0128650 100644 --- a/services/API-service/src/api/user/dto/login-user.dto.ts +++ b/services/API-service/src/api/user/dto/login-user.dto.ts @@ -1,6 +1,7 @@ -import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; + export class LoginUserDto { @ApiProperty({ example: 'dunant@redcross.nl' }) @IsEmail() diff --git a/services/API-service/src/api/user/dto/update-password.dto.ts b/services/API-service/src/api/user/dto/update-password.dto.ts index 6bed76aec..508b255c5 100644 --- a/services/API-service/src/api/user/dto/update-password.dto.ts +++ b/services/API-service/src/api/user/dto/update-password.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator'; + export class UpdatePasswordDto { @ApiProperty({ example: 'abcd' }) @IsNotEmpty() diff --git a/services/API-service/src/api/user/user.controller.ts b/services/API-service/src/api/user/user.controller.ts index aced6e8b4..afebd1343 100644 --- a/services/API-service/src/api/user/user.controller.ts +++ b/services/API-service/src/api/user/user.controller.ts @@ -1,26 +1,27 @@ import { - Post, Body, Controller, - UsePipes, HttpStatus, + Post, UseGuards, + UsePipes, } from '@nestjs/common'; -import { UserService } from './user.service'; -import { UserResponseObject } from './user.model'; -import { CreateUserDto, LoginUserDto, UpdatePasswordDto } from './dto'; import { HttpException } from '@nestjs/common/exceptions/http.exception'; -import { ValidationPipe } from '../../shared/pipes/validation.pipe'; import { - ApiTags, ApiBearerAuth, ApiOperation, ApiResponse, + ApiTags, } from '@nestjs/swagger'; + +import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; +import { ValidationPipe } from '../../shared/pipes/validation.pipe'; +import { CreateUserDto, LoginUserDto, UpdatePasswordDto } from './dto'; import { UserRole } from './user-role.enum'; -import { Roles } from '../../roles.decorator'; import { UserDecorator } from './user.decorator'; +import { UserResponseObject } from './user.model'; +import { UserService } from './user.service'; @ApiTags('-- user --') @Controller('user') @@ -86,7 +87,7 @@ export class UserController { public async update( @UserDecorator('userId') loggedInUserId: string, @Body() userData: UpdatePasswordDto, - ): Promise { + ) { return this.userService.update(loggedInUserId, userData); } } diff --git a/services/API-service/src/api/user/user.decorator.ts b/services/API-service/src/api/user/user.decorator.ts index 26da13080..0e0beadf3 100644 --- a/services/API-service/src/api/user/user.decorator.ts +++ b/services/API-service/src/api/user/user.decorator.ts @@ -1,5 +1,7 @@ -import { ExecutionContext, createParamDecorator } from '@nestjs/common'; +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + import * as jwt from 'jsonwebtoken'; + import { User } from './user.model'; export const UserDecorator = createParamDecorator( diff --git a/services/API-service/src/api/user/user.entity.ts b/services/API-service/src/api/user/user.entity.ts index 78689fb18..e7de74318 100644 --- a/services/API-service/src/api/user/user.entity.ts +++ b/services/API-service/src/api/user/user.entity.ts @@ -1,20 +1,22 @@ +import crypto from 'crypto'; + import { IsEmail } from 'class-validator'; import { - Entity, - PrimaryGeneratedColumn, - Column, BeforeInsert, - OneToMany, - ManyToMany, + Column, + Entity, JoinTable, + ManyToMany, + OneToMany, + PrimaryGeneratedColumn, } from 'typeorm'; -import crypto from 'crypto'; + import { CountryEntity } from '../country/country.entity'; +import { DisasterEntity } from '../disaster/disaster.entity'; import { EapActionStatusEntity } from '../eap-actions/eap-action-status.entity'; +import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; import { UserRole } from './user-role.enum'; import { UserStatus } from './user-status.enum'; -import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; -import { DisasterEntity } from '../disaster/disaster.entity'; @Entity('user') export class UserEntity { diff --git a/services/API-service/src/api/user/user.model.ts b/services/API-service/src/api/user/user.model.ts index a57c2b084..e0822aed5 100644 --- a/services/API-service/src/api/user/user.model.ts +++ b/services/API-service/src/api/user/user.model.ts @@ -1,6 +1,7 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { UserRole } from './user-role.enum'; import { UserStatus } from './user-status.enum'; -import { ApiProperty } from '@nestjs/swagger'; export class User { public userId: string; diff --git a/services/API-service/src/api/user/user.module.ts b/services/API-service/src/api/user/user.module.ts index f5784cd3a..8c5018ba5 100644 --- a/services/API-service/src/api/user/user.module.ts +++ b/services/API-service/src/api/user/user.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; -import { UserController } from './user.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserEntity } from './user.entity'; -import { UserService } from './user.service'; + import { CountryEntity } from '../country/country.entity'; -import { LookupModule } from '../notification/lookup/lookup.module'; import { DisasterEntity } from '../disaster/disaster.entity'; +import { LookupModule } from '../notification/lookup/lookup.module'; +import { UserController } from './user.controller'; +import { UserEntity } from './user.entity'; +import { UserService } from './user.service'; @Module({ imports: [ diff --git a/services/API-service/src/api/user/user.service.ts b/services/API-service/src/api/user/user.service.ts index 52dd269bd..3d3e6f5fd 100644 --- a/services/API-service/src/api/user/user.service.ts +++ b/services/API-service/src/api/user/user.service.ts @@ -1,18 +1,19 @@ -import { Injectable } from '@nestjs/common'; +import crypto from 'crypto'; +import { HttpStatus, Injectable } from '@nestjs/common'; +import { HttpException } from '@nestjs/common/exceptions/http.exception'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In } from 'typeorm'; -import { UserEntity } from './user.entity'; -import { CreateUserDto, LoginUserDto, UpdatePasswordDto } from './dto'; -import { UserResponseObject } from './user.model'; + import { validate } from 'class-validator'; -import { HttpException } from '@nestjs/common/exceptions/http.exception'; -import { HttpStatus } from '@nestjs/common'; -import crypto from 'crypto'; import jwt from 'jsonwebtoken'; +import { In, Repository } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; -import { UserRole } from './user-role.enum'; -import { LookupService } from '../notification/lookup/lookup.service'; import { DisasterEntity } from '../disaster/disaster.entity'; +import { LookupService } from '../notification/lookup/lookup.service'; +import { CreateUserDto, LoginUserDto, UpdatePasswordDto } from './dto'; +import { UserRole } from './user-role.enum'; +import { UserEntity } from './user.entity'; +import { UserResponseObject } from './user.model'; @Injectable() export class UserService { diff --git a/services/API-service/src/api/waterpoints/waterpoints.controller.ts b/services/API-service/src/api/waterpoints/waterpoints.controller.ts index 8220f5ec3..3764446ef 100644 --- a/services/API-service/src/api/waterpoints/waterpoints.controller.ts +++ b/services/API-service/src/api/waterpoints/waterpoints.controller.ts @@ -1,15 +1,17 @@ -import { Get, Param, Controller, UseGuards } from '@nestjs/common'; -import { AxiosResponse } from 'axios'; +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; import { - ApiTags, + ApiBearerAuth, ApiOperation, ApiParam, - ApiBearerAuth, ApiResponse, + ApiTags, } from '@nestjs/swagger'; + +import { AxiosResponse } from 'axios'; + +import { RolesGuard } from '../../roles.guard'; import { GeoJson } from '../../shared/geo.model'; import { WaterpointsService } from './waterpoints.service'; -import { RolesGuard } from '../../roles.guard'; @ApiBearerAuth() @UseGuards(RolesGuard) diff --git a/services/API-service/src/api/waterpoints/waterpoints.module.ts b/services/API-service/src/api/waterpoints/waterpoints.module.ts index e20253c41..56720635e 100644 --- a/services/API-service/src/api/waterpoints/waterpoints.module.ts +++ b/services/API-service/src/api/waterpoints/waterpoints.module.ts @@ -1,9 +1,10 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; + import { CountryModule } from '../country/country.module'; import { UserModule } from '../user/user.module'; import { WaterpointsController } from './waterpoints.controller'; import { WaterpointsService } from './waterpoints.service'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [HttpModule, UserModule, CountryModule], diff --git a/services/API-service/src/api/waterpoints/waterpoints.service.ts b/services/API-service/src/api/waterpoints/waterpoints.service.ts index c4ca8d656..ace8a43e5 100644 --- a/services/API-service/src/api/waterpoints/waterpoints.service.ts +++ b/services/API-service/src/api/waterpoints/waterpoints.service.ts @@ -1,10 +1,12 @@ +import { HttpService } from '@nestjs/axios'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + import { AxiosResponse } from 'axios'; -import { WKTStringFromGeometry } from 'wkt-io-ts'; import { isRight } from 'fp-ts/lib/Either'; -import { CountryService } from '../country/country.service'; +import { WKTStringFromGeometry } from 'wkt-io-ts'; + import { GeoJson } from '../../shared/geo.model'; -import { HttpService } from '@nestjs/axios'; +import { CountryService } from '../country/country.service'; @Injectable() export class WaterpointsService { diff --git a/services/API-service/src/app.controller.ts b/services/API-service/src/app.controller.ts index a3589bd93..10c750934 100644 --- a/services/API-service/src/app.controller.ts +++ b/services/API-service/src/app.controller.ts @@ -1,10 +1,11 @@ -import { Get, Controller, UseGuards } from '@nestjs/common'; +import { Controller, Get, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { RolesGuard } from './roles.guard'; @ApiTags('-- check API --') diff --git a/services/API-service/src/app.module.ts b/services/API-service/src/app.module.ts index e927b7518..c5ee48b02 100644 --- a/services/API-service/src/app.module.ts +++ b/services/API-service/src/app.module.ts @@ -1,26 +1,27 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { HealthModule } from './health.module'; -import { EapActionsModule } from './api/eap-actions/eap-actions.module'; -import { ScriptsModule } from './scripts/scripts.module'; +import { ScheduleModule } from '@nestjs/schedule'; + +import { AdminAreaDataModule } from './api/admin-area-data/admin-area-data.module'; +import { AdminAreaDynamicDataModule } from './api/admin-area-dynamic-data/admin-area-dynamic-data.module'; +import { AdminAreaModule } from './api/admin-area/admin-area.module'; import { CountryModule } from './api/country/country.module'; -import { WaterpointsModule } from './api/waterpoints/waterpoints.module'; +import { DisasterModule } from './api/disaster/disaster.module'; +import { EapActionsModule } from './api/eap-actions/eap-actions.module'; import { EventModule } from './api/event/event.module'; -import { MetadataModule } from './api/metadata/metadata.module'; -import { AdminAreaModule } from './api/admin-area/admin-area.module'; import { GlofasStationModule } from './api/glofas-station/glofas-station.module'; -import { AdminAreaDynamicDataModule } from './api/admin-area-dynamic-data/admin-area-dynamic-data.module'; -import { DisasterModule } from './api/disaster/disaster.module'; -import { AdminAreaDataModule } from './api/admin-area-data/admin-area-data.module'; -import { RainfallTriggersModule } from './api/rainfall-triggers/rainfall-triggers.module'; +import { LinesDataModule } from './api/lines-data/lines-data.module'; +import { MetadataModule } from './api/metadata/metadata.module'; import { NotificationModule } from './api/notification/notification.module'; -import { UserModule } from './api/user/user.module'; -import { TyphoonTrackModule } from './api/typhoon-track/typhoon-track.module'; import { WhatsappModule } from './api/notification/whatsapp/whatsapp.module'; -import { CronjobModule } from './cronjob/cronjob.module'; -import { ScheduleModule } from '@nestjs/schedule'; import { PointDataModule } from './api/point-data/point-data.module'; -import { LinesDataModule } from './api/lines-data/lines-data.module'; +import { RainfallTriggersModule } from './api/rainfall-triggers/rainfall-triggers.module'; +import { TyphoonTrackModule } from './api/typhoon-track/typhoon-track.module'; +import { UserModule } from './api/user/user.module'; +import { WaterpointsModule } from './api/waterpoints/waterpoints.module'; +import { AppController } from './app.controller'; +import { CronjobModule } from './cronjob/cronjob.module'; +import { HealthModule } from './health.module'; +import { ScriptsModule } from './scripts/scripts.module'; import { TypeOrmModule } from './typeorm.module'; @Module({ diff --git a/services/API-service/src/cronjob/cronjob.module.ts b/services/API-service/src/cronjob/cronjob.module.ts index 8f74ea67c..487b014ba 100644 --- a/services/API-service/src/cronjob/cronjob.module.ts +++ b/services/API-service/src/cronjob/cronjob.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; + import { AdminAreaDynamicDataModule } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.module'; import { CronjobService } from './cronjob.service'; diff --git a/services/API-service/src/cronjob/cronjob.service.ts b/services/API-service/src/cronjob/cronjob.service.ts index 9be1806b7..ec5d59843 100644 --- a/services/API-service/src/cronjob/cronjob.service.ts +++ b/services/API-service/src/cronjob/cronjob.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; + import { AdminAreaDynamicDataService } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.service'; @Injectable() diff --git a/services/API-service/src/main.ts b/services/API-service/src/main.ts index 17e95f170..5e6bbc7b9 100644 --- a/services/API-service/src/main.ts +++ b/services/API-service/src/main.ts @@ -1,15 +1,17 @@ -import { EXTERNAL_API, PORT } from './config'; +import { BadRequestException, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { ApplicationModule } from './app.module'; import { - SwaggerModule, DocumentBuilder, - SwaggerDocumentOptions, SwaggerCustomOptions, + SwaggerDocumentOptions, + SwaggerModule, } from '@nestjs/swagger'; -import { BadRequestException, ValidationPipe } from '@nestjs/common'; + import * as bodyParser from 'body-parser'; +import { ApplicationModule } from './app.module'; +import { EXTERNAL_API, PORT } from './config'; + async function bootstrap(): Promise { const appOptions = { cors: true }; const app = await NestFactory.create(ApplicationModule, appOptions); diff --git a/services/API-service/src/roles.decorator.ts b/services/API-service/src/roles.decorator.ts index a57ffef79..a4eb3aa1b 100644 --- a/services/API-service/src/roles.decorator.ts +++ b/services/API-service/src/roles.decorator.ts @@ -1,4 +1,5 @@ import { SetMetadata } from '@nestjs/common'; + import { UserRole } from './api/user/user-role.enum'; -export const Roles = (...roles: UserRole[]): any => SetMetadata('roles', roles); +export const Roles = (...roles: UserRole[]) => SetMetadata('roles', roles); diff --git a/services/API-service/src/roles.guard.ts b/services/API-service/src/roles.guard.ts index cd6d426e4..2be104149 100644 --- a/services/API-service/src/roles.guard.ts +++ b/services/API-service/src/roles.guard.ts @@ -1,9 +1,11 @@ -import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; -import * as jwt from 'jsonwebtoken'; -import { UserService } from './api/user/user.service'; -import { User } from './api/user/user.model'; +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; + +import * as jwt from 'jsonwebtoken'; + import { UserRole } from './api/user/user-role.enum'; +import { User } from './api/user/user.model'; +import { UserService } from './api/user/user.service'; @Injectable() export class RolesGuard implements CanActivate { diff --git a/services/API-service/src/scripts.ts b/services/API-service/src/scripts.ts index 8bf6fed4b..514255fda 100644 --- a/services/API-service/src/scripts.ts +++ b/services/API-service/src/scripts.ts @@ -1,5 +1,7 @@ import { NestFactory } from '@nestjs/core'; -import { ScriptsModule, InterfaceScript } from './scripts/scripts.module'; + +import { InterfaceScript, ScriptsModule } from './scripts/scripts.module'; + import yargs = require('yargs'); async function main(): Promise { diff --git a/services/API-service/src/scripts/enum/mock-scenario.enum.ts b/services/API-service/src/scripts/enum/mock-scenario.enum.ts index f849dec98..db494e37e 100644 --- a/services/API-service/src/scripts/enum/mock-scenario.enum.ts +++ b/services/API-service/src/scripts/enum/mock-scenario.enum.ts @@ -14,3 +14,12 @@ export enum EpidemicsScenario { Default = 'default', NoTrigger = 'no-trigger', } + +export enum TyphoonScenario { + NoEvent = 'noEvent', + EventNoLandfall = 'eventNoLandfall', + EventNoLandfallYet = 'eventNoLandfallYet', + EventNoTrigger = 'eventNoTrigger', + EventTrigger = 'eventTrigger', + EventAfterLandfall = 'eventAfterLandfall', +} diff --git a/services/API-service/src/scripts/geoserver-sync.service.ts b/services/API-service/src/scripts/geoserver-sync.service.ts index ae32d844e..465aba245 100644 --- a/services/API-service/src/scripts/geoserver-sync.service.ts +++ b/services/API-service/src/scripts/geoserver-sync.service.ts @@ -1,15 +1,17 @@ +import fs from 'fs'; import { HttpService } from '@nestjs/axios'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + import { firstValueFrom } from 'rxjs'; -import { INTERNAL_GEOSERVER_API_URL } from '../config'; -import countries from './json/countries.json'; + import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { INTERNAL_GEOSERVER_API_URL } from '../config'; import { DisasterTypeGeoServerMapper } from './disaster-type-geoserver-file.mapper'; -import fs from 'fs'; +import countries from './json/countries.json'; const workspaceName = 'ibf-system'; -class RecourceNameObject { +class ResourceNameObject { resourceName: string; disasterType: DisasterType; countryCodeISO3: string; @@ -24,7 +26,7 @@ export class GeoserverSyncService { disasterType?: DisasterType, ): Promise { const countriesCopy = JSON.parse(JSON.stringify(countries)); - const filteredCountries = countriesCopy.filter((country: any) => { + const filteredCountries = countriesCopy.filter((country) => { return countryCodeISO3 ? country.countryCodeISO3 === countryCodeISO3 : true; @@ -32,7 +34,7 @@ export class GeoserverSyncService { // also filter by disaster type for (const country of filteredCountries) { const disasterSettings = country.countryDisasterSettings.filter( - (disasterSetting: any) => { + (disasterSetting) => { return disasterType ? disasterSetting.disasterType === disasterType : true; @@ -47,7 +49,7 @@ export class GeoserverSyncService { await this.syncLayers(geoserverResourceNameObjects); } - private async syncStores(expectedStoreNameObjects: RecourceNameObject[]) { + private async syncStores(expectedStoreNameObjects: ResourceNameObject[]) { const foundStoreNames = await this.getStoreNamesFromGeoserver( workspaceName, ); @@ -58,8 +60,9 @@ export class GeoserverSyncService { } private generateGeoserverResourceNames( + // eslint-disable-next-line @typescript-eslint/no-explicit-any filteredCountries: any[], - ): RecourceNameObject[] { + ): ResourceNameObject[] { const resourceNameObjects = []; for (const country of filteredCountries) { resourceNameObjects.push(...this.generateStoreNameForCountry(country)); @@ -67,7 +70,7 @@ export class GeoserverSyncService { return resourceNameObjects; } - private generateStoreNameForCountry(country: any): RecourceNameObject[] { + private generateStoreNameForCountry(country): ResourceNameObject[] { const resourceNameObjects = []; const countryCode = country.countryCodeISO3; for (const disasterSetting of country.countryDisasterSettings) { @@ -92,13 +95,13 @@ export class GeoserverSyncService { private async getStoreNamesFromGeoserver(workspaceName: string) { const data = await this.get(`workspaces/${workspaceName}/coveragestores`); const storeNames = data.coverageStores.coverageStore.map( - (store: any) => store.name, + (store) => store.name, ); return storeNames; } private async postStoreNamesToGeoserver( - resourceNameObjects: RecourceNameObject[], + resourceNameObjects: ResourceNameObject[], ) { for (const resourceNameObject of resourceNameObjects) { const subfolder = DisasterTypeGeoServerMapper.getSubfolderForDisasterType( @@ -135,7 +138,7 @@ export class GeoserverSyncService { } } - public async syncLayers(expectedLayerNames: RecourceNameObject[]) { + public async syncLayers(expectedLayerNames: ResourceNameObject[]) { const foundLayerNames = await this.getLayerNamesFromGeoserver( workspaceName, ); @@ -147,12 +150,12 @@ export class GeoserverSyncService { private async getLayerNamesFromGeoserver(workspaceName: string) { const data = await this.get(`workspaces/${workspaceName}/layers`); - const layerNames = data.layers.layer.map((layer: any) => layer.name); + const layerNames = data.layers.layer.map((layer) => layer.name); return layerNames; } private async postLayerNamesToGeoserver( - resourceNameObjects: RecourceNameObject[], + resourceNameObjects: ResourceNameObject[], ) { for (const resourceNameObject of resourceNameObjects) { const publishLayerUrl = `workspaces/${workspaceName}/coveragestores/${resourceNameObject.resourceName}/coverages`; @@ -182,7 +185,7 @@ export class GeoserverSyncService { } } - private async post(path: string, body: any) { + private async post(path: string, body: unknown) { const url = `${INTERNAL_GEOSERVER_API_URL}/${path}`; const headers = this.getHeaders(); const result = await firstValueFrom( @@ -191,7 +194,7 @@ export class GeoserverSyncService { return result.data; } - private async put(path: string, body: any) { + private async put(path: string, body: unknown) { const url = `${INTERNAL_GEOSERVER_API_URL}/${path}`; const headers = this.getHeaders(); const result = await firstValueFrom( diff --git a/services/API-service/src/scripts/mock-helper.service.ts b/services/API-service/src/scripts/mock-helper.service.ts index a51a174f5..b75b305ea 100644 --- a/services/API-service/src/scripts/mock-helper.service.ts +++ b/services/API-service/src/scripts/mock-helper.service.ts @@ -1,16 +1,17 @@ +import fs from 'fs'; import { Injectable } from '@nestjs/common'; + +import { AdminAreaDynamicDataService } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.service'; import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { EventService } from '../api/event/event.service'; import { UploadLinesExposureStatusDto } from '../api/lines-data/dto/upload-asset-exposure-status.dto'; import { LinesDataEnum } from '../api/lines-data/lines-data.entity'; +import { LinesDataService } from '../api/lines-data/lines-data.service'; import { UploadDynamicPointDataDto } from '../api/point-data/dto/upload-asset-exposure-status.dto'; import { PointDataEnum } from '../api/point-data/point-data.entity'; -import { DisasterTypeGeoServerMapper } from './disaster-type-geoserver-file.mapper'; -import { AdminAreaDynamicDataService } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.service'; -import { EventService } from '../api/event/event.service'; -import { LinesDataService } from '../api/lines-data/lines-data.service'; import { PointDataService } from '../api/point-data/point-data.service'; -import fs from 'fs'; +import { DisasterTypeGeoServerMapper } from './disaster-type-geoserver-file.mapper'; @Injectable() export class MockHelperService { diff --git a/services/API-service/src/scripts/mock.controller.ts b/services/API-service/src/scripts/mock.controller.ts index 090ec78ed..d36611af8 100644 --- a/services/API-service/src/scripts/mock.controller.ts +++ b/services/API-service/src/scripts/mock.controller.ts @@ -1,9 +1,11 @@ import { + Body, Controller, + HttpStatus, + ParseBoolPipe, Post, - Body, + Query, Res, - HttpStatus, UseGuards, } from '@nestjs/common'; import { @@ -13,17 +15,19 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; -import { RolesGuard } from '../roles.guard'; + import { DisasterType } from '../api/disaster/disaster-type.enum'; -import { Roles } from '../roles.decorator'; import { UserRole } from '../api/user/user-role.enum'; -import { MockService } from './mock.service'; +import { Roles } from '../roles.decorator'; +import { RolesGuard } from '../roles.guard'; import { - FloodsScenario, - FlashFloodsScenario, EpidemicsScenario, + FlashFloodsScenario, + FloodsScenario, } from './enum/mock-scenario.enum'; +import { MockService } from './mock.service'; export class MockBaseScenario { @ApiProperty({ example: 'fill_in_secret' }) @@ -102,6 +106,13 @@ export class MockController { public async mockFloodsScenario( @Body() body: MockFloodsScenario, @Res() res, + @Query( + 'isApiTest', + new ParseBoolPipe({ + optional: true, + }), + ) + isApiTest: boolean, ): Promise { if (body.secret !== process.env.RESET_SECRET) { return res.status(HttpStatus.FORBIDDEN).send('Not allowed'); @@ -110,6 +121,7 @@ export class MockController { body, DisasterType.Floods, false, + isApiTest, ); return res.status(HttpStatus.ACCEPTED).send(result); @@ -127,6 +139,13 @@ export class MockController { public async mockFlashFloodsScenario( @Body() body: MockFlashFloodsScenario, @Res() res, + @Query( + 'isApiTest', + new ParseBoolPipe({ + optional: true, + }), + ) + isApiTest: boolean, ): Promise { if (body.secret !== process.env.RESET_SECRET) { return res.status(HttpStatus.FORBIDDEN).send('Not allowed'); @@ -135,6 +154,7 @@ export class MockController { body, DisasterType.FlashFloods, false, + isApiTest, ); return res.status(HttpStatus.ACCEPTED).send(result); @@ -152,6 +172,13 @@ export class MockController { public async mockEpidemicsScenario( @Body() body: MockEpidemicsScenario, @Res() res, + @Query( + 'isApiTest', + new ParseBoolPipe({ + optional: true, + }), + ) + isApiTest: boolean, ): Promise { if (body.secret !== process.env.RESET_SECRET) { return res.status(HttpStatus.FORBIDDEN).send('Not allowed'); @@ -161,7 +188,12 @@ export class MockController { body.countryCodeISO3 === 'PHL' ? DisasterType.Dengue : DisasterType.Malaria; - const result = await this.mockService.mock(body, disasterType, false); + const result = await this.mockService.mock( + body, + disasterType, + false, + isApiTest, + ); return res.status(HttpStatus.ACCEPTED).send(result); } diff --git a/services/API-service/src/scripts/mock.service.ts b/services/API-service/src/scripts/mock.service.ts index 4626a6555..559bd17e7 100644 --- a/services/API-service/src/scripts/mock.service.ts +++ b/services/API-service/src/scripts/mock.service.ts @@ -1,29 +1,31 @@ -import { Injectable } from '@nestjs/common'; -import { DisasterType } from '../api/disaster/disaster-type.enum'; import fs from 'fs'; -import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; -import { MetadataService } from '../api/metadata/metadata.service'; -import { DynamicIndicator } from '../api/admin-area-dynamic-data/enum/dynamic-data-unit'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { In, Repository } from 'typeorm'; + +import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { AdminAreaDynamicDataService } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.service'; +import { DynamicIndicator } from '../api/admin-area-dynamic-data/enum/dynamic-data-unit'; +import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; +import { AdminAreaService } from '../api/admin-area/admin-area.service'; import { AdminLevel } from '../api/country/admin-level.enum'; +import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { EapActionStatusEntity } from '../api/eap-actions/eap-action-status.entity'; +import { EventPlaceCodeEntity } from '../api/event/event-place-code.entity'; import { EventService } from '../api/event/event.service'; -import countries from './json/countries.json'; +import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; import { GlofasStationService } from '../api/glofas-station/glofas-station.service'; +import { MetadataService } from '../api/metadata/metadata.service'; +import { DEBUG } from '../config'; +import { GeoserverSyncService } from './geoserver-sync.service'; +import countries from './json/countries.json'; +import { MockHelperService } from './mock-helper.service'; import { MockEpidemicsScenario, MockFlashFloodsScenario, MockFloodsScenario, } from './mock.controller'; -import { In, Repository } from 'typeorm'; -import { EventPlaceCodeEntity } from '../api/event/event-place-code.entity'; -import { InjectRepository } from '@nestjs/typeorm'; -import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; -import { EapActionStatusEntity } from '../api/eap-actions/eap-action-status.entity'; -import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; -import { AdminAreaService } from '../api/admin-area/admin-area.service'; -import { MockHelperService } from './mock-helper.service'; -import { DEBUG } from '../config'; -import { GeoserverSyncService } from './geoserver-sync.service'; class Scenario { scenarioName: string; @@ -63,12 +65,13 @@ export class MockService { | MockFlashFloodsScenario, disasterType: DisasterType, useDefaultScenario: boolean, + isApiTest: boolean, ) { if (mockBody.removeEvents) { await this.removeEvents(mockBody.countryCodeISO3, disasterType); } - const selectedCountry = countries.find((country): any => { + const selectedCountry = countries.find((country) => { if (mockBody.countryCodeISO3 === country.countryCodeISO3) { return country; } @@ -234,7 +237,7 @@ export class MockService { // Add the needed stores and layers to geoserver, only do this in debug mode // The resulting XML files should be commited to git and will end up on the servers that way - if (DEBUG) { + if (DEBUG && !isApiTest) { await this.geoServerSyncService.sync( selectedCountry.countryCodeISO3, disasterType, @@ -244,6 +247,7 @@ export class MockService { private getLeadTimesForNoTrigger( disasterType: DisasterType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry: any, ): LeadTime[] { // NOTE: this reflects agreements with pipelines that are in place. This is ugly, and should be refactored better. diff --git a/services/API-service/src/scripts/scripts.controller.ts b/services/API-service/src/scripts/scripts.controller.ts index d5b6f694c..18425f628 100644 --- a/services/API-service/src/scripts/scripts.controller.ts +++ b/services/API-service/src/scripts/scripts.controller.ts @@ -1,9 +1,9 @@ import { + Body, Controller, + HttpStatus, Post, - Body, Res, - HttpStatus, UseGuards, } from '@nestjs/common'; import { @@ -13,6 +13,7 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { IsEnum, IsIn, @@ -20,12 +21,14 @@ import { IsOptional, IsString, } from 'class-validator'; -import { SeedInit } from './seed-init'; -import { ScriptsService } from './scripts.service'; -import { RolesGuard } from '../roles.guard'; + import { DisasterType } from '../api/disaster/disaster-type.enum'; -import { Roles } from '../roles.decorator'; import { UserRole } from '../api/user/user-role.enum'; +import { Roles } from '../roles.decorator'; +import { RolesGuard } from '../roles.guard'; +import { TyphoonScenario } from './enum/mock-scenario.enum'; +import { ScriptsService } from './scripts.service'; +import { SeedInit } from './seed-init'; class ResetDto { @ApiProperty({ example: 'fill_in_secret' }) @@ -82,15 +85,6 @@ export class MockAll { public readonly date: Date; } -export enum TyphoonScenario { - NoEvent = 'noEvent', - EventNoLandfall = 'eventNoLandfall', - EventNoLandfallYet = 'eventNoLandfallYet', - EventNoTrigger = 'eventNoTrigger', - EventTrigger = 'eventTrigger', - EventAfterLandfall = 'eventAfterLandfall', -} - export class MockTyphoonScenario { @ApiProperty({ example: 'fill_in_secret' }) @IsNotEmpty() diff --git a/services/API-service/src/scripts/scripts.module.ts b/services/API-service/src/scripts/scripts.module.ts index e1c38e464..e28a4b26f 100644 --- a/services/API-service/src/scripts/scripts.module.ts +++ b/services/API-service/src/scripts/scripts.module.ts @@ -1,39 +1,41 @@ -import { EapActionStatusEntity } from './../api/eap-actions/eap-action-status.entity'; -import { EventPlaceCodeEntity } from './../api/event/event-place-code.entity'; -import { AdminAreaDynamicDataModule } from './../api/admin-area-dynamic-data/admin-area-dynamic-data.module'; +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; + import { Arguments } from 'yargs'; -import { ScriptsController } from './scripts.controller'; -import { SeedInit } from './seed-init'; -import { GlofasStationModule } from '../api/glofas-station/glofas-station.module'; -import { ScriptsService } from './scripts.service'; -import { EventModule } from '../api/event/event.module'; -import { UserModule } from '../api/user/user.module'; + +import { ORMConfig } from '../../ormconfig'; +import { AdminAreaDataModule } from '../api/admin-area-data/admin-area-data.module'; +import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { AdminAreaEntity } from '../api/admin-area/admin-area.entity'; -import { LeadTimeEntity } from '../api/lead-time/lead-time.entity'; -import { CountryEntity } from '../api/country/country.entity'; -import { TyphoonTrackModule } from '../api/typhoon-track/typhoon-track.module'; -import SeedProd from './seed-prod'; -import { MetadataModule } from '../api/metadata/metadata.module'; -import SeedAdminArea from './seed-admin-area'; import { AdminAreaModule } from '../api/admin-area/admin-area.module'; +import { CountryEntity } from '../api/country/country.entity'; import { CountryModule } from '../api/country/country.module'; -import SeedAdminAreaData from './seed-admin-area-data'; -import SeedPointData from './seed-point-data'; -import SeedRainfallData from './seed-rainfall-data'; -import { PointDataModule } from '../api/point-data/point-data.module'; -import { AdminAreaDataModule } from '../api/admin-area-data/admin-area-data.module'; +import { EventModule } from '../api/event/event.module'; import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; -import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; +import { GlofasStationModule } from '../api/glofas-station/glofas-station.module'; +import { LeadTimeEntity } from '../api/lead-time/lead-time.entity'; import { LinesDataModule } from '../api/lines-data/lines-data.module'; -import SeedLineData from './seed-line-data'; -import { ORMConfig } from '../../ormconfig'; -import { MockService } from './mock.service'; -import { MockController } from './mock.controller'; +import { MetadataModule } from '../api/metadata/metadata.module'; +import { PointDataModule } from '../api/point-data/point-data.module'; +import { TyphoonTrackModule } from '../api/typhoon-track/typhoon-track.module'; +import { UserModule } from '../api/user/user.module'; +import { AdminAreaDynamicDataModule } from './../api/admin-area-dynamic-data/admin-area-dynamic-data.module'; +import { EapActionStatusEntity } from './../api/eap-actions/eap-action-status.entity'; +import { EventPlaceCodeEntity } from './../api/event/event-place-code.entity'; import { GeoserverSyncService } from './geoserver-sync.service'; -import { HttpModule } from '@nestjs/axios'; import { MockHelperService } from './mock-helper.service'; +import { MockController } from './mock.controller'; +import { MockService } from './mock.service'; +import { ScriptsController } from './scripts.controller'; +import { ScriptsService } from './scripts.service'; +import SeedAdminArea from './seed-admin-area'; +import SeedAdminAreaData from './seed-admin-area-data'; +import { SeedInit } from './seed-init'; +import SeedLineData from './seed-line-data'; +import SeedPointData from './seed-point-data'; +import SeedProd from './seed-prod'; +import SeedRainfallData from './seed-rainfall-data'; @Module({ imports: [ diff --git a/services/API-service/src/scripts/scripts.service.ts b/services/API-service/src/scripts/scripts.service.ts index 38c6547d8..893a30482 100644 --- a/services/API-service/src/scripts/scripts.service.ts +++ b/services/API-service/src/scripts/scripts.service.ts @@ -1,31 +1,33 @@ +import fs from 'fs'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { In, Repository } from 'typeorm'; + +import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { AdminAreaDynamicDataService } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.service'; +import { DynamicIndicator } from '../api/admin-area-dynamic-data/enum/dynamic-data-unit'; +import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; +import { AdminAreaEntity } from '../api/admin-area/admin-area.entity'; +import { AdminLevel } from '../api/country/admin-level.enum'; +import { CountryEntity } from '../api/country/country.entity'; import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { EapActionStatusEntity } from '../api/eap-actions/eap-action-status.entity'; +import { EventPlaceCodeEntity } from '../api/event/event-place-code.entity'; +import { EventService } from '../api/event/event.service'; +import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; import { GlofasStationService } from '../api/glofas-station/glofas-station.service'; +import { MetadataService } from '../api/metadata/metadata.service'; +import { TyphoonTrackService } from '../api/typhoon-track/typhoon-track.service'; +import { TyphoonScenario } from './enum/mock-scenario.enum'; +import countries from './json/countries.json'; +import { MockHelperService } from './mock-helper.service'; +import { MockService } from './mock.service'; import { MockAll, MockDynamic, MockTyphoonScenario, - TyphoonScenario, } from './scripts.controller'; -import countries from './json/countries.json'; -import fs from 'fs'; -import { DynamicIndicator } from '../api/admin-area-dynamic-data/enum/dynamic-data-unit'; -import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; -import { EventService } from '../api/event/event.service'; -import { InjectRepository } from '@nestjs/typeorm'; -import { EventPlaceCodeEntity } from '../api/event/event-place-code.entity'; -import { In, Repository } from 'typeorm'; -import { EapActionStatusEntity } from '../api/eap-actions/eap-action-status.entity'; -import { CountryEntity } from '../api/country/country.entity'; -import { TyphoonTrackService } from '../api/typhoon-track/typhoon-track.service'; -import { MetadataService } from '../api/metadata/metadata.service'; -import { AdminLevel } from '../api/country/admin-level.enum'; -import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; -import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; -import { AdminAreaEntity } from '../api/admin-area/admin-area.entity'; -import { MockHelperService } from './mock-helper.service'; -import { MockService } from './mock.service'; @Injectable() export class ScriptsService { @@ -56,6 +58,8 @@ export class ScriptsService { ) {} public async mockAll(mockAllInput: MockAll) { + const isApiTest = false; + const envCountries = process.env.COUNTRIES.split(','); const newMockServiceDisasterTypes = [ @@ -83,6 +87,7 @@ export class ScriptsService { }, disasterType.disasterType, true, + isApiTest, ); } else { await this.mockCountry({ @@ -155,7 +160,7 @@ export class ScriptsService { await this.eventPlaceCodeRepo.remove(allCountryEvents); } - const selectedCountry = countries.find((country): any => { + const selectedCountry = countries.find((country) => { if (mockInput.countryCodeISO3 === country.countryCodeISO3) { return country; } @@ -407,6 +412,7 @@ export class ScriptsService { } private getLeadTimes( + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry: any, disasterType: DisasterType, eventNr: number, @@ -456,6 +462,7 @@ export class ScriptsService { typhoonScenario?: TyphoonScenario, eventRegion?: string, leadTime?: LeadTime, + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry?: any, date?: Date, triggered?: boolean, @@ -494,6 +501,7 @@ export class ScriptsService { } private filterLeadTimesPerDisasterType( + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry: any, leadTime: string, disasterType: DisasterType, @@ -552,6 +560,7 @@ export class ScriptsService { } private getDroughtLeadTime( + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry: any, leadTime: string, disasterType: DisasterType, @@ -582,6 +591,7 @@ export class ScriptsService { forecastSeasons, leadTime: string, date: Date, + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry: any, ) { const { currentYear, currentUTCMonth, leadTimeMonthFirstDay } = @@ -650,7 +660,8 @@ export class ScriptsService { } private async mockAmount( - exposurePlacecodes: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + exposurePlaceCodes: any, exposureUnit: DynamicIndicator, triggered: boolean, disasterType: DisasterType, @@ -658,8 +669,8 @@ export class ScriptsService { activeLeadTime: LeadTime, date: Date, eventRegion?: string, - ): Promise { - let copyOfExposureUnit = JSON.parse(JSON.stringify(exposurePlacecodes)); + ) { + let copyOfExposureUnit = JSON.parse(JSON.stringify(exposurePlaceCodes)); if ( disasterType === DisasterType.Drought && selectedCountry.countryCodeISO3 !== 'ZWE' && // exclude ZWE drought from this rule @@ -718,9 +729,7 @@ export class ScriptsService { const month = leadTimeMonthFirstDay.getMonth() + 1; const triggeredAreas = droughtRegionAreas[droughtRegion].map( - (placeCode) => { - return { placeCode: placeCode, triggered: false }; - }, + (placeCode) => ({ placeCode, triggered: false }), ); for (const season of Object.values(forecastSeasonAreas[droughtRegion])) { const filteredSeason = season[this.rainMonthsKey].filter( diff --git a/services/API-service/src/scripts/seed-admin-area-data.ts b/services/API-service/src/scripts/seed-admin-area-data.ts index e6f063450..d10f90f41 100644 --- a/services/API-service/src/scripts/seed-admin-area-data.ts +++ b/services/API-service/src/scripts/seed-admin-area-data.ts @@ -1,9 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; -import { DataSource } from 'typeorm'; -import { SeedHelper } from './seed-helper'; + import { AdminLevel } from 'src/api/country/admin-level.enum'; +import { DataSource } from 'typeorm'; + import { AdminAreaDataService } from '../api/admin-area-data/admin-area-data.service'; +import { InterfaceScript } from './scripts.module'; +import { SeedHelper } from './seed-helper'; interface AdminAreaDataRecord { placeCode: string; diff --git a/services/API-service/src/scripts/seed-admin-area.ts b/services/API-service/src/scripts/seed-admin-area.ts index 3ace5a711..2248f1d8f 100644 --- a/services/API-service/src/scripts/seed-admin-area.ts +++ b/services/API-service/src/scripts/seed-admin-area.ts @@ -1,9 +1,10 @@ -import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; -import countries from './json/countries.json'; import fs from 'fs'; +import { Injectable } from '@nestjs/common'; + import { AdminAreaService } from '../api/admin-area/admin-area.service'; import { EventAreaService } from '../api/admin-area/services/event-area.service'; +import countries from './json/countries.json'; +import { InterfaceScript } from './scripts.module'; @Injectable() export class SeedAdminArea implements InterfaceScript { diff --git a/services/API-service/src/scripts/seed-helper.ts b/services/API-service/src/scripts/seed-helper.ts index 8d4094b31..c6aaf0c8f 100644 --- a/services/API-service/src/scripts/seed-helper.ts +++ b/services/API-service/src/scripts/seed-helper.ts @@ -1,6 +1,7 @@ import fs from 'fs'; -import csv from 'csv-parser'; import { Readable } from 'stream'; + +import csv from 'csv-parser'; import { DataSource } from 'typeorm'; export class SeedHelper { diff --git a/services/API-service/src/scripts/seed-init.ts b/services/API-service/src/scripts/seed-init.ts index 35ce840b6..fd7cdfbb3 100644 --- a/services/API-service/src/scripts/seed-init.ts +++ b/services/API-service/src/scripts/seed-init.ts @@ -1,41 +1,41 @@ import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; + import { DataSource } from 'typeorm'; + +import { + LeadTime, + LeadTimeUnit, +} from '../api/admin-area-dynamic-data/enum/lead-time.enum'; import { CountryEntity } from '../api/country/country.entity'; +import { CountryService } from '../api/country/country.service'; +import { NotificationInfoDto } from '../api/country/dto/notification-info.dto'; +import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { DisasterEntity } from '../api/disaster/disaster.entity'; import { AreaOfFocusEntity } from '../api/eap-actions/area-of-focus.entity'; import { EapActionEntity } from '../api/eap-actions/eap-action.entity'; -import { IndicatorMetadataEntity } from '../api/metadata/indicator-metadata.entity'; import { LeadTimeEntity } from '../api/lead-time/lead-time.entity'; +import { IndicatorMetadataEntity } from '../api/metadata/indicator-metadata.entity'; +import { LayerMetadataEntity } from '../api/metadata/layer-metadata.entity'; +import { NotificationInfoEntity } from '../api/notification/notifcation-info.entity'; import { UserRole } from '../api/user/user-role.enum'; import { UserStatus } from '../api/user/user-status.enum'; import { UserEntity } from '../api/user/user.entity'; -import { LayerMetadataEntity } from '../api/metadata/layer-metadata.entity'; -import { DisasterType } from '../api/disaster/disaster-type.enum'; -import { DisasterEntity } from '../api/disaster/disaster.entity'; -import { NotificationInfoEntity } from '../api/notification/notifcation-info.entity'; - -import leadTimes from './json/lead-times.json'; -import notificationInfo from './json/notification-info.json'; -import countries from './json/countries.json'; -import users from './json/users.json'; import areasOfFocus from './json/areas-of-focus.json'; +import countries from './json/countries.json'; +import disasters from './json/disasters.json'; import eapActions from './json/EAP-actions.json'; import indicatorMetadata from './json/indicator-metadata.json'; import layerMetadata from './json/layer-metadata.json'; -import disasters from './json/disasters.json'; - +import leadTimes from './json/lead-times.json'; +import notificationInfo from './json/notification-info.json'; +import users from './json/users.json'; +import { InterfaceScript } from './scripts.module'; import SeedAdminArea from './seed-admin-area'; -import { SeedHelper } from './seed-helper'; import SeedAdminAreaData from './seed-admin-area-data'; -import SeedRainfallData from './seed-rainfall-data'; -import SeedPointData from './seed-point-data'; -import { CountryService } from '../api/country/country.service'; -import { NotificationInfoDto } from '../api/country/dto/notification-info.dto'; +import { SeedHelper } from './seed-helper'; import SeedLineData from './seed-line-data'; -import { - LeadTime, - LeadTimeUnit, -} from '../api/admin-area-dynamic-data/enum/lead-time.enum'; +import SeedPointData from './seed-point-data'; +import SeedRainfallData from './seed-rainfall-data'; @Injectable() export class SeedInit implements InterfaceScript { diff --git a/services/API-service/src/scripts/seed-line-data.ts b/services/API-service/src/scripts/seed-line-data.ts index 0d5776a46..87c6931f0 100644 --- a/services/API-service/src/scripts/seed-line-data.ts +++ b/services/API-service/src/scripts/seed-line-data.ts @@ -1,10 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; + import { DataSource } from 'typeorm'; -import { SeedHelper } from './seed-helper'; -import countries from './json/countries.json'; -import { LinesDataService } from '../api/lines-data/lines-data.service'; + import { LinesDataEnum } from '../api/lines-data/lines-data.entity'; +import { LinesDataService } from '../api/lines-data/lines-data.service'; +import countries from './json/countries.json'; +import { InterfaceScript } from './scripts.module'; +import { SeedHelper } from './seed-helper'; @Injectable() export class SeedLineData implements InterfaceScript { diff --git a/services/API-service/src/scripts/seed-point-data.ts b/services/API-service/src/scripts/seed-point-data.ts index c5e462c41..61d85688c 100644 --- a/services/API-service/src/scripts/seed-point-data.ts +++ b/services/API-service/src/scripts/seed-point-data.ts @@ -1,10 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; + import { DataSource } from 'typeorm'; -import { SeedHelper } from './seed-helper'; -import countries from './json/countries.json'; + import { PointDataEnum } from '../api/point-data/point-data.entity'; import { PointDataService } from '../api/point-data/point-data.service'; +import countries from './json/countries.json'; +import { InterfaceScript } from './scripts.module'; +import { SeedHelper } from './seed-helper'; @Injectable() export class SeedPointData implements InterfaceScript { diff --git a/services/API-service/src/scripts/seed-prod.ts b/services/API-service/src/scripts/seed-prod.ts index 34b6a9057..f4a598d38 100644 --- a/services/API-service/src/scripts/seed-prod.ts +++ b/services/API-service/src/scripts/seed-prod.ts @@ -1,10 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; + import { DataSource } from 'typeorm'; -import { UserEntity } from '../api/user/user.entity'; -import users from './json/users.json'; + import { UserRole } from '../api/user/user-role.enum'; import { UserStatus } from '../api/user/user-status.enum'; +import { UserEntity } from '../api/user/user.entity'; +import users from './json/users.json'; +import { InterfaceScript } from './scripts.module'; @Injectable() export class SeedProd implements InterfaceScript { diff --git a/services/API-service/src/scripts/seed-rainfall-data.ts b/services/API-service/src/scripts/seed-rainfall-data.ts index a08d48d1a..f46a78da6 100644 --- a/services/API-service/src/scripts/seed-rainfall-data.ts +++ b/services/API-service/src/scripts/seed-rainfall-data.ts @@ -1,10 +1,12 @@ -import { DisasterType } from './../api/disaster/disaster-type.enum'; import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; + import { DataSource } from 'typeorm'; -import { SeedHelper } from './seed-helper'; + import { RainfallTriggersEntity } from '../api/rainfall-triggers/rainfall-triggers.entity'; +import { DisasterType } from './../api/disaster/disaster-type.enum'; import countries from './json/countries.json'; +import { InterfaceScript } from './scripts.module'; +import { SeedHelper } from './seed-helper'; @Injectable() export class SeedRainfallData implements InterfaceScript { diff --git a/services/API-service/src/shared/data.model.ts b/services/API-service/src/shared/data.model.ts index 8982723e1..4aa3ffc45 100644 --- a/services/API-service/src/shared/data.model.ts +++ b/services/API-service/src/shared/data.model.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; import { Geometry } from './geo.model'; @@ -55,17 +56,27 @@ export class TriggeredArea { public displayName: string; } +export class EapAlertClass { + key: EapAlertClassKeyEnum; + label: string; + color: string; + value: number; + textColor?: string; +} + +export enum EapAlertClassKeyEnum { + min = 'min', + med = 'med', + max = 'max', + no = 'no', +} + export class DisasterSpecificProperties { typhoonLandfall?: boolean; typhoonNoLandfallYet?: boolean; - eapAlertClass?: { - key: string; - label: string; - color: string; - value: number; - textColor?: string; - }; + eapAlertClass?: EapAlertClass; } + export class EventSummaryCountry { @ApiProperty({ example: 'UGA' }) public countryCodeISO3: string; @@ -94,6 +105,9 @@ export class EventSummaryCountry { @ApiProperty({ example: {} }) public disasterSpecificProperties: DisasterSpecificProperties; + @ApiProperty({ example: 100 }) + public triggerValue: number; + @ApiProperty({ example: 5 }) public affectedAreas: number; } diff --git a/services/API-service/src/shared/helper.service.ts b/services/API-service/src/shared/helper.service.ts index ea607c9df..a10e2c406 100644 --- a/services/API-service/src/shared/helper.service.ts +++ b/services/API-service/src/shared/helper.service.ts @@ -1,15 +1,17 @@ import { Injectable } from '@nestjs/common'; -import { Readable } from 'typeorm/platform/PlatformTools'; -import { DisasterType } from '../api/disaster/disaster-type.enum'; -import { GeoJson, GeoJsonFeature } from './geo.model'; + import csv from 'csv-parser'; -import { DateDto } from '../api/event/dto/date.dto'; import { DataSource } from 'typeorm'; -import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; +import { Readable } from 'typeorm/platform/PlatformTools'; + import { LeadTime, LeadTimeUnit, } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; +import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { DateDto } from '../api/event/dto/date.dto'; +import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; +import { GeoJson, GeoJsonFeature } from './geo.model'; @Injectable() export class HelperService { diff --git a/services/API-service/src/shared/pipes/validation.pipe.ts b/services/API-service/src/shared/pipes/validation.pipe.ts index eacc9a7bf..6420e979a 100644 --- a/services/API-service/src/shared/pipes/validation.pipe.ts +++ b/services/API-service/src/shared/pipes/validation.pipe.ts @@ -1,14 +1,15 @@ import { - PipeTransform, ArgumentMetadata, BadRequestException, HttpStatus, Injectable, + PipeTransform, } from '@nestjs/common'; -import { validate } from 'class-validator'; -import { plainToClass } from 'class-transformer'; import { HttpException } from '@nestjs/common/exceptions/http.exception'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; + @Injectable() export class ValidationPipe implements PipeTransform { public async transform( diff --git a/services/API-service/src/typeorm.module.ts b/services/API-service/src/typeorm.module.ts index c92e0ba7d..451d21c9f 100644 --- a/services/API-service/src/typeorm.module.ts +++ b/services/API-service/src/typeorm.module.ts @@ -1,5 +1,7 @@ import { Global, Module } from '@nestjs/common'; + import { DataSource } from 'typeorm'; + import { AppDataSource } from '../appdatasource'; @Global() diff --git a/services/API-service/test/email/drought/email-uga-drought.test.ts b/services/API-service/test/email/drought/email-uga-drought.test.ts new file mode 100644 index 000000000..ad781c13b --- /dev/null +++ b/services/API-service/test/email/drought/email-uga-drought.test.ts @@ -0,0 +1,102 @@ +import { JSDOM } from 'jsdom'; + +import { DisasterType } from '../../../src/api/disaster/disaster-type.enum'; +import { + getAccessToken, + mockDynamicData, + resetDB, + sendNotification, +} from '../../helpers/utility.helper'; + +const countryCodeISO3 = 'UGA'; +const disasterType = DisasterType.Drought; + +describe('Should send an email for uga drought', () => { + let accessToken: string; + + beforeEach(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + it('triggered in january', async () => { + // Mock settings + const dateJanuary = new Date(new Date().getFullYear(), 0, 1); + const triggered = true; + + // TODO: Do not hard code this but get it from the seed data + const expectedEventNames = ['Mam', 'Karamoja']; + + const nrOfEvents = 2; + const mockResult = await mockDynamicData( + disasterType, + countryCodeISO3, + triggered, + accessToken, + dateJanuary, + ); + const response = await sendNotification( + countryCodeISO3, + disasterType, + accessToken, + ); + + // Assert + // Also checking the status of the mockResult here as I think it also breaks often + expect(mockResult.status).toBe(202); + expect(response.status).toBe(201); + expect(response.body.activeEvents.email).toBeDefined(); + + expect(response.body.activeEvents.whatsapp).toBeFalsy(); + expect(response.body.finishedEvents).toBeFalsy(); + + // Parse the HTML content + const dom = new JSDOM(response.body.activeEvents.email); + const document = dom.window.document; + + // Get all span elements with apiTest="eventName" and their lower case text content + const eventNamesInEmail = Array.from( + document.querySelectorAll('span[apiTest="eventName"]'), + (el) => (el as Element).textContent.toLowerCase(), + ); + + expect(eventNamesInEmail.length).toBe(nrOfEvents); + + // Check if each expected event name is included in at least one title + for (const expectedEventName of expectedEventNames) { + const eventTitle = `${disasterType} ${expectedEventName}`.toLowerCase(); + const hasEvent = eventNamesInEmail.some((eventNameInEmail) => + eventNameInEmail.includes(eventTitle), + ); + expect(hasEvent).toBe(true); + } + }); + + it('non triggered any month', async () => { + // Mock settings + const currentDate = new Date(); + const triggered = false; + + const mockResult = await mockDynamicData( + disasterType, + countryCodeISO3, + triggered, + accessToken, + currentDate, + ); + const response = await sendNotification( + countryCodeISO3, + disasterType, + accessToken, + ); + + // Assert + // Also checking the status of the mockResult here as I think it also breaks often + expect(mockResult.status).toBe(202); + expect(response.status).toBe(201); + expect(response.body.activeEvents.email).toBeFalsy(); + expect(response.body.activeEvents.whatsapp).toBeFalsy(); + }); + + // TODO: Add more tests for different months when this issue is fixed AB#27890 +}); diff --git a/services/API-service/test/email/floods/email-ssd-floods.test.ts b/services/API-service/test/email/floods/email-ssd-floods.test.ts new file mode 100644 index 000000000..50e0ffb95 --- /dev/null +++ b/services/API-service/test/email/floods/email-ssd-floods.test.ts @@ -0,0 +1,34 @@ +import { FloodsScenario } from '../../../src/scripts/enum/mock-scenario.enum'; +import scenarios from '../../../src/scripts/mock-data/floods/ssd/scenarios.json'; +import { getAccessToken, resetDB } from '../../helpers/utility.helper'; +import { testFloodScenario } from './test-flood-scenario.helper'; + +const countryCodeISO3 = 'SSD'; +describe('Should send an email for ssd floods', () => { + let accessToken: string; + + beforeEach(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + it('default', async () => { + // Arrange + const scenario = FloodsScenario.Default; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); + + it('no-trigger', async () => { + // Arrange + const scenario = FloodsScenario.NoTrigger; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); +}); diff --git a/services/API-service/test/email/floods/email-uga-floods.test.ts b/services/API-service/test/email/floods/email-uga-floods.test.ts new file mode 100644 index 000000000..e0ea2e6c3 --- /dev/null +++ b/services/API-service/test/email/floods/email-uga-floods.test.ts @@ -0,0 +1,54 @@ +import { FloodsScenario } from '../../../src/scripts/enum/mock-scenario.enum'; +import scenarios from '../../../src/scripts/mock-data/floods/uga/scenarios.json'; +import { getAccessToken, resetDB } from '../../helpers/utility.helper'; +import { testFloodScenario } from './test-flood-scenario.helper'; + +const countryCodeISO3 = 'UGA'; +describe('Should send an email for uga floods', () => { + let accessToken: string; + + beforeEach(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + it('default', async () => { + // Arrange + const scenario = FloodsScenario.Default; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); + + it('warning', async () => { + // Arrange + const scenario = FloodsScenario.Warning; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); + + it('warning-to-trigger', async () => { + // Arrange + const scenario = FloodsScenario.WarningToTrigger; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); + + it('no-trigger', async () => { + // Arrange + const scenario = FloodsScenario.NoTrigger; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); +}); diff --git a/services/API-service/test/email/floods/test-flood-scenario.helper.ts b/services/API-service/test/email/floods/test-flood-scenario.helper.ts new file mode 100644 index 000000000..30b67375b --- /dev/null +++ b/services/API-service/test/email/floods/test-flood-scenario.helper.ts @@ -0,0 +1,65 @@ +import { JSDOM } from 'jsdom'; + +import { DisasterType } from '../../../src/api/disaster/disaster-type.enum'; +import { FloodsScenario } from '../../../src/scripts/enum/mock-scenario.enum'; +import disasters from '../../../src/scripts/json/disasters.json'; +import { mockFloods, sendNotification } from '../../helpers/utility.helper'; + +export interface TestFloodScenarioDto { + scenarios: any[]; + countryCodeISO3: string; + accessToken: string; +} + +export async function testFloodScenario( + scenario: FloodsScenario, + params: TestFloodScenarioDto, +): Promise { + const { scenarios, countryCodeISO3, accessToken } = params; + const disasterType = DisasterType.Floods; + const disasterTypeLabel = disasters.find( + (d) => d.disasterType === disasterType, + ).label; + const scenarioSeed = scenarios.find((s) => s.scenarioName === scenario); + const mockResult = await mockFloods(scenario, countryCodeISO3, accessToken); + const eventsSeed = scenarioSeed.events ? scenarioSeed.events : []; + // Act + const response = await sendNotification( + countryCodeISO3, + DisasterType.Floods, + accessToken, + ); + + // Assert + // Also checking the status of the mockResult here as I think it also breaks often + expect(mockResult.status).toBe(202); + expect(response.status).toBe(201); + if (eventsSeed.length > 0) { + expect(response.body.activeEvents.email).toBeDefined(); + } else { + expect(response.body.activeEvents.email).toBeFalsy(); + } + expect(response.body.activeEvents.whatsapp).toBeFalsy(); + expect(response.body.finishedEvents).toBeFalsy(); + + // Parse the HTML content + const dom = new JSDOM(response.body.activeEvents.email); + const document = dom.window.document; + + // Get all span elements with apiTest="eventName" and their lower case text content + const eventNamesInEmail = Array.from( + document.querySelectorAll('span[apiTest="eventName"]'), + (el) => (el as Element).textContent.toLowerCase(), + ); + + expect(eventNamesInEmail.length).toBe(eventsSeed.length); + + // Check if there are elements with the desired text content + for (const event of eventsSeed) { + const eventTitle = `${disasterTypeLabel} ${event.eventName}`.toLowerCase(); + const hasEvent = eventNamesInEmail.some((eventName) => + eventName.includes(eventTitle), + ); + expect(hasEvent).toBe(true); + } +} diff --git a/services/API-service/test/email/typhoon/email-phl-typhoon.test.ts b/services/API-service/test/email/typhoon/email-phl-typhoon.test.ts new file mode 100644 index 000000000..b43595c5e --- /dev/null +++ b/services/API-service/test/email/typhoon/email-phl-typhoon.test.ts @@ -0,0 +1,21 @@ +import { TyphoonScenario } from '../../../src/scripts/enum/mock-scenario.enum'; +import { getAccessToken, resetDB } from '../../helpers/utility.helper'; +import { testTyphoonScenario } from './test-typhoon-scenario.helper'; + +const countryCodeISO3 = 'PHL'; +describe('Should send an email for phl typhoon', () => { + let accessToken: string; + + beforeEach(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + it('default', async () => { + await testTyphoonScenario( + TyphoonScenario.EventTrigger, + countryCodeISO3, + accessToken, + ); + }); +}); diff --git a/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts b/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts new file mode 100644 index 000000000..80ca33af3 --- /dev/null +++ b/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts @@ -0,0 +1,54 @@ +import { JSDOM } from 'jsdom'; + +import { DisasterType } from '../../../src/api/disaster/disaster-type.enum'; +import { TyphoonScenario } from '../../../src/scripts/enum/mock-scenario.enum'; +import { mockTyphoon, sendNotification } from '../../helpers/utility.helper'; + +export async function testTyphoonScenario( + scenario: TyphoonScenario, + countryCodeISO3: string, + accessToken: string, +): Promise { + const nrOfEvents = 2; + const eventName = 'Mock typhoon'; + const disasterTypeLabel = DisasterType.Typhoon; + + // const disasterType = DisasterType.Typhoon; + // const disasterTypeLabel = disasters.find( + // (d) => d.disasterType === disasterType, + // ).label; + const mockResult = await mockTyphoon(scenario, countryCodeISO3, accessToken); + // Act + const response = await sendNotification( + countryCodeISO3, + DisasterType.Typhoon, + accessToken, + ); + // Assert + // Also checking the status of the mockResult here as I think it also breaks often + expect(mockResult.status).toBe(202); + expect(response.status).toBe(201); + expect(response.body.activeEvents.email).toBeDefined(); + + expect(response.body.activeEvents.whatsapp).toBeFalsy(); + expect(response.body.finishedEvents).toBeFalsy(); + + // Parse the HTML content + const dom = new JSDOM(response.body.activeEvents.email); + const document = dom.window.document; + + // Get all span elements with apiTest="eventName" and their lower case text content + const eventNamesInEmail = Array.from( + document.querySelectorAll('span[apiTest="eventName"]'), + (el) => (el as Element).textContent.toLowerCase(), + ); + + expect(eventNamesInEmail.length).toBe(nrOfEvents); + + // Check if there are elements with the desired text content + for (const eventNameInEmail of eventNamesInEmail) { + const eventTitle = `${disasterTypeLabel} ${eventName}`.toLowerCase(); + const hasEvent = eventNameInEmail.includes(eventTitle); + expect(hasEvent).toBe(true); + } +} diff --git a/services/API-service/test/helpers/utility.helper.ts b/services/API-service/test/helpers/utility.helper.ts new file mode 100644 index 000000000..49d91eaee --- /dev/null +++ b/services/API-service/test/helpers/utility.helper.ts @@ -0,0 +1,117 @@ +import * as request from 'supertest'; +import TestAgent from 'supertest/lib/agent'; + +import { DisasterType } from '../../src/api/disaster/disaster-type.enum'; +import { + FloodsScenario, + TyphoonScenario, +} from '../../src/scripts/enum/mock-scenario.enum'; +import users from '../../src/scripts/json/users.json'; + +export async function getAccessToken(): Promise { + const admin = users.find((user) => user.userRole === 'admin'); + const login = await loginApi(admin.email, admin.password); + + const accessToken = login.body.user.token; + return accessToken; +} + +export function loginApi( + email: string, + password: string, +): Promise { + return getServer().post(`/user/login`).send({ + email, + password, + }); +} + +export function getHostname(): string { + return 'http://localhost:3000/api'; +} + +export function getServer(): TestAgent { + return request.agent(getHostname()); +} + +export function resetDB(accessToken: string): Promise { + return getServer() + .post('/scripts/reset') + .set('Authorization', `Bearer ${accessToken}`) + .send({ + secret: process.env.RESET_SECRET, + }); +} + +export function mockFloods( + scenario: FloodsScenario, + countryCodeISO3: string, + accessToken: string, +): Promise { + return getServer() + .post('/mock/floods') + .set('Authorization', `Bearer ${accessToken}`) + .query({ isApiTest: true }) + .send({ + scenario, + secret: process.env.RESET_SECRET, + removeEvents: true, + date: new Date(), + countryCodeISO3, + }); +} + +export function mockTyphoon( + scenario: TyphoonScenario, + countryCodeISO3: string, + accessToken: string, +): Promise { + return getServer() + .post('/scripts/mock-typhoon-scenario') + .set('Authorization', `Bearer ${accessToken}`) + .query({ isApiTest: true }) + .send({ + scenario, + eventNr: 1, + secret: process.env.RESET_SECRET, + removeEvents: true, + date: new Date(), + countryCodeISO3, + }); +} + +export function mockDynamicData( + disasterType: DisasterType, + countryCodeISO3: string, + triggered: boolean, + accessToken: string, + date?: Date, +): Promise { + return getServer() + .post('/scripts/mock-dynamic-data') + .set('Authorization', `Bearer ${accessToken}`) + .query({ isApiTest: true }) + .send({ + disasterType, + secret: process.env.RESET_SECRET, + triggered, + removeEvents: true, + date: date ? date : new Date(), + countryCodeISO3, + }); +} + +export function sendNotification( + countryCodeISO3: string, + disasterType: DisasterType, + accessToken: string, +): Promise { + return getServer() + .post('/notification/send') + .set('Authorization', `Bearer ${accessToken}`) + .query({ isApiTest: true }) + .send({ + countryCodeISO3, + disasterType, + }); +} diff --git a/services/API-service/test/tsconfig.json b/services/API-service/test/tsconfig.json new file mode 100644 index 000000000..c05167595 --- /dev/null +++ b/services/API-service/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["node", "jest", "supertest"] + }, + "exclude": ["../node_modules", "../dist", "../src/**/*"], + "include": ["**/*.ts"] +} diff --git a/services/API-service/tsconfig.json b/services/API-service/tsconfig.json index 91fd4879a..d8166b82c 100644 --- a/services/API-service/tsconfig.json +++ b/services/API-service/tsconfig.json @@ -5,7 +5,7 @@ "noImplicitAny": false, "removeComments": true, "noLib": false, - "lib": ["es2017"], + "lib": ["es2017", "dom"], "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "es6", @@ -18,7 +18,7 @@ "baseUrl": "./", "skipLibCheck": true }, - "watchOptions": { + "watchOptions": { "watchFile": "fixedPollingInterval", "excludeDirectories": ["node_modules", "dist"], "excludeFiles": ["**/*.json"]