diff --git a/app/domain/types.ts b/app/domain/types.ts index 15bfd321..0dce1bdb 100644 --- a/app/domain/types.ts +++ b/app/domain/types.ts @@ -34,3 +34,10 @@ export enum Symptom { HEADACHE = 'HEADACHE', SORE_THROAT = 'SORE_THROAT' } + +export interface AggregatedCovidReportData { + numberOfReports: number; + numberOfPeopleShowingSymptoms: number; + numberOfConfirmedInfected: number; + numberOfTested: number; +} diff --git a/app/repository/CovidReportRepository.ts b/app/repository/CovidReportRepository.ts index 5bc9d260..8bdb38da 100644 --- a/app/repository/CovidReportRepository.ts +++ b/app/repository/CovidReportRepository.ts @@ -2,7 +2,7 @@ import { getInstance, SqlLiteDatabase } from './SqlLiteDatabase'; import { CovidReport } from '../domain/types'; -const SELECT_ALL_COVID_REPORTS = 'select json_dump from covid_report'; +const SELECT_ALL_COVID_REPORTS = 'select passcode, json_dump from covid_report'; const SELECT_COVID_REPORT = 'select * from covid_report where passcode = (?)'; @@ -57,6 +57,17 @@ export class CovidReportRepository { ); } + async getLatestCovidReports(): Promise { + const rows = await this.db.getAll(SELECT_ALL_COVID_REPORTS); + const latestReports: { [key: string]: CovidReport } = {}; + rows.forEach((row: CovidReportRow) => { + latestReports[row.passcode] = this.parseJsonDumpToCovidReport( + row.json_dump + ); + }); + return Object.values(latestReports); + } + private parseJsonDumpToCovidReport(jsonDump: string): CovidReport { return JSON.parse(jsonDump); } diff --git a/app/routes/api-routes.ts b/app/routes/api-routes.ts new file mode 100644 index 00000000..45c2f2dd --- /dev/null +++ b/app/routes/api-routes.ts @@ -0,0 +1,19 @@ +import express from 'express'; + +import { CovidReportRepository } from '../repository/CovidReportRepository'; +import { aggregateCovidReports } from '../util/report-aggregator'; + +const router = express.Router(); +const reportRepo = new CovidReportRepository(); + +router.get('/aggregated', async (req, res) => { + const reports = await reportRepo.getLatestCovidReports(); + const aggregated = aggregateCovidReports(reports); + res.send(aggregated); +}); + +router.get('/', (req, res) => { + return res.render('pages/map'); +}); + +export default router; diff --git a/app/server.ts b/app/server.ts index b02d1c4d..dbd4b439 100644 --- a/app/server.ts +++ b/app/server.ts @@ -4,6 +4,7 @@ import bodyParser from 'body-parser'; import path from 'path'; import reportRoutes from './routes/report-routes'; import mapRoutes from './routes/map-routes'; +import apiRoutes from './routes/api-routes'; import { getInstance } from './repository/SqlLiteDatabase'; const SqLiteStore = require('connect-sqlite3')(session); @@ -38,6 +39,7 @@ app.set('views', [ app.use('/', reportRoutes); app.use('/kart', mapRoutes); +app.use('/api', apiRoutes); app.use( '/static', diff --git a/app/util/report-aggregator.test.ts b/app/util/report-aggregator.test.ts new file mode 100644 index 00000000..6f81ee64 --- /dev/null +++ b/app/util/report-aggregator.test.ts @@ -0,0 +1,112 @@ +import { aggregateCovidReports } from './report-aggregator'; +import { + CovidReport, + Sex, + TestResult, + Symptom, + AggregatedCovidReportData +} from '../domain/types'; + +const reports: CovidReport[] = [ + { + yearOfBirth: '1993', // Deprecated + sex: Sex.MALE, + postalCode: '1234', + hasBeenTested: false, + testedAt: new Date(), // Not in use + symptomStart: '2020-03-02', // YYYY-MM-DD + testResult: TestResult.POSITIVE, + inQuarantine: true, + hasBeenAbroadLastTwoWeeks: true, + symptoms: { + [Symptom.DRY_COUGH]: true, + [Symptom.EXHAUSTION]: false, + [Symptom.FEVER]: false, + [Symptom.HEAVY_BREATHING]: false, + [Symptom.MUSCLE_ACHING]: false, + [Symptom.DIARRHEA]: false, + [Symptom.HEADACHE]: false, + [Symptom.SORE_THROAT]: false + }, + submissionTimestamp: 123123123, + age: '50' + }, + { + yearOfBirth: '1993', // Deprecated + sex: Sex.MALE, + postalCode: '1234', + hasBeenTested: false, + testedAt: new Date(), // YYYY-MM-DD + symptomStart: '2020-03-02', // YYYY-MM-DD + testResult: undefined, + inQuarantine: false, + hasBeenAbroadLastTwoWeeks: false, + symptoms: { + [Symptom.DRY_COUGH]: true, + [Symptom.EXHAUSTION]: true, + [Symptom.FEVER]: true, + [Symptom.HEAVY_BREATHING]: false, + [Symptom.MUSCLE_ACHING]: false, + [Symptom.DIARRHEA]: false, + [Symptom.HEADACHE]: false, + [Symptom.SORE_THROAT]: false + }, + submissionTimestamp: 123123123, + age: '50' + }, + { + yearOfBirth: '1993', // Deprecated + sex: Sex.MALE, + postalCode: '1234', + hasBeenTested: false, + testedAt: new Date(), // YYYY-MM-DD + symptomStart: '2020-03-02', // YYYY-MM-DD + testResult: TestResult.PENDING, + inQuarantine: false, + hasBeenAbroadLastTwoWeeks: false, + symptoms: { + [Symptom.DRY_COUGH]: false, + [Symptom.EXHAUSTION]: false, + [Symptom.FEVER]: false, + [Symptom.HEAVY_BREATHING]: false, + [Symptom.MUSCLE_ACHING]: false, + [Symptom.DIARRHEA]: false, + [Symptom.HEADACHE]: false, + [Symptom.SORE_THROAT]: false + }, + submissionTimestamp: 123123123, + age: '50' + }, + { + yearOfBirth: '1993', // Deprecated + sex: Sex.MALE, + postalCode: '1234', + hasBeenTested: false, + testedAt: new Date(), // YYYY-MM-DD + symptomStart: '2020-03-02', // YYYY-MM-DD + testResult: TestResult.NEGATIVE, + inQuarantine: false, + hasBeenAbroadLastTwoWeeks: false, + symptoms: { + [Symptom.DRY_COUGH]: true, + [Symptom.EXHAUSTION]: false, + [Symptom.FEVER]: false, + [Symptom.HEAVY_BREATHING]: false, + [Symptom.MUSCLE_ACHING]: false, + [Symptom.DIARRHEA]: false, + [Symptom.HEADACHE]: false, + [Symptom.SORE_THROAT]: false + }, + submissionTimestamp: 123123123, + age: '50' + } +]; + +it('should count all correctly', () => { + const aggregated: AggregatedCovidReportData = aggregateCovidReports(reports); + + expect(aggregated.numberOfReports).toBe(4); + expect(aggregated.numberOfPeopleShowingSymptoms).toBe(3); + expect(aggregated.numberOfConfirmedInfected).toBe(1); + expect(aggregated.numberOfTested).toBe(3); +}); diff --git a/app/util/report-aggregator.ts b/app/util/report-aggregator.ts new file mode 100644 index 00000000..f70c03be --- /dev/null +++ b/app/util/report-aggregator.ts @@ -0,0 +1,33 @@ +import { + CovidReport, + AggregatedCovidReportData, + TestResult +} from '../domain/types'; + +const isShowingAtLeastOneSymptom = (report: CovidReport): boolean => { + return Object.values(report.symptoms).filter(value => !!value).length > 0; +}; + +export const aggregateCovidReports = ( + reports: CovidReport[] +): AggregatedCovidReportData => { + const aggredatedData: AggregatedCovidReportData = { + numberOfReports: 0, + numberOfPeopleShowingSymptoms: 0, + numberOfConfirmedInfected: 0, + numberOfTested: 0 + }; + for (const report of reports) { + aggredatedData.numberOfReports += 1; + if (isShowingAtLeastOneSymptom(report)) { + aggredatedData.numberOfPeopleShowingSymptoms += 1; + } + if (report.testResult) { + aggredatedData.numberOfTested += 1; + } + if (report.testResult === TestResult.POSITIVE) { + aggredatedData.numberOfConfirmedInfected += 1; + } + } + return aggredatedData; +}; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..5056e385 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: 'ts-jest' +}; diff --git a/package.json b/package.json index 5a880219..cf5d6371 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@types/body-parser": "^1.19.0", "@types/express": "^4.17.3", "@types/express-session": "^1.17.0", + "@types/jest": "^25.1.4", "@types/node-fetch": "^2.5.5", "@typescript-eslint/eslint-plugin": "^2.23.0", "@typescript-eslint/parser": "^2.23.0", @@ -38,7 +39,8 @@ "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-prettier": "^3.1.2", "jest": "^25.1.0", - "prettier": "^1.19.1" + "prettier": "^1.19.1", + "ts-jest": "^25.2.1" }, "eslintConfig": { "settings": { diff --git a/yarn.lock b/yarn.lock index e6d9c18d..5417a874 100644 --- a/yarn.lock +++ b/yarn.lock @@ -442,6 +442,14 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/jest@^25.1.4": + version "25.1.4" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.1.4.tgz#9e9f1e59dda86d3fd56afce71d1ea1b331f6f760" + integrity sha512-QDDY2uNAhCV7TMCITrxz+MRk1EizcsevzfeS6LykIlq2V1E5oO4wXG8V2ZEd9w7Snxeeagk46YbMgZ8ESHx3sw== + dependencies: + jest-diff "^25.1.0" + pretty-format "^25.1.0" + "@types/json-schema@^7.0.3": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" @@ -932,6 +940,13 @@ browser-resolve@^1.11.3: dependencies: resolve "1.1.7" +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -939,7 +954,7 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -buffer-from@^1.0.0: +buffer-from@1.x, buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== @@ -1849,7 +1864,7 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -3014,6 +3029,13 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json5@2.x, json5@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" + integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== + dependencies: + minimist "^1.2.0" + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -3021,13 +3043,6 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" - integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== - dependencies: - minimist "^1.2.0" - jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -3113,6 +3128,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -3137,7 +3157,7 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" -make-error@^1.1.1: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -3270,7 +3290,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -4047,7 +4067,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.3.2: +resolve@1.x, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.3.2: version "1.15.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== @@ -4156,7 +4176,7 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -4719,6 +4739,22 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +ts-jest@^25.2.1: + version "25.2.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.2.1.tgz#49bf05da26a8b7fbfbc36b4ae2fcdc2fef35c85d" + integrity sha512-TnntkEEjuXq/Gxpw7xToarmHbAafgCaAzOpnajnFC6jI7oo1trMzAHA04eWpc3MhV6+yvhE8uUBAmN+teRJh0A== + dependencies: + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + mkdirp "0.x" + resolve "1.x" + semver "^5.5" + yargs-parser "^16.1.0" + ts-node@^8.6.2: version "8.6.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35" @@ -5050,6 +5086,14 @@ yallist@^3.0.0, yallist@^3.0.3: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yargs-parser@^16.1.0: + version "16.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1" + integrity sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^18.1.0: version "18.1.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.0.tgz#1b0ab1118ebd41f68bb30e729f4c83df36ae84c3"