diff --git a/associateTests.js b/associateTests.js index c806d9da..ac1f29d6 100644 --- a/associateTests.js +++ b/associateTests.js @@ -14,7 +14,8 @@ const v3CovidTests = [ 'tests/v3/covid-19/api_nyt.spec.js', 'tests/v3/covid-19/api_therapeutics.spec.js', 'tests/v3/covid-19/api_vaccine.spec.js', - 'tests/v3/covid-19/api_worldometers.spec.js' + 'tests/v3/covid-19/api_worldometers.spec.js', + 'tests/v3/covid-19/api_variants.spec.js' ]; // Influenza tests are broken @@ -41,6 +42,7 @@ const fileNameToTestMap = { 'apiWorldometers.js': ['tests/v2/api_worldometers.spec.js', 'tests/v3/covid-19/api_worldometers.spec.js'], 'apiTherapeutics.js': ['tests/v3/covid-19/api_therapeutics.spec.js'], 'apiVaccine.js': ['tests/v3/covid-19/api_vaccine.spec.js'], + 'apiVariants.js': ['tests/v3/covid-19/api_variants.spec.js'], 'instances.js': allTests, 'appleMobilityData.js': ['tests/v2/api_apple.spec.js', 'tests/v3/covid-19/api_apple.spec.js'], 'getVaccine.js': ['tests/v3/covid-19/api_vaccine.spec.js'], diff --git a/config/config.keys.json b/config/config.keys.json index 424e26db..030804e1 100644 --- a/config/config.keys.json +++ b/config/config.keys.json @@ -19,6 +19,7 @@ "vaccine_coverage": "covidapi:vaccine_coverage", "vaccine_state_coverage": "covidapi:vaccine_state_coverage", "therapeutics": "covidapi:therapeutics", + "variants":"covidapi:variants", "influenza_ILINET": "influenza:ILINET", "influenza_USPHL": "influenza:USPHL", "influenza_USCL": "influenza:USCL" diff --git a/config/index.js b/config/index.js index 03c3251a..19e72cf7 100644 --- a/config/index.js +++ b/config/index.js @@ -38,6 +38,8 @@ config.therapeuticsInterval = process.env.THERAPEUTICS_INTERVAL || 864e5; config.ebolaInterval = process.env.EBOLA_INTERVAL || 864e5; // eslint-disable-next-line camelcase config.cdcInterval = process.env.CDC_INTERVAL || 864e5; +// eslint-disable-next-line camelcase +config.variantInterval = process.env.VARIANT_INTERVAL || 864e5; // SENTRY KEY (ONLY FOR PRODUCTION) // eslint-disable-next-line camelcase diff --git a/package-lock.json b/package-lock.json index 9ef374ed..eaff6561 100644 --- a/package-lock.json +++ b/package-lock.json @@ -333,41 +333,12 @@ } }, "node_modules/ansi-align": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", - "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", - "dev": true, - "dependencies": { - "string-width": "^3.0.0" - } - }, - "node_modules/ansi-align/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ansi-align/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", "dev": true, "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" + "string-width": "^4.1.0" } }, "node_modules/ansi-colors": { @@ -401,9 +372,9 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "engines": { "node": ">=8" @@ -994,9 +965,9 @@ "dev": true }, "node_modules/concurrently": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.2.0.tgz", - "integrity": "sha512-XxcDbQ4/43d6CxR7+iV8IZXhur4KbmEJk1CetVMUqCy34z9l0DkszbY+/9wvmSnToTej0SYomc2WSRH+L0zVJw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", + "integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==", "dev": true, "dependencies": { "chalk": "^2.4.2", @@ -1968,9 +1939,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", "funding": [ { "type": "individual", @@ -2181,9 +2152,9 @@ } }, "node_modules/glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "dependencies": { "is-glob": "^4.0.1" @@ -3410,9 +3381,9 @@ } }, "node_modules/normalize-url": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "dev": true, "engines": { "node": ">=8" @@ -3692,9 +3663,9 @@ } }, "node_modules/path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "node_modules/path-proxy": { @@ -4560,19 +4531,22 @@ } }, "node_modules/swagger-ui-dist": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.25.0.tgz", - "integrity": "sha512-vwvJPPbdooTvDwLGzjIXinOXizDJJ6U1hxnJL3y6U3aL1d2MSXDmKg2139XaLBhsVZdnQJV2bOkX4reB+RXamg==" + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.1.3.tgz", + "integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ==" }, "node_modules/swagger-ui-express": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.4.tgz", - "integrity": "sha512-Ea96ecpC+Iq9GUqkeD/LFR32xSs8gYqmTW1gXCuKg81c26WV6ZC2FsBSPVExQP6WkyUuz5HEiR0sEv/HCC343g==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz", + "integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==", "dependencies": { - "swagger-ui-dist": "^3.18.1" + "swagger-ui-dist": ">=4.1.3" }, "engines": { "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0" } }, "node_modules/table": { @@ -5500,37 +5474,12 @@ } }, "ansi-align": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", - "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", "dev": true, "requires": { - "string-width": "^3.0.0" - }, - "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } + "string-width": "^4.1.0" } }, "ansi-colors": { @@ -5557,9 +5506,9 @@ } }, "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { @@ -6049,9 +5998,9 @@ "dev": true }, "concurrently": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.2.0.tgz", - "integrity": "sha512-XxcDbQ4/43d6CxR7+iV8IZXhur4KbmEJk1CetVMUqCy34z9l0DkszbY+/9wvmSnToTej0SYomc2WSRH+L0zVJw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", + "integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -6846,9 +6795,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" }, "form-data": { "version": "2.5.1", @@ -7019,9 +6968,9 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -8022,9 +7971,9 @@ "dev": true }, "normalize-url": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "dev": true }, "nth-check": { @@ -8246,9 +8195,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-proxy": { @@ -8970,16 +8919,16 @@ } }, "swagger-ui-dist": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.25.0.tgz", - "integrity": "sha512-vwvJPPbdooTvDwLGzjIXinOXizDJJ6U1hxnJL3y6U3aL1d2MSXDmKg2139XaLBhsVZdnQJV2bOkX4reB+RXamg==" + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.1.3.tgz", + "integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ==" }, "swagger-ui-express": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.4.tgz", - "integrity": "sha512-Ea96ecpC+Iq9GUqkeD/LFR32xSs8gYqmTW1gXCuKg81c26WV6ZC2FsBSPVExQP6WkyUuz5HEiR0sEv/HCC343g==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz", + "integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==", "requires": { - "swagger-ui-dist": "^3.18.1" + "swagger-ui-dist": ">=4.1.3" } }, "table": { diff --git a/package.json b/package.json index 6bc0c8bd..c2c87866 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "lint": "eslint '**/*.js'", "lint:fix": "eslint ./**/*.js --fix", "lint:win32": "eslint ./**/*.js", - "test": "mocha ./tests --exit --recursive --timeout 200000", - "test-single": "mocha $1 --exit --timeout 200000", + "test": "mocha ./tests --exit --recursive --timeout 300000", + "test-single": "mocha $1 --exit --timeout 300000", "docker-start": "docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build", "docker-down": "docker-compose -f docker-compose.yml -f docker-compose.prod.yml down --rmi all", "docker-start-dev": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build", diff --git a/public/apidocs/swagger_v3.json b/public/apidocs/swagger_v3.json index b1ce38c2..4e61f75c 100644 --- a/public/apidocs/swagger_v3.json +++ b/public/apidocs/swagger_v3.json @@ -44,6 +44,10 @@ "name": "COVID-19: Therapeutics", "description": "(COVID-19 therapeutic trial data from raps.org, updated every 24 hours)" }, + { + "name": "COVID-19: Variants", + "description": "(COVID-19 data from The European Surveillance System -TESSy, provided by [Austria, Belgium, Bulgaria, Croatia, Cyprus, Czechia, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Iceland, Ireland, Italy, Latvia, Liechtenstein, Lithuania, Luxembourg, Malta, Netherlands, Norway, Poland, Portugal, Romania, Slovakia, Slovenia, Spain and Sweden] https://www.ecdc.europa.eu and released by ECDC updated every week)" + }, { "name": "Influenza: CDC", "description": "(Influenza data reported by the United States CDC, updated every 24 hours)" @@ -1507,6 +1511,68 @@ } } }, + "/v3/covid-19/variants/countries/": { + "get": { + "tags": [ + "COVID-19: Variants" + ], + "summary": "Get a list of supported countries for ECDC specific data", + "description": "Returns a list of supported country names", + "responses": { + "200": { + "description": "Status Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/variantsECDC" + } + } + } + } + } + } + }, + "/v3/covid-19/variants/countries/{country}": { + "get": { + "tags": [ + "COVID-19: Variants" + ], + "parameters": [ + { + "name": "country", + "in": "path", + "required": true, + "description": "A valid country name from the /v3/covid-19/variants/countries/ endpoint", + "type": "string" + }, + { + "name": "allowNull", + "in": "query", + "enum": [ + "true", + "false", + "1", + "0" + ], + "description": "By default, if a value is missing, it is returned as 0. This allows nulls to be returned", + "type": "string" + } + ], + "summary": "Get COVID-19 ECDC reported data for a specific country", + "responses": { + "200": { + "description": "Status Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/variantsCountriesECDC" + } + } + } + } + } + } + }, "/v3/influenza/cdc/ILINet": { "get": { "tags": [ @@ -2593,6 +2659,57 @@ } } }, + "variantsECDC": { + "type": "array", + "items": { + "type": "string" + } + }, + "variantsCountriesECDC": { + "type": "array", + "items": { + "type": "object", + "properties": { + "updated": { + "type": "number", + "format": "date" + }, + "country": { + "type": "string" + }, + "yearWeek": { + "type": "string" + }, + "source": { + "type": "string" + }, + "newCases": { + "type": "number" + }, + "numberSequenced": { + "type": "number" + }, + "percentSequenced": { + "type": "number" + }, + "validDenominator": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "numberDetectionsVariant": { + "type": "number" + }, + "numberSequencedKnownVariant": { + "type": "number" + }, + "percentVariant": { + "type": "number" + } + } + } + }, "influenzaUSPHL": { "properties": { "updated": { diff --git a/routes/instances.js b/routes/instances.js index 6cd8c2bd..4f041b35 100644 --- a/routes/instances.js +++ b/routes/instances.js @@ -12,6 +12,7 @@ const appleData = require('../scrapers/covid-19/appleMobilityData'); const govData = require('../scrapers/covid-19/govScrapers/getGovData'); const { getVaccineData, getVaccineCoverageData, getVaccineStateCoverageData } = require('../scrapers/covid-19/getVaccine'); const getTherapeuticsData = require('../scrapers/covid-19/getTherapeutics'); +const variantsData = require('../scrapers/covid-19/getVariants'); const getCDCDInfluenzaData = require('../scrapers/influenza/getCDC'); // KEYS @@ -72,6 +73,10 @@ module.exports = { await getTherapeuticsData(keys, redis); logger.info('Finished Therapeutics scraping!'); }, + executeScraperVariants: async () => { + await variantsData(keys, redis); + logger.info('Finished Variants scraping!'); + }, excecuteScraperInfluenza: async () => { await getCDCDInfluenzaData(keys, redis); logger.info('Finished CDC Influenza scraping!'); diff --git a/routes/v3/covid-19/apiVariants.js b/routes/v3/covid-19/apiVariants.js new file mode 100644 index 00000000..ee1e1cd4 --- /dev/null +++ b/routes/v3/covid-19/apiVariants.js @@ -0,0 +1,24 @@ +// eslint-disable-next-line new-cap +const router = require('express').Router(); + +const nameUtils = require('../../../utils/nameUtils'); +const { wordToBoolean } = require('../../../utils/stringUtils'); +const { redis, keys } = require('../../instances'); + +router.get('/v3/covid-19/variants/countries/:country?', async (req, res) => { + const { allowNull } = req.query; + const { country: countryName } = req.params; + if (countryName) { + const standardizedCountryName = nameUtils.getCountryData(countryName.trim()).country || countryName.trim(); + const data = JSON.parse(await redis.hget(keys.variants, standardizedCountryName)); + if (data) { + res.send(!wordToBoolean(allowNull) ? nameUtils.transformNull(data) : data); + } else { + res.status(404).send({ message: `Country '${standardizedCountryName}' not found or no data found for country` }); + } + } else { + res.send((await redis.hkeys(keys.variants)).sort()); + } +}); + +module.exports = router; diff --git a/scrapers/covid-19/getVariants.js b/scrapers/covid-19/getVariants.js new file mode 100644 index 00000000..86d8cf60 --- /dev/null +++ b/scrapers/covid-19/getVariants.js @@ -0,0 +1,69 @@ +const axios = require('axios'); +const logger = require('../../utils/logger'); +const csvUtils = require('../../utils/csvUtils'); + +const PATH + = 'https://opendata.ecdc.europa.eu/covid19/virusvariant/csv/data.csv'; + +/** + * Requests and parses csv data that is used to populate the data table on the European Centre for Disease Prevention and Control (ECDPC) site + */ +const europeanCountriesData = async () => { + try { + const europeRes = (await axios.get(PATH)).data; + const parsedEuropeanCountriesData = await csvUtils.parseCsvData( + europeRes + ); + return parsedEuropeanCountriesData.map((country) => ({ + updated: Date.now(), + country: country.country, + yearWeek: country.year_week, + source: country.source, + newCases: parseInt(country.new_cases) || null, + numberSequenced: parseInt(country.number_sequenced) || null, + percentSequenced: parseFloat(country.percent_sequenced) || null, + validDenominator: country.valid_denominator, + variant: country.variant, + numberDetectionsVariant: + parseInt(country.number_detections_variant) || null, + numberSequencedKnownVariant: + parseInt(country.number_sequenced_known_variant) || null, + percentVariant: parseFloat(country.percent_variant) || null + })); + } catch (err) { + logger.err('Error: Requesting ECDC Data failed!', err); + return null; + } +}; + +const variantsData = async (keys, redis) => { + try { + const countriesData = await europeanCountriesData(); + + const dataByCountry = countriesData + .map((obj) => obj.country) + .reduce((obj, country) => { + const groupByCountry = countriesData.filter( + (item) => item.country === country + ); + obj[country] = groupByCountry; + return obj; + }, {}); + + const uniquesCountries = countriesData + .map((country) => country.country) + .filter((value, index, self) => self.indexOf(value) === index); + + for (var i in uniquesCountries) { + await redis.hset( + keys.variants, + uniquesCountries[i], + JSON.stringify(dataByCountry[uniquesCountries[i]]) + ); + } + } catch (err) { + logger.err('Error: Formating Variants data failed!', err); + } +}; + +module.exports = variantsData; diff --git a/server.js b/server.js index 4214c527..e156c534 100644 --- a/server.js +++ b/server.js @@ -94,6 +94,7 @@ app.use(require('./routes/v3/covid-19/apiApple')); app.use(require('./routes/v3/covid-19/apiGov')); app.use(require('./routes/v3/covid-19/apiVaccine')); app.use(require('./routes/v3/covid-19/apiTherapeutics')); +app.use(require('./routes/v3/covid-19/apiVariants')); app.use(require('./routes/v3/influenza/apiInfluenza')); app.listen(port, () => logger.info(`Your app is listening on port ${port}`)); diff --git a/serverScraper.js b/serverScraper.js index 3103b89d..5f799bbe 100644 --- a/serverScraper.js +++ b/serverScraper.js @@ -1,5 +1,5 @@ const { scraper: { executeScraper, executeScraperNYTData, excecuteScraperAppleData, excecuteScraperGov, excecuteScraperVaccine, excecuteScraperVaccineCoverage, excecuteScraperVaccineStateCoverage, - executeScraperTherapeutics, excecuteScraperInfluenza }, + executeScraperTherapeutics, executeScraperVariants, excecuteScraperInfluenza }, config } = require('./routes/instances'); executeScraper(); @@ -11,6 +11,7 @@ excecuteScraperVaccine(); excecuteScraperVaccineCoverage(); excecuteScraperVaccineStateCoverage(); executeScraperTherapeutics(); +executeScraperVariants(); // Update Worldometer and Johns Hopkins data every 10 minutes setInterval(executeScraper, config.worldometersInterval); @@ -30,3 +31,5 @@ setInterval(excecuteScraperVaccineStateCoverage, config.vaccineCoverageInterval) setInterval(executeScraperTherapeutics, config.therapeuticsInterval); // Update CDC Influenza data every 24 hours setInterval(excecuteScraperInfluenza, config.cdcInterval); +// Update ECDC data every 24 hours +setInterval(executeScraperVariants, config.variantInterval); diff --git a/tests/mochaSetup.spec.js b/tests/mochaSetup.spec.js index ec76ccfc..f6f50e92 100644 --- a/tests/mochaSetup.spec.js +++ b/tests/mochaSetup.spec.js @@ -1,9 +1,21 @@ -const { scraper: { executeScraper, executeScraperNYTData, excecuteScraperAppleData, excecuteScraperGov, excecuteScraperInfluenza, excecuteScraperVaccineCoverage, excecuteScraperVaccineStateCoverage }, - redis } = require('../routes/instances'); +const { + scraper: { + executeScraper, + executeScraperNYTData, + excecuteScraperAppleData, + excecuteScraperGov, + excecuteScraperInfluenza, + excecuteScraperVaccineCoverage, + excecuteScraperVaccineStateCoverage, + executeScraperVariants + }, + redis +} = require('../routes/instances'); const logger = require('../utils/logger'); const [arg] = process.argv[5].split('/').slice(-1); const argValue = arg.substring(arg.indexOf('_') + 1, arg.indexOf('.')); +console.log(argValue); const mapArgToScraper = { worldometers: executeScraper, jhucsse: executeScraper, @@ -13,7 +25,8 @@ const mapArgToScraper = { gov: excecuteScraperGov, influenza: excecuteScraperInfluenza, vaccine: excecuteScraperVaccineCoverage, - vaccinestate: excecuteScraperVaccineStateCoverage + vaccinestate: excecuteScraperVaccineStateCoverage, + variants: executeScraperVariants }; // eslint-disable-next-line @@ -30,6 +43,7 @@ before(async () => { await excecuteScraperInfluenza(); await excecuteScraperVaccineCoverage(); await excecuteScraperVaccineStateCoverage(); + await executeScraperVariants(); logger.info('Scraping all data finished.'); } }); diff --git a/tests/v3/covid-19/api_variants.spec.js b/tests/v3/covid-19/api_variants.spec.js new file mode 100644 index 00000000..f6898423 --- /dev/null +++ b/tests/v3/covid-19/api_variants.spec.js @@ -0,0 +1,90 @@ +/* eslint-disable no-undef */ +const chai = require('chai'); +const chaiHttp = require('chai-http'); +const app = require('../../../server'); +const { testBasicProperties } = require('../../testingFunctions'); + +chai.use(chaiHttp); + +const variantCountries = [ + 'Austria', + 'Belgium', + 'Bulgaria', + 'Croatia', + 'Cyprus', + 'Czechia', + 'Denmark', + 'Estonia', + 'Finland', + 'France', + 'Germany', + 'Greece', + 'Hungary', + 'Iceland', + 'Ireland', + 'Italy', + 'Latvia', + 'Liechtenstein', + 'Lithuania', + 'Luxembourg', + 'Malta', + 'Netherlands', + 'Norway', + 'Poland', + 'Portugal', + 'Romania', + 'Slovakia', + 'Slovenia', + 'Spain', + 'Sweden' +]; + +describe('TESTING /v3/covid-19/variants/countries general', () => { + it('/v3/covid-19/variants/countries/ correct countries', (done) => { + chai.request(app) + .get('/v3/covid-19/variants/countries') + .end((err, res) => { + testBasicProperties(err, res, 200, 'array'); + res.body.length.should.be.equal(variantCountries.length); + res.body.forEach((country) => variantCountries.should.include(country)); + done(); + }); + }); + + + it('TESTING /v3/covid-19/variants/countries invalid country', (done) => { + chai.request(app) + .get('/v3/covid-19/variants/countries/notACountry') + .end((err, res) => { + testBasicProperties(err, res, 404, 'object'); + res.body.should.have.property('message'); + done(); + }); + }); +}); + +describe('TESTING /v3/covid-19/variants/countries/country', () => { + it('/v3/covid-19/variants/Country variants correct fields set', (done) => { + chai.request(app) + .get('/v3/covid-19/variants/countries/Austria') + .end((err, res) => { + testBasicProperties(err, res, 200, 'array'); + res.body.length.should.be.above(1); + res.body.forEach((element) => { + element.should.have.property('updated'); + element.should.have.property('country'); + element.should.have.property('yearWeek'); + element.should.have.property('source'); + element.should.have.property('newCases'); + element.should.have.property('numberSequenced'); + element.should.have.property('percentSequenced'); + element.should.have.property('validDenominator'); + element.should.have.property('variant'); + element.should.have.property('numberDetectionsVariant'); + element.should.have.property('numberSequencedKnownVariant'); + element.should.have.property('percentVariant'); + }); + done(); + }); + }); +}); diff --git a/tests/v3/influenza/api_influenza.spec.js b/tests/v3/influenza/api_influenza.spec.js index 8393ccaa..b0acf718 100644 --- a/tests/v3/influenza/api_influenza.spec.js +++ b/tests/v3/influenza/api_influenza.spec.js @@ -1,8 +1,8 @@ /* eslint-disable no-undef */ -const chai = require('chai'); -const chaiHttp = require('chai-http'); -const app = require('../../../server'); -const { testBasicProperties } = require('../../testingFunctions'); +// const chai = require('chai'); +// const chaiHttp = require('chai-http'); +// const app = require('../../../server'); +// const { testBasicProperties } = require('../../testingFunctions'); // chai.use(chaiHttp);