diff --git a/.gitignore b/.gitignore index 0560fa6921..ce99915bed 100755 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,10 @@ dist/ *.sublime-project *.sublime-workspace +# IntelliJ editor +# ============== +.idea/ + # General # ======= *.log diff --git a/.husky/pre-commit b/.husky/pre-commit index b4a05d6e00..976b0d29cd 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx lint-staged && sh secrets-check.sh \ No newline at end of file +npm run pre-commit && sh secrets-check.sh \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 87f59187c6..a90b4ef2a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,61 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v5.10.1](https://github.com/opengovsg/FormSG/compare/v5.10.0...v5.10.1) + +- fix: allow for unknown keys in updateEndPage validator [`617d86a`](https://github.com/opengovsg/FormSG/commit/617d86a28910eec6ebd3249a2de636086429d6a6) + +#### [v5.10.0](https://github.com/opengovsg/FormSG/compare/v5.9.0...v5.10.0) + +> 11 May 2021 + +- fix(collaborator-modal/client/controller): bugfix for spinner ui [`#1858`](https://github.com/opengovsg/FormSG/pull/1858) +- fix(deps): bump @opengovsg/spcp-auth-client from 1.4.6 to 1.4.7 [`#1853`](https://github.com/opengovsg/FormSG/pull/1853) +- fix(deps): bump @sentry/browser from 6.3.5 to 6.3.6 [`#1851`](https://github.com/opengovsg/FormSG/pull/1851) +- chore(deps-dev): bump @typescript-eslint/parser from 4.22.1 to 4.23.0 [`#1852`](https://github.com/opengovsg/FormSG/pull/1852) +- fix(deps): bump abortcontroller-polyfill from 1.7.1 to 1.7.3 [`#1823`](https://github.com/opengovsg/FormSG/pull/1823) +- fix: align joi validation types to schema [`#1800`](https://github.com/opengovsg/FormSG/pull/1800) +- chore(deps-dev): bump concurrently from 6.0.2 to 6.1.0 [`#1832`](https://github.com/opengovsg/FormSG/pull/1832) +- chore(deps-dev): bump @types/bcrypt from 3.0.1 to 5.0.0 [`#1830`](https://github.com/opengovsg/FormSG/pull/1830) +- chore(deps-dev): bump lint-staged from 10.5.4 to 11.0.0 [`#1827`](https://github.com/opengovsg/FormSG/pull/1827) +- chore(deps-dev): bump core-js from 3.12.0 to 3.12.1 [`#1829`](https://github.com/opengovsg/FormSG/pull/1829) +- chore(deps-dev): bump eslint from 7.25.0 to 7.26.0 [`#1828`](https://github.com/opengovsg/FormSG/pull/1828) +- fix(deps): bump aws-sdk from 2.901.0 to 2.903.0 [`#1826`](https://github.com/opengovsg/FormSG/pull/1826) +- chore(deps-dev): bump date-fns from 2.21.2 to 2.21.3 [`#1824`](https://github.com/opengovsg/FormSG/pull/1824) +- fix: speed up precommit hooks [`#1820`](https://github.com/opengovsg/FormSG/pull/1820) +- fix(deps): bump twilio from 3.61.0 to 3.62.0 [`#1818`](https://github.com/opengovsg/FormSG/pull/1818) +- chore(deps-dev): bump core-js from 3.11.3 to 3.12.0 [`#1817`](https://github.com/opengovsg/FormSG/pull/1817) +- fix(deps): bump aws-sdk from 2.900.0 to 2.901.0 [`#1816`](https://github.com/opengovsg/FormSG/pull/1816) +- feat(email-submission): separate error logging for db and state checks [`#1813`](https://github.com/opengovsg/FormSG/pull/1813) +- feat(admin-form): individual form field api [`#1799`](https://github.com/opengovsg/FormSG/pull/1799) +- chore(deps-dev): bump @types/bluebird from 3.5.33 to 3.5.34 [`#1807`](https://github.com/opengovsg/FormSG/pull/1807) +- chore(deps-dev): bump @types/node from 14.14.43 to 14.14.44 [`#1805`](https://github.com/opengovsg/FormSG/pull/1805) +- fix(deps): bump aws-sdk from 2.899.0 to 2.900.0 [`#1809`](https://github.com/opengovsg/FormSG/pull/1809) +- chore(deps-dev): bump core-js from 3.11.2 to 3.11.3 [`#1808`](https://github.com/opengovsg/FormSG/pull/1808) +- chore(deps-dev): bump date-fns from 2.21.1 to 2.21.2 [`#1806`](https://github.com/opengovsg/FormSG/pull/1806) +- chore(deps-dev): bump ts-jest from 26.5.5 to 26.5.6 [`#1804`](https://github.com/opengovsg/FormSG/pull/1804) +- fix: remove Learn More link for MyInfo field limit [`#1802`](https://github.com/opengovsg/FormSG/pull/1802) +- refactor(adminform): update form collab [`#1744`](https://github.com/opengovsg/FormSG/pull/1744) +- chore: gitignore intellij files [`#1798`](https://github.com/opengovsg/FormSG/pull/1798) +- chore: merge v5.9.0 into develop [`#1797`](https://github.com/opengovsg/FormSG/pull/1797) +- refactor: extract update logic endpoint [`#1695`](https://github.com/opengovsg/FormSG/pull/1695) +- feat: add <sg-govt-banner-component> to top of public forms [`#1439`](https://github.com/opengovsg/FormSG/pull/1439) +- fix(deps): bump aws-sdk from 2.897.0 to 2.899.0 [`#1793`](https://github.com/opengovsg/FormSG/pull/1793) +- chore(deps-dev): bump @typescript-eslint/parser from 4.22.0 to 4.22.1 [`#1794`](https://github.com/opengovsg/FormSG/pull/1794) +- chore(deps-dev): bump @babel/preset-env from 7.14.0 to 7.14.1 [`#1795`](https://github.com/opengovsg/FormSG/pull/1795) +- fix(deps): bump convict from 6.0.1 to 6.1.0 [`#1792`](https://github.com/opengovsg/FormSG/pull/1792) +- fix(deps): bump libphonenumber-js from 1.9.16 to 1.9.17 [`#1791`](https://github.com/opengovsg/FormSG/pull/1791) +- chore(deps-dev): bump @typescript-eslint/eslint-plugin [`#1790`](https://github.com/opengovsg/FormSG/pull/1790) +- feat(api-refactor): add specific update end page endpoint in server [`#1760`](https://github.com/opengovsg/FormSG/pull/1760) +- feat: move server.ts into src/app [`#1785`](https://github.com/opengovsg/FormSG/pull/1785) +- fix: trigger digest cycle for delete logic [`#1787`](https://github.com/opengovsg/FormSG/pull/1787) +- chore: bump version to 5.9.0 [`6d6e475`](https://github.com/opengovsg/FormSG/commit/6d6e475c417cfb5efacb203888b0f296159d8ac1) +- chore: bump version to v5.10.0 [`0615ce5`](https://github.com/opengovsg/FormSG/commit/0615ce5262fcdb65932ad6c9be9ee66503b0e949) + #### [v5.9.0](https://github.com/opengovsg/FormSG/compare/v5.8.0...v5.9.0) -- fix: trigger digest cycle for delete logic [`#1787`](https://github.com/opengovsg/FormSG/pull/1787) +> 4 May 2021 + - fix: allow commas in email confirmation sender [`#1782`](https://github.com/opengovsg/FormSG/pull/1782) - chore(deps-dev): bump core-js from 3.11.1 to 3.11.2 [`#1780`](https://github.com/opengovsg/FormSG/pull/1780) - fix(deps): bump fp-ts from 2.10.4 to 2.10.5 [`#1781`](https://github.com/opengovsg/FormSG/pull/1781) @@ -31,6 +83,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(deps): bump aws-sdk from 2.893.0 to 2.894.0 [`#1756`](https://github.com/opengovsg/FormSG/pull/1756) - fix(deps): bump @sentry/integrations from 6.3.1 to 6.3.3 [`#1755`](https://github.com/opengovsg/FormSG/pull/1755) - chore: merge v5.8.0 into develop [`#1751`](https://github.com/opengovsg/FormSG/pull/1751) +- chore: bump version to 5.9.0 [`902fd6a`](https://github.com/opengovsg/FormSG/commit/902fd6a764e94bd0882ca1f7bebb3e79f916c9f3) #### [v5.8.0](https://github.com/opengovsg/FormSG/compare/v5.7.1...v5.8.0) diff --git a/README.md b/README.md index 9d0e61aba3..b650819853 100755 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ npm run dev After the Docker image has finished building, the application can be accessed at [localhost:5000](localhost:5000). If there have been no dependency changes in `package.json` or changes in the -`src/server.ts` file, you can run +`src/app/server.ts` file, you can run ```bash docker-compose up diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1ff2eb9229..699cc095e7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -13,7 +13,7 @@ identities, and E-mail servers hosted in Government Data Centres. ## Backend -The backend for FormSG is bootstrapped using `src/server.ts` and `src/app/loaders`. +The backend for FormSG is bootstrapped using `src/app/server.ts` and `src/app/loaders`. It sets up express.js routes defined in `src/app/**/*.routes.ts`, with business logic defined in `src/app/**/*.controller.ts` and mongoose models defined in `src/app/**/*.model.ts`. diff --git a/package-lock.json b/package-lock.json index e0492e2b0a..5566d39eda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "FormSG", - "version": "5.9.0", + "version": "5.10.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -579,9 +579,9 @@ "dev": true }, "@babel/types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", - "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", + "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.0", @@ -627,12 +627,12 @@ } }, "@babel/generator": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz", - "integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", + "integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", "dev": true, "requires": { - "@babel/types": "^7.14.0", + "@babel/types": "^7.14.1", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -690,9 +690,9 @@ } }, "@babel/parser": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz", - "integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", "dev": true }, "@babel/template": { @@ -723,9 +723,9 @@ } }, "@babel/types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", - "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", + "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.0", @@ -817,12 +817,12 @@ } }, "@babel/generator": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz", - "integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", + "integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", "dev": true, "requires": { - "@babel/types": "^7.14.0", + "@babel/types": "^7.14.1", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -874,9 +874,9 @@ } }, "@babel/parser": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz", - "integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", "dev": true }, "@babel/template": { @@ -907,9 +907,9 @@ } }, "@babel/types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", - "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", + "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.0", @@ -965,19 +965,19 @@ } }, "@babel/helper-module-transforms": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz", - "integrity": "sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz", + "integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.13.12", "@babel/helper-replace-supers": "^7.13.12", "@babel/helper-simple-access": "^7.13.12", "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.12.11", + "@babel/helper-validator-identifier": "^7.14.0", "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.13", - "@babel/types": "^7.13.14" + "@babel/traverse": "^7.14.0", + "@babel/types": "^7.14.0" }, "dependencies": { "@babel/code-frame": { @@ -990,12 +990,12 @@ } }, "@babel/generator": { - "version": "7.13.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", - "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", + "integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", "dev": true, "requires": { - "@babel/types": "^7.13.0", + "@babel/types": "^7.14.1", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -1069,26 +1069,26 @@ } }, "@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.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", "dev": true }, "@babel/highlight": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", - "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.12.11", + "@babel/helper-validator-identifier": "^7.14.0", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.13.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", - "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", "dev": true }, "@babel/template": { @@ -1103,29 +1103,28 @@ } }, "@babel/traverse": { - "version": "7.13.15", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz", - "integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", + "integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", "dev": true, "requires": { "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.13.9", + "@babel/generator": "^7.14.0", "@babel/helper-function-name": "^7.12.13", "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.13.15", - "@babel/types": "^7.13.14", + "@babel/parser": "^7.14.0", + "@babel/types": "^7.14.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", - "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", + "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", + "@babel/helper-validator-identifier": "^7.14.0", "to-fast-properties": "^2.0.0" } }, @@ -1365,9 +1364,9 @@ "dev": true }, "@babel/types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", - "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", + "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.0", @@ -1867,12 +1866,12 @@ } }, "@babel/generator": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz", - "integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", + "integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", "dev": true, "requires": { - "@babel/types": "^7.14.0", + "@babel/types": "^7.14.1", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -1887,9 +1886,9 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.0.tgz", - "integrity": "sha512-6pXDPguA5zC40Y8oI5mqr+jEUpjMJonKvknvA+vD8CYDz5uuXEwWBK8sRAsE/t3gfb1k15AQb9RhwpscC4nUJQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.1.tgz", + "integrity": "sha512-r8rsUahG4ywm0QpGcCrLaUSOuNAISR3IZCg4Fx05Ozq31aCUrQsTLH6KPxy0N5ULoQ4Sn9qjNdGNtbPWAC6hYg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.12.13", @@ -1983,9 +1982,9 @@ } }, "@babel/parser": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz", - "integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", "dev": true }, "@babel/template": { @@ -2016,9 +2015,9 @@ } }, "@babel/types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", - "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", + "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.0", @@ -2562,9 +2561,9 @@ } }, "@babel/parser": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz", - "integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", "dev": true }, "@babel/template": { @@ -2579,9 +2578,9 @@ } }, "@babel/types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", - "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", + "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.0", @@ -2635,377 +2634,31 @@ "babel-plugin-dynamic-import-node": "^2.3.3" }, "dependencies": { - "@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==", - "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" - } - }, - "@babel/generator": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz", - "integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==", - "dev": true, - "requires": { - "@babel/types": "^7.14.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", - "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", - "dev": true, - "requires": { - "@babel/types": "^7.13.12" - } - }, - "@babel/helper-module-imports": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", - "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", - "dev": true, - "requires": { - "@babel/types": "^7.13.12" - } - }, - "@babel/helper-module-transforms": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz", - "integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.13.12", - "@babel/helper-replace-supers": "^7.13.12", - "@babel/helper-simple-access": "^7.13.12", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.14.0", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.14.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, "@babel/helper-plugin-utils": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz", - "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.13.12", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.12" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", - "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", - "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz", - "integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q==", + } + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.0.tgz", + "integrity": "sha512-EX4QePlsTaRZQmw9BsoPeyh5OCtRGIhwfLquhxGp5e32w+dyL8htOcDwamlitmNFK6xBZYlygjdye9dbd9rUlQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.14.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-simple-access": "^7.13.12", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", "dev": true - }, - "@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" - } - }, - "@babel/traverse": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", - "integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.14.0", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.14.0", - "@babel/types": "^7.14.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", - "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.0", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - } - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.0.tgz", - "integrity": "sha512-EX4QePlsTaRZQmw9BsoPeyh5OCtRGIhwfLquhxGp5e32w+dyL8htOcDwamlitmNFK6xBZYlygjdye9dbd9rUlQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.14.0", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/helper-simple-access": "^7.13.12", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@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==", - "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" - } - }, - "@babel/generator": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz", - "integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==", - "dev": true, - "requires": { - "@babel/types": "^7.14.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", - "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", - "dev": true, - "requires": { - "@babel/types": "^7.13.12" - } - }, - "@babel/helper-module-imports": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", - "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", - "dev": true, - "requires": { - "@babel/types": "^7.13.12" - } - }, - "@babel/helper-module-transforms": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz", - "integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.13.12", - "@babel/helper-replace-supers": "^7.13.12", - "@babel/helper-simple-access": "^7.13.12", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.14.0", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.14.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", - "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", - "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz", - "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.13.12", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.12" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", - "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", - "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz", - "integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q==", - "dev": true - }, - "@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" - } - }, - "@babel/traverse": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", - "integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.14.0", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.14.0", - "@babel/types": "^7.14.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", - "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.0", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } } } }, @@ -3019,211 +2672,38 @@ "@babel/helper-module-transforms": "^7.13.0", "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-validator-identifier": "^7.12.11", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", - "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", - "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", - "dev": true - } - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.0.tgz", - "integrity": "sha512-nPZdnWtXXeY7I87UZr9VlsWme3Y0cfFFE41Wbxz4bbaexAjNMInXPFUpRRUJ8NoMm0Cw+zxbqjdPmLhcjfazMw==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.14.0", - "@babel/helper-plugin-utils": "^7.13.0" - }, - "dependencies": { - "@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==", - "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" - } - }, - "@babel/generator": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz", - "integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==", - "dev": true, - "requires": { - "@babel/types": "^7.14.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", - "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", - "dev": true, - "requires": { - "@babel/types": "^7.13.12" - } - }, - "@babel/helper-module-imports": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", - "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", - "dev": true, - "requires": { - "@babel/types": "^7.13.12" - } - }, - "@babel/helper-module-transforms": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz", - "integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.13.12", - "@babel/helper-replace-supers": "^7.13.12", - "@babel/helper-simple-access": "^7.13.12", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.14.0", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.14.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", - "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", - "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz", - "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.13.12", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.12" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", - "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", - "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz", - "integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q==", - "dev": true - }, - "@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" - } - }, - "@babel/traverse": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", - "integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.14.0", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.14.0", - "@babel/types": "^7.14.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", + "dev": true }, - "@babel/types": { + "@babel/helper-validator-identifier": { "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", - "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.0", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + } + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.0.tgz", + "integrity": "sha512-nPZdnWtXXeY7I87UZr9VlsWme3Y0cfFFE41Wbxz4bbaexAjNMInXPFUpRRUJ8NoMm0Cw+zxbqjdPmLhcjfazMw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.14.0", + "@babel/helper-plugin-utils": "^7.13.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", + "dev": true } } }, @@ -3273,12 +2753,12 @@ } }, "@babel/generator": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz", - "integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", + "integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", "dev": true, "requires": { - "@babel/types": "^7.14.0", + "@babel/types": "^7.14.1", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -3366,9 +2846,9 @@ } }, "@babel/parser": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz", - "integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", "dev": true }, "@babel/template": { @@ -3399,9 +2879,9 @@ } }, "@babel/types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", - "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", + "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.0", @@ -3984,9 +3464,9 @@ } }, "@babel/preset-env": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.0.tgz", - "integrity": "sha512-GWRCdBv2whxqqaSi7bo/BEXf070G/fWFMEdCnmoRg2CZJy4GK06ovFuEjJrZhDRXYgBsYtxVbG8GUHvw+UWBkQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.1.tgz", + "integrity": "sha512-0M4yL1l7V4l+j/UHvxcdvNfLB9pPtIooHTbEhgD/6UGyh8Hy3Bm1Mj0buzjDXATCSz3JFibVdnoJZCrlUCanrQ==", "dev": true, "requires": { "@babel/compat-data": "^7.14.0", @@ -4026,7 +3506,7 @@ "@babel/plugin-transform-arrow-functions": "^7.13.0", "@babel/plugin-transform-async-to-generator": "^7.13.0", "@babel/plugin-transform-block-scoped-functions": "^7.12.13", - "@babel/plugin-transform-block-scoping": "^7.13.16", + "@babel/plugin-transform-block-scoping": "^7.14.1", "@babel/plugin-transform-classes": "^7.13.0", "@babel/plugin-transform-computed-properties": "^7.13.0", "@babel/plugin-transform-destructuring": "^7.13.17", @@ -4056,7 +3536,7 @@ "@babel/plugin-transform-unicode-escapes": "^7.12.13", "@babel/plugin-transform-unicode-regex": "^7.12.13", "@babel/preset-modules": "^0.1.4", - "@babel/types": "^7.14.0", + "@babel/types": "^7.14.1", "babel-plugin-polyfill-corejs2": "^0.2.0", "babel-plugin-polyfill-corejs3": "^0.2.0", "babel-plugin-polyfill-regenerator": "^0.2.0", @@ -4080,12 +3560,12 @@ "dev": true }, "@babel/generator": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz", - "integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", + "integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", "dev": true, "requires": { - "@babel/types": "^7.14.0", + "@babel/types": "^7.14.1", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -4194,9 +3674,9 @@ } }, "@babel/parser": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz", - "integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", "dev": true }, "@babel/plugin-proposal-async-generator-functions": { @@ -4238,9 +3718,9 @@ } }, "@babel/plugin-transform-block-scoping": { - "version": "7.13.16", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.13.16.tgz", - "integrity": "sha512-ad3PHUxGnfWF4Efd3qFuznEtZKoBp0spS+DgqzVzRPV7urEBvPLue3y2j80w4Jf2YLzZHj8TOv/Lmvdmh3b2xg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.1.tgz", + "integrity": "sha512-2mQXd0zBrwfp0O1moWIhPpEeTKDvxyHcnma3JATVP1l+CctWBuot6OJG8LQ4DnBj4ZZPSmlb/fm4mu47EOAnVA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.13.0" @@ -4298,9 +3778,9 @@ } }, "@babel/types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", - "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", + "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.0", @@ -4321,9 +3801,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001220", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001220.tgz", - "integrity": "sha512-pjC2T4DIDyGAKTL4dMvGUQaMUHRmhvPpAgNNTa14jaBWHu+bLQgvpFqElxh9L4829Fdx0PlKiMp3wnYldRtECA==", + "version": "1.0.30001221", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001221.tgz", + "integrity": "sha512-b9TOZfND3uGSLjMOrLh8XxSQ41x8mX+9MLJYDM4AAHLfaZHttrLNPrScWjVnBITRZbY5sPpCt7X85n7VSLZ+/g==", "dev": true }, "colorette": { @@ -4342,9 +3822,9 @@ } }, "electron-to-chromium": { - "version": "1.3.725", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.725.tgz", - "integrity": "sha512-2BbeAESz7kc6KBzs7WVrMc1BY5waUphk4D4DX5dSQXJhsc3tP5ZFaiyuL0AB7vUKzDYpIeYwTYlEfxyjsGUrhw==", + "version": "1.3.726", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.726.tgz", + "integrity": "sha512-dw7WmrSu/JwtACiBzth8cuKf62NKL1xVJuNvyOg0jvruN/n4NLtGYoTzciQquCPNaS2eR+BT5GrxHbslfc/w1w==", "dev": true }, "node-releases": { @@ -4500,9 +3980,9 @@ } }, "@eslint/eslintrc": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz", - "integrity": "sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.1.tgz", + "integrity": "sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -5335,9 +4815,9 @@ "integrity": "sha512-YqR6GIsum9K7Cg6wOTxwJnKP+KDOxbZ9dnQE2/M47vP0ynXyTadvwflGBukzJ/MhzrS2R6buNhFjFnVJRXJinw==" }, "@opengovsg/spcp-auth-client": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@opengovsg/spcp-auth-client/-/spcp-auth-client-1.4.6.tgz", - "integrity": "sha512-vW6kxwt7kGKuQytip1HGghibVvePgwvtPzv2n5JqqqBFHMkLed26ZrvModjrROjDJecFMydDm6zIB5xMVnHrXQ==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@opengovsg/spcp-auth-client/-/spcp-auth-client-1.4.7.tgz", + "integrity": "sha512-Elt4eg0HozU5cjzpycAKDPhp0phGvcgt0iPOuWmL3a1D6KHszKMWtbMPE64Qfz0toAaFgFshJVJh3fVIx8yncw==", "requires": { "base-64": "^1.0.0", "jsonwebtoken": "^8.3.0", @@ -5351,81 +4831,81 @@ } }, "@sentry/browser": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.3.5.tgz", - "integrity": "sha512-fjkhPR5gLCGVWhbWjEoN64hnmTvfTLRCgWmYTc9SiGchWFoFEmLqZyF2uJFyt27+qamLQ9fN58nnv4Ly2yyxqg==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.3.6.tgz", + "integrity": "sha512-l4323jxuBOArki6Wf+EHes39IEyJ2Zj/CIUaTY7GWh7CntpfHQAfFmZWQw3Ozq+ka1u8lVp25RPhb4Wng3azNA==", "requires": { - "@sentry/core": "6.3.5", - "@sentry/types": "6.3.5", - "@sentry/utils": "6.3.5", + "@sentry/core": "6.3.6", + "@sentry/types": "6.3.6", + "@sentry/utils": "6.3.6", "tslib": "^1.9.3" }, "dependencies": { "@sentry/types": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.5.tgz", - "integrity": "sha512-tY/3pkAmGYJ3F0BtwInsdt/uclNvF8aNG7XHsTPQNzk7BkNVWjCXx0sjxi6CILirl5nwNxYxVeTr2ZYAEZ/dSQ==" + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.6.tgz", + "integrity": "sha512-93cFJdJkWyCfyZeWFARSU11qnoHVOS/R2h5WIsEf+jbQmkqG2C+TXVz/19s6nHVsfDrwpvYpwALPv4/nrxfU7g==" }, "@sentry/utils": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.5.tgz", - "integrity": "sha512-kHUcZ37QYlNzz7c9LVdApITXHaNmQK7+sw/If3M/qpff1fd5XoecA8laLfcYuz+Cw5mRhVmdhPcCRM3Xi1IGXg==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.6.tgz", + "integrity": "sha512-HnYlDBf8Dq8MEv7AulH7B6R1D/2LAooVclGdjg48tSrr9g+31kmtj+SAj2WWVHP9+bp29BWaC7i5nkfKrOibWw==", "requires": { - "@sentry/types": "6.3.5", + "@sentry/types": "6.3.6", "tslib": "^1.9.3" } } } }, "@sentry/core": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.3.5.tgz", - "integrity": "sha512-VR2ibDy33mryD0mT6d9fGhKjdNzS2FSwwZPe9GvmNOjkyjly/oV91BKVoYJneCqOeq8fyj2lvkJGKuupdJNDqg==", - "requires": { - "@sentry/hub": "6.3.5", - "@sentry/minimal": "6.3.5", - "@sentry/types": "6.3.5", - "@sentry/utils": "6.3.5", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.3.6.tgz", + "integrity": "sha512-w6BRizAqh7BaiM9oeKzO6aACXwRijUPacYaVLX/OfhqCSueF9uDxpMRT7+4D/eCeDVqgJYhBJ4Vsu2NSstkk4A==", + "requires": { + "@sentry/hub": "6.3.6", + "@sentry/minimal": "6.3.6", + "@sentry/types": "6.3.6", + "@sentry/utils": "6.3.6", "tslib": "^1.9.3" }, "dependencies": { "@sentry/types": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.5.tgz", - "integrity": "sha512-tY/3pkAmGYJ3F0BtwInsdt/uclNvF8aNG7XHsTPQNzk7BkNVWjCXx0sjxi6CILirl5nwNxYxVeTr2ZYAEZ/dSQ==" + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.6.tgz", + "integrity": "sha512-93cFJdJkWyCfyZeWFARSU11qnoHVOS/R2h5WIsEf+jbQmkqG2C+TXVz/19s6nHVsfDrwpvYpwALPv4/nrxfU7g==" }, "@sentry/utils": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.5.tgz", - "integrity": "sha512-kHUcZ37QYlNzz7c9LVdApITXHaNmQK7+sw/If3M/qpff1fd5XoecA8laLfcYuz+Cw5mRhVmdhPcCRM3Xi1IGXg==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.6.tgz", + "integrity": "sha512-HnYlDBf8Dq8MEv7AulH7B6R1D/2LAooVclGdjg48tSrr9g+31kmtj+SAj2WWVHP9+bp29BWaC7i5nkfKrOibWw==", "requires": { - "@sentry/types": "6.3.5", + "@sentry/types": "6.3.6", "tslib": "^1.9.3" } } } }, "@sentry/hub": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.3.5.tgz", - "integrity": "sha512-ZYFo7VYKwdPVjuV9BDFiYn+MpANn6eZMz5QDBfZ2dugIvIVbuOyOOLx8PSa3ZXJoVTZZ7s2wD2fi/ZxKjNjZOQ==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.3.6.tgz", + "integrity": "sha512-foBZ3ilMnm9Gf9OolrAxYHK8jrA6IF72faDdJ3Al+1H27qcpnBaMdrdEp2/jzwu/dgmwuLmbBaMjEPXaGH/0JQ==", "requires": { - "@sentry/types": "6.3.5", - "@sentry/utils": "6.3.5", + "@sentry/types": "6.3.6", + "@sentry/utils": "6.3.6", "tslib": "^1.9.3" }, "dependencies": { "@sentry/types": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.5.tgz", - "integrity": "sha512-tY/3pkAmGYJ3F0BtwInsdt/uclNvF8aNG7XHsTPQNzk7BkNVWjCXx0sjxi6CILirl5nwNxYxVeTr2ZYAEZ/dSQ==" + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.6.tgz", + "integrity": "sha512-93cFJdJkWyCfyZeWFARSU11qnoHVOS/R2h5WIsEf+jbQmkqG2C+TXVz/19s6nHVsfDrwpvYpwALPv4/nrxfU7g==" }, "@sentry/utils": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.5.tgz", - "integrity": "sha512-kHUcZ37QYlNzz7c9LVdApITXHaNmQK7+sw/If3M/qpff1fd5XoecA8laLfcYuz+Cw5mRhVmdhPcCRM3Xi1IGXg==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.6.tgz", + "integrity": "sha512-HnYlDBf8Dq8MEv7AulH7B6R1D/2LAooVclGdjg48tSrr9g+31kmtj+SAj2WWVHP9+bp29BWaC7i5nkfKrOibWw==", "requires": { - "@sentry/types": "6.3.5", + "@sentry/types": "6.3.6", "tslib": "^1.9.3" } } @@ -5443,19 +4923,19 @@ } }, "@sentry/minimal": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.3.5.tgz", - "integrity": "sha512-4RqIGAU0+8iI/1sw0GYPTr4SUA88/i2+JPjFJ+qloh5ANVaNwhFPRChw+Ys9xpre8LV9JZrEsEf8AvQr4fkNbA==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.3.6.tgz", + "integrity": "sha512-uM2/dH0a6zfvI5f+vg+/mST+uTBdN6Jgpm585ipH84ckCYQwIIDRg6daqsen4S1sy/xgg1P1YyC3zdEC4G6b1Q==", "requires": { - "@sentry/hub": "6.3.5", - "@sentry/types": "6.3.5", + "@sentry/hub": "6.3.6", + "@sentry/types": "6.3.6", "tslib": "^1.9.3" }, "dependencies": { "@sentry/types": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.5.tgz", - "integrity": "sha512-tY/3pkAmGYJ3F0BtwInsdt/uclNvF8aNG7XHsTPQNzk7BkNVWjCXx0sjxi6CILirl5nwNxYxVeTr2ZYAEZ/dSQ==" + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.6.tgz", + "integrity": "sha512-93cFJdJkWyCfyZeWFARSU11qnoHVOS/R2h5WIsEf+jbQmkqG2C+TXVz/19s6nHVsfDrwpvYpwALPv4/nrxfU7g==" } } }, @@ -5600,15 +5080,18 @@ } }, "@types/bcrypt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-3.0.1.tgz", - "integrity": "sha512-SwBrq5wb6jXP0o3O3jStdPWbKpimTImfdFD/OZE3uW+jhGpds/l5wMX9lfYOTDOa5Bod2QmOgo9ln+tMp2XP/w==", - "dev": true + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "@types/bluebird": { - "version": "3.5.33", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.33.tgz", - "integrity": "sha512-ndEo1xvnYeHxm7I/5sF6tBvnsA4Tdi3zj1keRKRs12SP+2ye2A27NDJ1B6PqkfMbGAcT+mqQVqbZRIrhfOp5PQ==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.34.tgz", + "integrity": "sha512-QMc57Pf067Rr78l6f4FftvuIXPYxu0VYFRKrZk1Clv+LWy7gN2fTBiAiv68askFHEHZcTLPFd01kNlpKOiSPgQ==", "dev": true }, "@types/body-parser": { @@ -5931,9 +5414,9 @@ "dev": true }, "@types/node": { - "version": "14.14.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.43.tgz", - "integrity": "sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ==" + "version": "14.14.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.44.tgz", + "integrity": "sha512-+gaugz6Oce6ZInfI/tK4Pq5wIIkJMEJUu92RB3Eu93mtj4wjjjz9EB5mLp5s1pSsLXdC/CPut/xF20ZzAQJbTA==" }, "@types/nodemailer": { "version": "6.4.1", @@ -6145,13 +5628,13 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.0.tgz", - "integrity": "sha512-U8SP9VOs275iDXaL08Ln1Fa/wLXfj5aTr/1c0t0j6CdbOnxh+TruXu1p4I0NAvdPBQgoPjHsgKn28mOi0FzfoA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.1.tgz", + "integrity": "sha512-kVTAghWDDhsvQ602tHBc6WmQkdaYbkcTwZu+7l24jtJiYvm9l+/y/b2BZANEezxPDiX5MK2ZecE+9BFi/YJryw==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.22.0", - "@typescript-eslint/scope-manager": "4.22.0", + "@typescript-eslint/experimental-utils": "4.22.1", + "@typescript-eslint/scope-manager": "4.22.1", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "lodash": "^4.17.15", @@ -6161,43 +5644,43 @@ }, "dependencies": { "@typescript-eslint/experimental-utils": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.0.tgz", - "integrity": "sha512-xJXHHl6TuAxB5AWiVrGhvbGL8/hbiCQ8FiWwObO3r0fnvBdrbWEDy1hlvGQOAWc6qsCWuWMKdVWlLAEMpxnddg==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.1.tgz", + "integrity": "sha512-svYlHecSMCQGDO2qN1v477ax/IDQwWhc7PRBiwAdAMJE7GXk5stF4Z9R/8wbRkuX/5e9dHqbIWxjeOjckK3wLQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.22.0", - "@typescript-eslint/types": "4.22.0", - "@typescript-eslint/typescript-estree": "4.22.0", + "@typescript-eslint/scope-manager": "4.22.1", + "@typescript-eslint/types": "4.22.1", + "@typescript-eslint/typescript-estree": "4.22.1", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/scope-manager": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.0.tgz", - "integrity": "sha512-OcCO7LTdk6ukawUM40wo61WdeoA7NM/zaoq1/2cs13M7GyiF+T4rxuA4xM+6LeHWjWbss7hkGXjFDRcKD4O04Q==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.1.tgz", + "integrity": "sha512-d5bAiPBiessSmNi8Amq/RuLslvcumxLmyhf1/Xa9IuaoFJ0YtshlJKxhlbY7l2JdEk3wS0EnmnfeJWSvADOe0g==", "dev": true, "requires": { - "@typescript-eslint/types": "4.22.0", - "@typescript-eslint/visitor-keys": "4.22.0" + "@typescript-eslint/types": "4.22.1", + "@typescript-eslint/visitor-keys": "4.22.1" } }, "@typescript-eslint/types": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.0.tgz", - "integrity": "sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.1.tgz", + "integrity": "sha512-2HTkbkdAeI3OOcWbqA8hWf/7z9c6gkmnWNGz0dKSLYLWywUlkOAQ2XcjhlKLj5xBFDf8FgAOF5aQbnLRvgNbCw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.0.tgz", - "integrity": "sha512-TkIFeu5JEeSs5ze/4NID+PIcVjgoU3cUQUIZnH3Sb1cEn1lBo7StSV5bwPuJQuoxKXlzAObjYTilOEKRuhR5yg==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.1.tgz", + "integrity": "sha512-p3We0pAPacT+onSGM+sPR+M9CblVqdA9F1JEdIqRVlxK5Qth4ochXQgIyb9daBomyQKAXbygxp1aXQRV0GC79A==", "dev": true, "requires": { - "@typescript-eslint/types": "4.22.0", - "@typescript-eslint/visitor-keys": "4.22.0", + "@typescript-eslint/types": "4.22.1", + "@typescript-eslint/visitor-keys": "4.22.1", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -6206,12 +5689,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.0.tgz", - "integrity": "sha512-nnMu4F+s4o0sll6cBSsTeVsT4cwxB7zECK3dFxzEjPBii9xLpq4yqqsy/FU5zMfan6G60DKZSCXAa3sHJZrcYw==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.1.tgz", + "integrity": "sha512-WPkOrIRm+WCLZxXQHCi+WG8T2MMTUFR70rWjdWYddLT7cEfb2P4a3O/J2U1FBVsSFTocXLCoXWY6MZGejeStvQ==", "dev": true, "requires": { - "@typescript-eslint/types": "4.22.0", + "@typescript-eslint/types": "4.22.1", "eslint-visitor-keys": "^2.0.0" } }, @@ -6225,9 +5708,9 @@ } }, "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true }, "lru-cache": { @@ -6271,41 +5754,41 @@ } }, "@typescript-eslint/parser": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.22.0.tgz", - "integrity": "sha512-z/bGdBJJZJN76nvAY9DkJANYgK3nlRstRRi74WHm3jjgf2I8AglrSY+6l7ogxOmn55YJ6oKZCLLy+6PW70z15Q==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.23.0.tgz", + "integrity": "sha512-wsvjksHBMOqySy/Pi2Q6UuIuHYbgAMwLczRl4YanEPKW5KVxI9ZzDYh3B5DtcZPQTGRWFJrfcbJ6L01Leybwug==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.22.0", - "@typescript-eslint/types": "4.22.0", - "@typescript-eslint/typescript-estree": "4.22.0", + "@typescript-eslint/scope-manager": "4.23.0", + "@typescript-eslint/types": "4.23.0", + "@typescript-eslint/typescript-estree": "4.23.0", "debug": "^4.1.1" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.0.tgz", - "integrity": "sha512-OcCO7LTdk6ukawUM40wo61WdeoA7NM/zaoq1/2cs13M7GyiF+T4rxuA4xM+6LeHWjWbss7hkGXjFDRcKD4O04Q==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.23.0.tgz", + "integrity": "sha512-ZZ21PCFxPhI3n0wuqEJK9omkw51wi2bmeKJvlRZPH5YFkcawKOuRMQMnI8mH6Vo0/DoHSeZJnHiIx84LmVQY+w==", "dev": true, "requires": { - "@typescript-eslint/types": "4.22.0", - "@typescript-eslint/visitor-keys": "4.22.0" + "@typescript-eslint/types": "4.23.0", + "@typescript-eslint/visitor-keys": "4.23.0" } }, "@typescript-eslint/types": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.0.tgz", - "integrity": "sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.23.0.tgz", + "integrity": "sha512-oqkNWyG2SLS7uTWLZf6Sr7Dm02gA5yxiz1RP87tvsmDsguVATdpVguHr4HoGOcFOpCvx9vtCSCyQUGfzq28YCw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.0.tgz", - "integrity": "sha512-TkIFeu5JEeSs5ze/4NID+PIcVjgoU3cUQUIZnH3Sb1cEn1lBo7StSV5bwPuJQuoxKXlzAObjYTilOEKRuhR5yg==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.23.0.tgz", + "integrity": "sha512-5Sty6zPEVZF5fbvrZczfmLCOcby3sfrSPu30qKoY1U3mca5/jvU5cwsPb/CO6Q3ByRjixTMIVsDkqwIxCf/dMw==", "dev": true, "requires": { - "@typescript-eslint/types": "4.22.0", - "@typescript-eslint/visitor-keys": "4.22.0", + "@typescript-eslint/types": "4.23.0", + "@typescript-eslint/visitor-keys": "4.23.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -6314,12 +5797,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.0.tgz", - "integrity": "sha512-nnMu4F+s4o0sll6cBSsTeVsT4cwxB7zECK3dFxzEjPBii9xLpq4yqqsy/FU5zMfan6G60DKZSCXAa3sHJZrcYw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.23.0.tgz", + "integrity": "sha512-5PNe5cmX9pSifit0H+nPoQBXdbNzi5tOEec+3riK+ku4e3er37pKxMKDH5Ct5Y4fhWxcD4spnlYjxi9vXbSpwg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.22.0", + "@typescript-eslint/types": "4.23.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -6333,9 +5816,9 @@ } }, "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true }, "lru-cache": { @@ -6661,9 +6144,9 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "abortcontroller-polyfill": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.1.tgz", - "integrity": "sha512-yml9NiDEH4M4p0G4AcPkg8AAa4mF3nfYF28VQxaokpO67j9H7gWgmsVWJ/f1Rn+PzsnDYvzJzWIQzCqDKRvWlA==" + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz", + "integrity": "sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==" }, "accepts": { "version": "1.3.7", @@ -7233,9 +6716,9 @@ "integrity": "sha512-24q5Rh3bno7ldoyCq99d6hpnLI+PAMocdeVaaGt/5BTQMprvDwQToHfNnruqN11odCHZZIQbRBw+nZo1lTCH9g==" }, "aws-sdk": { - "version": "2.897.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.897.0.tgz", - "integrity": "sha512-GnjnZ5kgmeGe1BW+wsfRJ8Hu5mU7py/GBLXikSgtNPbMmF66yTMfND99hpS5U7m3SSaHG0qBYGVySC7Z+U1AJA==", + "version": "2.903.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.903.0.tgz", + "integrity": "sha512-BP/giYLP8QJ63Jta59kph1F76oPITxRt/wNr3BdoEs9BtshWlGKk149UaseDB4wJtI+0TER5jtzBIUBcP6E+wA==", "requires": { "buffer": "4.9.2", "events": "1.1.1", @@ -8777,9 +8260,9 @@ } }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -9128,9 +8611,9 @@ } }, "concurrently": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.0.2.tgz", - "integrity": "sha512-u+1Q0dJG5BidgUTpz9CU16yoHTt/oApFDQ3mbvHwSDgMjU7aGqy0q8ZQyaZyaNxdwRKTD872Ux3Twc6//sWA+Q==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.1.0.tgz", + "integrity": "sha512-jy+xj49pvqeKjc2TAVXRIhrgPG51eBKDZti0kZ41kaWk9iLbyWBjH6KMFpW7peOLkEymD+ZM83Lx6UEy3N/M9g==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -9160,9 +8643,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -9386,9 +8869,9 @@ } }, "convict": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/convict/-/convict-6.0.1.tgz", - "integrity": "sha512-M4YNNq5NV4/VS8JhvBSHAokwvQRL4evEuU0VFe1GNPiqnj9TAkLXpf39ImCCVZlsp3CFp04bc/kRSWPGsJGJWg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/convict/-/convict-6.1.0.tgz", + "integrity": "sha512-8dzppr6Z9URlm6P8N9NiydFRq2NWtQyf4RZOK5m0Q48fWWuKamHLXD7Qz/SiLvRXnjQcKCuHayIk9Fk51sax0w==", "requires": { "lodash.clonedeep": "^4.5.0", "yargs-parser": "^18.1.3" @@ -9591,18 +9074,18 @@ } }, "core-js": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.11.2.tgz", - "integrity": "sha512-3tfrrO1JpJSYGKnd9LKTBPqgUES/UYiCzMKeqwR1+jF16q4kD1BY2NvqkfuzXwQ6+CIWm55V9cjD7PQd+hijdw==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.12.1.tgz", + "integrity": "sha512-Ne9DKPHTObRuB09Dru5AjwKjY4cJHVGu+y5f7coGn1E9Grkc3p2iBwE9AI/nJzsE29mQF7oq+mhYYRqOMFN1Bw==", "dev": true }, "core-js-compat": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.11.1.tgz", - "integrity": "sha512-aZ0e4tmlG/aOBHj92/TuOuZwp6jFvn1WNabU5VOVixzhu5t5Ao+JZkQOPlgNXu6ynwLrwJxklT4Gw1G1VGEh+g==", + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.11.2.tgz", + "integrity": "sha512-gYhNwu7AJjecNtRrIfyoBabQ3ZG+llfPmg9BifIX8yxIpDyfNLRM73zIjINSm6z3dMdI1nwNC9C7uiy4pIC6cw==", "dev": true, "requires": { - "browserslist": "^4.16.5", + "browserslist": "^4.16.6", "semver": "7.0.0" }, "dependencies": { @@ -9620,9 +9103,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001220", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001220.tgz", - "integrity": "sha512-pjC2T4DIDyGAKTL4dMvGUQaMUHRmhvPpAgNNTa14jaBWHu+bLQgvpFqElxh9L4829Fdx0PlKiMp3wnYldRtECA==", + "version": "1.0.30001221", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001221.tgz", + "integrity": "sha512-b9TOZfND3uGSLjMOrLh8XxSQ41x8mX+9MLJYDM4AAHLfaZHttrLNPrScWjVnBITRZbY5sPpCt7X85n7VSLZ+/g==", "dev": true }, "colorette": { @@ -9632,9 +9115,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.725", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.725.tgz", - "integrity": "sha512-2BbeAESz7kc6KBzs7WVrMc1BY5waUphk4D4DX5dSQXJhsc3tP5ZFaiyuL0AB7vUKzDYpIeYwTYlEfxyjsGUrhw==", + "version": "1.3.726", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.726.tgz", + "integrity": "sha512-dw7WmrSu/JwtACiBzth8cuKf62NKL1xVJuNvyOg0jvruN/n4NLtGYoTzciQquCPNaS2eR+BT5GrxHbslfc/w1w==", "dev": true }, "node-releases": { @@ -10174,9 +9657,9 @@ } }, "date-fns": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.21.1.tgz", - "integrity": "sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA==", + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.21.3.tgz", + "integrity": "sha512-HeYdzCaFflc1i4tGbj7JKMjM4cKGYoyxwcIIkHzNgCkX8xXDNJDZXgDDVchIWpN4eQc3lH37WarduXFZJOtxfw==", "dev": true }, "dateformat": { @@ -11355,13 +10838,13 @@ } }, "eslint": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.25.0.tgz", - "integrity": "sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.26.0.tgz", + "integrity": "sha512-4R1ieRf52/izcZE7AlLy56uIHHDLT74Yzz2Iv2l6kDaYvEu9x+wMB5dZArVL8SYGXSYV2YAg70FcW5Y5nGGNIg==", "dev": true, "requires": { "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.0", + "@eslint/eslintrc": "^0.4.1", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -11409,18 +10892,18 @@ } }, "@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.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", "dev": true }, "@babel/highlight": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", - "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.12.11", + "@babel/helper-validator-identifier": "^7.14.0", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -11520,9 +11003,9 @@ } }, "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true }, "esrecurse": { @@ -16757,9 +16240,9 @@ } }, "libphonenumber-js": { - "version": "1.9.16", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.16.tgz", - "integrity": "sha512-PaHT7nTtnejZ0HHekAaA0olv6BUTKZGtKM4SCQS0yE3XjFuVo/tjePMHUAr32FKwIZfyPky1ExMUuaiBAUmV6w==" + "version": "1.9.17", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.17.tgz", + "integrity": "sha512-ElJki901OynMg1l+evooPH1VyHrECuLqpgc12z2BkK25dFU5lUKTuMHEYV2jXxvtns/PIuJax56cBeoSK7ANow==" }, "lie": { "version": "3.3.0", @@ -16776,22 +16259,22 @@ "dev": true }, "lint-staged": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.5.4.tgz", - "integrity": "sha512-EechC3DdFic/TdOPgj/RB3FicqE6932LTHCUm0Y2fsD9KGlLB+RwJl2q1IYBIvEsKzDOgn0D4gll+YxG5RsrKg==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.0.0.tgz", + "integrity": "sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw==", "dev": true, "requires": { - "chalk": "^4.1.0", + "chalk": "^4.1.1", "cli-truncate": "^2.1.0", - "commander": "^6.2.0", + "commander": "^7.2.0", "cosmiconfig": "^7.0.0", - "debug": "^4.2.0", + "debug": "^4.3.1", "dedent": "^0.7.0", "enquirer": "^2.3.6", - "execa": "^4.1.0", - "listr2": "^3.2.2", - "log-symbols": "^4.0.0", - "micromatch": "^4.0.2", + "execa": "^5.0.0", + "listr2": "^3.8.2", + "log-symbols": "^4.1.0", + "micromatch": "^4.0.4", "normalize-path": "^3.0.0", "please-upgrade-node": "^3.2.0", "string-argv": "0.3.1", @@ -16808,9 +16291,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -16833,9 +16316,9 @@ "dev": true }, "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true }, "cross-spawn": { @@ -16859,34 +16342,56 @@ } }, "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", + "integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==", "dev": true, "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", "dev": true }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -16896,12 +16401,27 @@ "path-key": "^3.0.0" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "picomatch": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "dev": true + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16947,18 +16467,18 @@ } }, "listr2": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.3.1.tgz", - "integrity": "sha512-8Zoxe7s/8nNr4bJ8bdAduHD8uJce+exmMmUWTXlq0WuUdffnH3muisHPHPFtW2vvOfohIsq7FGCaguUxN/h3Iw==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.8.2.tgz", + "integrity": "sha512-E28Fw7Zd3HQlCJKzb9a8C8M0HtFWQeucE+S8YrSrqZObuCLPRHMRrR8gNmYt65cU9orXYHwvN5agXC36lYt7VQ==", "dev": true, "requires": { - "chalk": "^4.1.0", + "chalk": "^4.1.1", "cli-truncate": "^2.1.0", "figures": "^3.2.0", "indent-string": "^4.0.0", "log-update": "^4.0.0", "p-map": "^4.0.0", - "rxjs": "^6.6.3", + "rxjs": "^6.6.7", "through": "^2.3.8", "wrap-ansi": "^7.0.0" }, @@ -16979,9 +16499,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -17015,19 +16535,10 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -17262,28 +16773,28 @@ "dev": true }, "log-symbols": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", - "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "requires": { - "chalk": "^4.0.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -17312,9 +16823,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -23374,21 +22885,23 @@ "dev": true }, "table": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz", - "integrity": "sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.0.tgz", + "integrity": "sha512-SAM+5p6V99gYiiy2gT5ArdzgM1dLDed0nkrWmG6Fry/bUS/m9x83BwpJUOf1Qj/x2qJd+thL6IkIx7qPGRxqBw==", "dev": true, "requires": { - "ajv": "^7.0.2", - "lodash": "^4.17.20", + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", - "string-width": "^4.2.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0" }, "dependencies": { "ajv": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.4.tgz", - "integrity": "sha512-xzzzaqgEQfmuhbhAoqjJ8T/1okb6gAzXn/eQRNpAN1AEUoHJTNF9xCDRTtf/s3SKldtZfa+RJeTs+BQq+eZ/sw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.3.0.tgz", + "integrity": "sha512-RYE7B5An83d7eWnDR8kbdaIFqmKCNsP16ay1hDbJEU+sa0e3H9SebskCt0Uufem6cfAVu7Col6ubcn/W+Sm8/Q==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -23416,9 +22929,9 @@ "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -24907,9 +24420,9 @@ "dev": true }, "ts-jest": { - "version": "26.5.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.5.tgz", - "integrity": "sha512-7tP4m+silwt1NHqzNRAPjW1BswnAhopTdc2K3HEkRZjF0ZG2F/e/ypVH0xiZIMfItFtD3CX0XFbwPzp9fIEUVg==", + "version": "26.5.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.6.tgz", + "integrity": "sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==", "dev": true, "requires": { "bs-logger": "0.x", @@ -25105,9 +24618,9 @@ "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" }, "twilio": { - "version": "3.61.0", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-3.61.0.tgz", - "integrity": "sha512-hSnPvxogJLC6RrAkE1p2COqO6L0TMElImYDaI4eJJAn6EpJhwpHIwulpNH1R11TsJp0f9lqT7VvwYHhVXSvrvw==", + "version": "3.62.0", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-3.62.0.tgz", + "integrity": "sha512-4UyTN/sWWKWRR3QAKnpJAeceoPoB69Vdb5gjWFZ4FonWJf0fY097IM+FewzE+iISmpMvBK62WeE1cQeCnozX7g==", "requires": { "axios": "^0.21.1", "dayjs": "^1.8.29", diff --git a/package.json b/package.json index b207f7b4dd..ab24d63420 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "5.9.0", + "version": "5.10.1", "homepage": "https://form.gov.sg", "authors": [ "FormSG " @@ -24,12 +24,12 @@ "build-frontend": "webpack --config webpack.prod.js", "build-frontend-dev": "webpack --config webpack.dev.js", "build-frontend-dev:watch": "webpack --config webpack.dev.js --watch", - "start": "node dist/backend/server.js", + "start": "node dist/backend/app/server.js", "dev": "docker-compose up --build", - "docker-dev": "npm run build-frontend-dev:watch & ts-node-dev --respawn --transpile-only --inspect=0.0.0.0 --exit-child -- src/server.ts", + "docker-dev": "npm run build-frontend-dev:watch & ts-node-dev --respawn --transpile-only --inspect=0.0.0.0 --exit-child -- src/app/server.ts", "test": "npm run test-backend && npm run test-frontend", "test-e2e-build": "npm run build-backend && npm run build-frontend-dev", - "test-run": "concurrently --success last --kill-others \"mockpass\" \"maildev\" \"node dist/backend/server.js\" \"node ./tests/mock-webhook-server.js\"", + "test-run": "concurrently --success last --kill-others \"mockpass\" \"maildev\" \"node dist/backend/app/server.js\" \"node ./tests/mock-webhook-server.js\"", "testcafe-full-env": "testcafe --skip-js-errors -c 3 chrome:headless ./tests/end-to-end --test-meta full-env=true --app \"npm run test-run\" --app-init-delay 10000", "testcafe-basic-env": "testcafe --skip-js-errors -c 3 chrome:headless ./tests/end-to-end --test-meta basic-env=true --app \"npm run test-run\" --app-init-delay 10000", "download-binary": "node tests/end-to-end/helpers/get-mongo-binary.js", @@ -43,7 +43,8 @@ "lint": "npm run lint-code && npm run lint-style && npm run lint-html", "lint-ci": "concurrently \"eslint src/ --quiet\" \"stylelint '*/**/*.css' --quiet\" \"htmlhint\" \"prettier --c './src/public/**/*.html' --ignore-path './dist/**'\"", "version": "auto-changelog -p && git add CHANGELOG.md", - "prepare": "husky install" + "prepare": "husky install", + "pre-commit": "lint-staged" }, "lint-staged": { "*.{js,ts}": "eslint --fix", @@ -62,12 +63,12 @@ "@opengovsg/formsg-sdk": "^0.8.4-beta.0", "@opengovsg/myinfo-gov-client": "=4.0.0-beta.0", "@opengovsg/ng-file-upload": "^12.2.15", - "@opengovsg/spcp-auth-client": "^1.4.6", - "@sentry/browser": "^6.3.5", + "@opengovsg/spcp-auth-client": "^1.4.7", + "@sentry/browser": "^6.3.6", "@sentry/integrations": "^6.3.5", "@stablelib/base64": "^1.0.0", "JSONStream": "^1.3.5", - "abortcontroller-polyfill": "^1.7.1", + "abortcontroller-polyfill": "^1.7.3", "angular": "~1.8.2", "angular-animate": "^1.8.2", "angular-aria": "^1.8.2", @@ -83,7 +84,7 @@ "angular-ui-bootstrap": "~2.5.6", "angular-ui-router": "~1.0.29", "aws-info": "^1.2.0", - "aws-sdk": "^2.897.0", + "aws-sdk": "^2.903.0", "axios": "^0.21.1", "bcrypt": "^5.0.1", "bluebird": "^3.5.2", @@ -95,7 +96,7 @@ "celebrate": "^14.0.0", "compression": "~1.7.2", "connect-mongo": "^4.4.1", - "convict": "^6.0.1", + "convict": "^6.1.0", "convict-format-with-validator": "^6.0.1", "cookie-parser": "~1.4.0", "css-toggle-switch": "^4.1.0", @@ -120,7 +121,7 @@ "json-stringify-safe": "^5.0.1", "jszip": "^3.6.0", "jwt-decode": "^3.1.2", - "libphonenumber-js": "^1.9.16", + "libphonenumber-js": "^1.9.17", "lodash": "^4.17.21", "moment-timezone": "0.5.33", "mongodb-uri": "^0.9.7", @@ -145,7 +146,7 @@ "toastr": "^2.1.4", "triple-beam": "^1.3.0", "tweetnacl": "^1.0.1", - "twilio": "^3.61.0", + "twilio": "^3.62.0", "ui-select": "^0.19.8", "uid-generator": "^2.0.0", "uuid": "^8.3.2", @@ -158,10 +159,10 @@ "devDependencies": { "@babel/core": "^7.14.0", "@babel/plugin-transform-runtime": "^7.13.15", - "@babel/preset-env": "^7.14.0", + "@babel/preset-env": "^7.14.1", "@opengovsg/mockpass": "^2.6.9", - "@types/bcrypt": "^3.0.1", - "@types/bluebird": "^3.5.33", + "@types/bcrypt": "^5.0.0", + "@types/bluebird": "^3.5.34", "@types/busboy": "^0.2.3", "@types/compression": "^1.7.0", "@types/convict": "^6.0.1", @@ -180,7 +181,7 @@ "@types/json-stringify-safe": "^5.0.0", "@types/mongodb": "^3.6.12", "@types/mongodb-uri": "^0.9.0", - "@types/node": "^14.14.43", + "@types/node": "^14.14.44", "@types/nodemailer": "^6.4.1", "@types/opossum": "^4.1.1", "@types/promise-retry": "^1.1.3", @@ -191,20 +192,20 @@ "@types/uid-generator": "^2.0.2", "@types/uuid": "^8.3.0", "@types/validator": "^13.1.3", - "@typescript-eslint/eslint-plugin": "^4.22.0", - "@typescript-eslint/parser": "^4.22.0", + "@typescript-eslint/eslint-plugin": "^4.22.1", + "@typescript-eslint/parser": "^4.23.0", "auto-changelog": "^2.2.1", "axios-mock-adapter": "^1.19.0", "babel-loader": "^8.2.2", - "concurrently": "^6.0.2", + "concurrently": "^6.1.0", "copy-webpack-plugin": "^6.0.2", - "core-js": "^3.11.2", + "core-js": "^3.12.1", "coveralls": "^3.1.0", "css-loader": "^2.1.1", "csv-parse": "^4.15.4", - "date-fns": "^2.21.1", + "date-fns": "^2.21.3", "env-cmd": "^10.1.0", - "eslint": "^7.25.0", + "eslint": "^7.26.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-angular": "^4.0.1", "eslint-plugin-import": "^2.22.1", @@ -220,7 +221,7 @@ "jest": "^26.6.3", "jest-extended": "^0.11.5", "jest-mock-axios": "^4.4.0", - "lint-staged": "^10.5.4", + "lint-staged": "^11.0.0", "maildev": "^1.1.0", "mini-css-extract-plugin": "^0.5.0", "mockdate": "^3.0.5", @@ -241,7 +242,7 @@ "terser-webpack-plugin": "^1.2.3", "testcafe": "^1.14.0", "ts-essentials": "^7.0.1", - "ts-jest": "^26.5.5", + "ts-jest": "^26.5.6", "ts-loader": "^7.0.5", "ts-node": "^9.1.1", "ts-node-dev": "^1.1.6", diff --git a/src/app/models/__tests__/form.server.model.spec.ts b/src/app/models/__tests__/form.server.model.spec.ts index 14c47e6932..152a660b06 100644 --- a/src/app/models/__tests__/form.server.model.spec.ts +++ b/src/app/models/__tests__/form.server.model.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { ObjectId } from 'bson-ext' -import { cloneDeep, merge, omit, orderBy, pick } from 'lodash' +import { cloneDeep, map, merge, omit, orderBy, pick } from 'lodash' import mongoose, { Types } from 'mongoose' import getFormModel, { @@ -10,12 +10,14 @@ import getFormModel, { } from 'src/app/models/form.server.model' import { BasicField, + EndPage, FormFieldWithId, IEncryptedForm, IFieldSchema, IFormSchema, ILogicSchema, IPopulatedUser, + LogicType, Permission, ResponseMode, Status, @@ -1202,6 +1204,247 @@ describe('Form Model', () => { }) }) }) + + describe('updateEndPageById', () => { + it('should update end page and return updated form when successful', async () => { + // Arrange + const formParams = merge({}, MOCK_EMAIL_FORM_PARAMS, { + admin: MOCK_ADMIN_OBJ_ID, + endPage: { + title: 'old title', + }, + }) + const form = (await Form.create(formParams)).toObject() + const updatedEndPage: EndPage = { + title: 'some new title', + paragraph: 'some description paragraph', + buttonText: 'custom button text', + } + + // Act + const actual = await Form.updateEndPageById(form._id, updatedEndPage) + + // Assert + // Should have defaults populated but also replace the endpage with the new params + expect(actual?.toObject()).toEqual({ + ...form, + lastModified: expect.any(Date), + endPage: { ...updatedEndPage }, + }) + }) + + it('should update end page with defaults when optional values are not provided', async () => { + // Arrange + const formParams = merge({}, MOCK_EMAIL_FORM_PARAMS, { + admin: MOCK_ADMIN_OBJ_ID, + }) + const form = (await Form.create(formParams)).toObject() + const updatedEndPage: EndPage = { + paragraph: 'some description paragraph', + } + + // Act + const actual = await Form.updateEndPageById(form._id, updatedEndPage) + + // Assert + // Should have defaults populated but also replace the endpage with the new params + expect(actual?.toObject()).toEqual({ + ...form, + lastModified: expect.any(Date), + endPage: { + ...updatedEndPage, + // Defaults should be populated and returned + buttonText: 'Submit another form', + title: 'Thank you for filling out the form.', + }, + }) + }) + + it('should return null when formId given is not in the database', async () => { + // Arrange + await expect(Form.countDocuments()).resolves.toEqual(0) + const updatedEndPage: EndPage = { + title: 'some new title', + paragraph: 'does not really matter', + } + + // Act + const actual = await Form.updateEndPageById( + new ObjectId().toHexString(), + updatedEndPage, + ) + + // Assert + expect(actual).toEqual(null) + await expect(Form.countDocuments()).resolves.toEqual(0) + }) + }) + + describe('updateFormLogic', () => { + const logicId1 = new ObjectId().toHexString() + const logicId2 = new ObjectId().toHexString() + + const mockExistingFormLogic = { + form_logics: [ + { + _id: logicId1, + logicType: LogicType.ShowFields, + } as ILogicSchema, + { + _id: logicId2, + logicType: LogicType.ShowFields, + } as ILogicSchema, + ], + } + + const mockUpdatedFormLogic = { + _id: logicId1, + logicType: LogicType.PreventSubmit, + } as ILogicSchema + + it('should return form upon successful update of logic when there is one logic', async () => { + // arrange + const mockExistingFormLogicSingle = { + form_logics: [ + { + _id: logicId1, + logicType: LogicType.ShowFields, + } as ILogicSchema, + ], + } + + const formParams = merge({}, MOCK_EMAIL_FORM_PARAMS, { + admin: populatedAdmin, + status: Status.Public, + responseMode: ResponseMode.Email, + ...mockExistingFormLogicSingle, + }) + const form = await Form.create(formParams) + + // act + const modifiedForm = await Form.updateFormLogic( + form._id, + logicId1, + mockUpdatedFormLogic, + ) + + // assert + // Form should be returned + expect(modifiedForm).not.toBeNull() + + // Form should have correct status, responsemode + expect(modifiedForm?.responseMode).not.toBeNull() + expect(modifiedForm?.responseMode).toEqual(ResponseMode.Email) + expect(modifiedForm?.status).not.toBeNull() + expect(modifiedForm?.status).toEqual(Status.Public) + + // Check that form logic has been updated + expect(modifiedForm?.form_logics).toBeDefined() + expect(modifiedForm?.form_logics).toHaveLength(1) + expect(modifiedForm!.form_logics![0].logicType).toEqual( + LogicType.PreventSubmit, + ) + }) + + it('should return form upon successful update of logic when there are more than one logics', async () => { + // arrange + const formParams = merge({}, MOCK_EMAIL_FORM_PARAMS, { + admin: populatedAdmin, + status: Status.Public, + responseMode: ResponseMode.Email, + ...mockExistingFormLogic, + }) + const form = await Form.create(formParams) + + // act + const modifiedForm = await Form.updateFormLogic( + form._id, + logicId1, + mockUpdatedFormLogic, + ) + + // assert + // Form should be returned + expect(modifiedForm).not.toBeNull() + + // Form should have correct status, responsemode + expect(modifiedForm?.responseMode).not.toBeNull() + expect(modifiedForm?.responseMode).toEqual(ResponseMode.Email) + expect(modifiedForm?.status).not.toBeNull() + expect(modifiedForm?.status).toEqual(Status.Public) + + // Check that first form logic has been updated but second is unchanges + expect(modifiedForm?.form_logics).toBeDefined() + expect(modifiedForm?.form_logics).toHaveLength(2) + expect(modifiedForm!.form_logics![0].logicType).toEqual( + LogicType.PreventSubmit, + ) + expect(modifiedForm!.form_logics![1].logicType).toEqual( + LogicType.ShowFields, + ) + }) + + it('should return null if formId is invalid', async () => { + // arrange + const invalidFormId = new ObjectId().toHexString() + + // act + const modifiedForm = await Form.updateFormLogic( + invalidFormId, + logicId1, + mockUpdatedFormLogic, + ) + + // assert + // should return null + expect(modifiedForm).toBeNull() + }) + + it('should return unmodified form if logicId is invalid', async () => { + // arrange + const invalidLogicId = new ObjectId().toHexString() + const mockExistingFormLogicSingle = { + form_logics: [ + { + _id: invalidLogicId, + logicType: LogicType.ShowFields, + } as ILogicSchema, + ], + } + + const formParams = merge({}, MOCK_EMAIL_FORM_PARAMS, { + admin: populatedAdmin, + status: Status.Public, + responseMode: ResponseMode.Email, + ...mockExistingFormLogicSingle, + }) + const form = await Form.create(formParams) + + // act + const modifiedForm = await Form.updateFormLogic( + form._id, + logicId1, + mockUpdatedFormLogic, + ) + + // assert + // Form should be returned + expect(modifiedForm).not.toBeNull() + + // Form should have correct status, responsemode + expect(modifiedForm?.responseMode).not.toBeNull() + expect(modifiedForm?.responseMode).toEqual(ResponseMode.Email) + expect(modifiedForm?.status).not.toBeNull() + expect(modifiedForm?.status).toEqual(Status.Public) + + // Check that form logic has not been updated and there are no new form logics introduced + expect(modifiedForm?.form_logics).toBeDefined() + expect(modifiedForm?.form_logics).toHaveLength(1) + expect(modifiedForm!.form_logics![0].logicType).toEqual( + LogicType.ShowFields, + ) + }) + }) }) describe('Methods', () => { @@ -1676,5 +1919,45 @@ describe('Form Model', () => { expect(updatedForm).toBeNull() }) }) + + describe('updateFormCollaborators', () => { + it('should return the form with an updated list of collaborators', async () => { + // Arrange + const newCollaborators = [ + { + email: `fakeuser@${MOCK_ADMIN_DOMAIN}`, + write: false, + }, + ] + + // Act + const actual = await validForm.updateFormCollaborators(newCollaborators) + + // Assert + const actualPermissionsWithoutId = map( + actual.permissionList, + (collaborator) => pick(collaborator, ['email', 'write']), + ) + expect(actualPermissionsWithoutId).toEqual(newCollaborators) + }) + + it('should return an error if validation fails', async () => { + // Arrange + const newCollaborators = [ + { + email: `fakeuser@fakeemail.com`, + write: false, + }, + ] + + // Act + const actual = validForm.updateFormCollaborators(newCollaborators) + + // Assert + await expect(actual).rejects.toBeInstanceOf( + mongoose.Error.ValidationError, + ) + }) + }) }) }) diff --git a/src/app/models/form.server.model.ts b/src/app/models/form.server.model.ts index 11bc39015a..aada0cd8fc 100644 --- a/src/app/models/form.server.model.ts +++ b/src/app/models/form.server.model.ts @@ -14,6 +14,7 @@ import { AuthType, BasicField, Colors, + EndPage, FormField, FormFieldWithId, FormLogoState, @@ -29,6 +30,7 @@ import { IFormModel, IFormSchema, IPopulatedForm, + LogicDto, LogicType, Permission, PickDuplicateForm, @@ -523,6 +525,14 @@ const compileFormModel = (db: Mongoose): IFormModel => { return this.save() } + FormDocumentSchema.methods.updateFormCollaborators = async function ( + this: IFormDocument, + updatedPermissions: Permission[], + ) { + this.permissionList = updatedPermissions + return this.save() + } + FormDocumentSchema.methods.updateFormFieldById = function ( this: IFormDocument, fieldId: string, @@ -683,6 +693,37 @@ const compileFormModel = (db: Mongoose): IFormModel => { { new: true, runValidators: true }, ).exec() } + // Updates specified form logic. + FormSchema.statics.updateFormLogic = async function ( + this: IFormModel, + formId: string, + logicId: string, + updatedLogic: LogicDto, + ): Promise { + return this.findByIdAndUpdate( + formId, + { + $set: { 'form_logics.$[object]': updatedLogic }, + }, + { + arrayFilters: [{ 'object._id': logicId }], + new: true, + runValidators: true, + }, + ).exec() + } + + FormSchema.statics.updateEndPageById = async function ( + this: IFormModel, + formId: string, + newEndPage: EndPage, + ) { + return this.findByIdAndUpdate( + formId, + { endPage: newEndPage }, + { new: true, runValidators: true }, + ).exec() + } // Hooks FormSchema.pre('validate', function (next) { diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts index c304db17d2..e83d66b790 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts @@ -1,5 +1,6 @@ import { PresignedPost } from 'aws-sdk/clients/s3' import { ObjectId } from 'bson-ext' +import { StatusCodes } from 'http-status-codes' import { assignIn, cloneDeep, merge, pick } from 'lodash' import { err, errAsync, ok, okAsync, Result } from 'neverthrow' import { PassThrough } from 'stream' @@ -48,6 +49,7 @@ import { IEncryptedSubmissionSchema, IFieldSchema, IForm, + IFormDocument, IFormSchema, ILogicSchema, IPopulatedEmailForm, @@ -7664,7 +7666,10 @@ describe('admin-form.controller', () => { MockAuthService.getFormAfterPermissionChecks.mockReturnValue( okAsync(MOCK_FORM), ) - MockAdminFormService.deleteFormLogic.mockReturnValue(okAsync(true)) + + MockAdminFormService.deleteFormLogic.mockReturnValue( + okAsync(MOCK_FORM as IFormSchema), + ) }) it('should call all services correctly when request is valid', async () => { @@ -8037,4 +8042,1021 @@ describe('admin-form.controller', () => { ) }) }) + + describe('_handleUpdateEndPage', () => { + const MOCK_USER_ID = new ObjectId().toHexString() + const MOCK_FORM_ID = new ObjectId().toHexString() + const MOCK_USER = { + _id: MOCK_USER_ID, + email: 'somerandom@example.com', + } as IPopulatedUser + const MOCK_FORM = { + admin: MOCK_USER, + _id: MOCK_FORM_ID, + endPage: { + title: 'old end page', + }, + title: 'mock end page title', + } as IPopulatedForm + + const MOCK_UPDATED_FORM = { + ...MOCK_FORM, + endPage: { + title: 'new mock end page title', + }, + } as IFormDocument + + const MOCK_UPDATED_END_PAGE = MOCK_UPDATED_FORM.endPage + + const MOCK_REQ = expressHandler.mockRequest({ + params: { + formId: MOCK_FORM_ID, + }, + body: MOCK_UPDATED_END_PAGE, + session: { + user: { + _id: MOCK_USER_ID, + }, + }, + }) + + beforeEach(() => { + // Mock various services to return expected results. + MockUserService.getPopulatedUserById.mockReturnValue(okAsync(MOCK_USER)) + MockAuthService.getFormAfterPermissionChecks.mockReturnValue( + okAsync(MOCK_FORM), + ) + MockAdminFormService.updateEndPage.mockReturnValue( + okAsync(MOCK_UPDATED_END_PAGE), + ) + }) + + it('should return 200 with updated end page', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateEndPage( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(200) + expect(mockRes.json).toHaveBeenCalledWith(MOCK_UPDATED_END_PAGE) + expect(MockAdminFormService.updateEndPage).toHaveBeenCalledWith( + MOCK_REQ.params.formId, + MOCK_REQ.body, + ) + }) + + it('should return 403 when current user does not have permissions to update the end page', async () => { + // Arrange + const expectedErrorString = 'no permissions pls' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new ForbiddenFormError(expectedErrorString)), + ) + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateEndPage( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(403) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAdminFormService.updateEndPage).not.toHaveBeenCalled() + }) + + it('should return 404 when form cannot be found', async () => { + // Arrange + const expectedErrorString = 'no form pls' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormNotFoundError(expectedErrorString)), + ) + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateEndPage( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(404) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAdminFormService.updateEndPage).not.toHaveBeenCalled() + }) + + it('should return 410 when attempting to update an archived form', async () => { + // Arrange + const expectedErrorString = 'form gone pls' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormDeletedError(expectedErrorString)), + ) + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateEndPage( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(410) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAdminFormService.updateEndPage).not.toHaveBeenCalled() + }) + + it('should return 413 when updating the end page causes the form to be too large to be saved in the database', async () => { + // Arrange + const expectedErrorString = 'payload too large' + MockAdminFormService.updateEndPage.mockReturnValueOnce( + errAsync(new DatabasePayloadSizeError(expectedErrorString)), + ) + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateEndPage( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(413) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAdminFormService.updateEndPage).toHaveBeenCalledWith( + MOCK_REQ.params.formId, + MOCK_REQ.body, + ) + }) + + it('should return 422 when DatabaseValidationError occurs', async () => { + // Arrange + const expectedErrorString = 'invalid thing' + MockAdminFormService.updateEndPage.mockReturnValueOnce( + errAsync(new DatabaseValidationError(expectedErrorString)), + ) + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateEndPage( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(422) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAdminFormService.updateEndPage).toHaveBeenCalledWith( + MOCK_REQ.params.formId, + MOCK_REQ.body, + ) + }) + + it('should return 422 when user in session cannot be retrieved from the database', async () => { + // Arrange + const expectedErrorString = 'user gone' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new MissingUserError(expectedErrorString)), + ) + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateEndPage( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(422) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAdminFormService.updateEndPage).not.toHaveBeenCalled() + }) + + it('should return 500 when database error occurs whilst retrieving user from database', async () => { + // Arrange + const expectedErrorString = 'database error' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new DatabaseError(expectedErrorString)), + ) + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateEndPage( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() + expect(MockAdminFormService.updateEndPage).not.toHaveBeenCalled() + }) + + it('should return 500 when database error occurs whilst retrieving form from database', async () => { + // Arrange + const expectedErrorString = 'database error' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new DatabaseError(expectedErrorString)), + ) + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateEndPage( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: String(MOCK_FORM_ID), + level: PermissionLevel.Write, + }, + ) + expect(MockAdminFormService.updateEndPage).not.toHaveBeenCalled() + }) + + it('should return 500 when database error occurs whilst updating end page', async () => { + // Arrange + const expectedErrorString = 'database error' + MockAdminFormService.updateEndPage.mockReturnValueOnce( + errAsync(new DatabaseError(expectedErrorString)), + ) + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateEndPage( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAdminFormService.updateEndPage).toHaveBeenCalledWith( + MOCK_REQ.params.formId, + MOCK_REQ.body, + ) + }) + }) + + describe('_handleUpdateLogic', () => { + const MOCK_USER_ID = new ObjectId().toHexString() + const MOCK_FORM_ID = new ObjectId().toHexString() + const MOCK_USER = { + _id: MOCK_USER_ID, + email: 'somerandom@example.com', + } as IPopulatedUser + const logicId = new ObjectId().toHexString() + const mockFormLogic = { + form_logics: [ + { + _id: logicId, + id: logicId, + } as ILogicSchema, + ], + } + + const mockUpdatedLogic = { + _id: logicId, + } as ILogicSchema + + const MOCK_FORM = { + admin: MOCK_USER, + _id: MOCK_FORM_ID, + title: 'mock title', + ...mockFormLogic, + } as IPopulatedForm + + const mockReq = expressHandler.mockRequest({ + params: { + formId: MOCK_FORM_ID, + logicId, + }, + session: { + user: { + _id: MOCK_USER_ID, + }, + }, + body: mockUpdatedLogic, + }) + const mockRes = expressHandler.mockResponse() + + beforeEach(() => { + MockUserService.getPopulatedUserById.mockReturnValue(okAsync(MOCK_USER)) + MockAuthService.getFormAfterPermissionChecks.mockReturnValue( + okAsync(MOCK_FORM), + ) + MockAdminFormService.updateFormLogic.mockReturnValue( + okAsync(mockUpdatedLogic), + ) + }) + + it('should call all services correctly when request is valid', async () => { + await AdminFormController._handleUpdateLogic(mockReq, mockRes, jest.fn()) + + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID, + level: PermissionLevel.Write, + }, + ) + expect(MockAdminFormService.updateFormLogic).toHaveBeenCalledWith( + MOCK_FORM, + logicId, + mockUpdatedLogic, + ) + + expect(mockRes.status).toHaveBeenCalledWith(200) + expect(mockRes.json).toHaveBeenCalledWith(mockUpdatedLogic) + }) + + it('should return 403 when user does not have permissions to update logic', async () => { + MockAuthService.getFormAfterPermissionChecks.mockReturnValue( + errAsync( + new ForbiddenFormError('not authorized to perform write operation'), + ), + ) + + await AdminFormController._handleUpdateLogic(mockReq, mockRes, jest.fn()) + + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID, + level: PermissionLevel.Write, + }, + ) + expect(MockAdminFormService.updateFormLogic).not.toHaveBeenCalled() + + expect(mockRes.status).toHaveBeenCalledWith(403) + + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'not authorized to perform write operation', + }) + }) + + it('should return 404 when logicId cannot be found', async () => { + MockAdminFormService.updateFormLogic.mockReturnValue( + errAsync(new LogicNotFoundError()), + ) + + await AdminFormController._handleUpdateLogic(mockReq, mockRes, jest.fn()) + + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID, + level: PermissionLevel.Write, + }, + ) + + expect(MockAdminFormService.updateFormLogic).toHaveBeenCalledWith( + MOCK_FORM, + logicId, + mockUpdatedLogic, + ) + + expect(mockRes.status).toHaveBeenCalledWith(404) + + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'logicId does not exist on form', + }) + }) + + it('should return 404 when form cannot be found', async () => { + MockAuthService.getFormAfterPermissionChecks.mockReturnValue( + errAsync(new FormNotFoundError()), + ) + + await AdminFormController._handleUpdateLogic(mockReq, mockRes, jest.fn()) + + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID, + level: PermissionLevel.Write, + }, + ) + expect(MockAdminFormService.updateFormLogic).not.toHaveBeenCalled() + + expect(mockRes.status).toHaveBeenCalledWith(404) + + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'Form not found', + }) + }) + + it('should return 422 when user in session cannot be retrieved from the database', async () => { + MockUserService.getPopulatedUserById.mockReturnValue( + errAsync(new MissingUserError()), + ) + + await AdminFormController._handleUpdateLogic(mockReq, mockRes, jest.fn()) + + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() + + expect(MockAdminFormService.updateFormLogic).not.toHaveBeenCalled() + + expect(mockRes.status).toHaveBeenCalledWith(422) + + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'User not found', + }) + }) + + it('should return 500 when database error occurs', async () => { + MockUserService.getPopulatedUserById.mockReturnValue( + errAsync(new DatabaseError()), + ) + + await AdminFormController._handleUpdateLogic(mockReq, mockRes, jest.fn()) + + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() + + expect(MockAdminFormService.updateFormLogic).not.toHaveBeenCalled() + + expect(mockRes.status).toHaveBeenCalledWith(500) + + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'Something went wrong. Please try again.', + }) + }) + }) + + describe('_handleUpdateCollaborators', () => { + const MOCK_USER_ID = new ObjectId().toHexString() + const MOCK_FORM_ID = new ObjectId().toHexString() + const MOCK_USER = { + _id: MOCK_USER_ID, + email: 'somerandom@example.com', + } as IPopulatedUser + const MOCK_FORM = { + admin: MOCK_USER, + _id: MOCK_FORM_ID, + } as IPopulatedForm + const MOCK_COLLABORATORS = [ + { + email: `fakeuser@test.gov.sg`, + write: false, + }, + ] + const MOCK_REQ = expressHandler.mockRequest({ + params: { + formId: MOCK_FORM_ID, + }, + body: MOCK_COLLABORATORS, + session: { + user: { + _id: MOCK_USER_ID, + }, + }, + }) + + beforeEach(() => { + // Mock various services to return expected results. + MockUserService.getPopulatedUserById.mockReturnValue(okAsync(MOCK_USER)) + MockAuthService.getFormAfterPermissionChecks.mockReturnValue( + okAsync(MOCK_FORM), + ) + }) + it('should return 200 when collaborators are updated successfully', async () => { + // Arrange + MockAdminFormService.updateFormCollaborators.mockReturnValueOnce( + okAsync(MOCK_COLLABORATORS), + ) + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.OK) + expect(mockRes.json).toBeCalledWith(MOCK_COLLABORATORS) + }) + + it('should return 403 when the user does not have sufficient permissions to update the form', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new ForbiddenFormError(ERROR_MESSAGE)), + ) + const mockRes = expressHandler.mockResponse() + const expectedResponse = { message: ERROR_MESSAGE } + + // Act + await AdminFormController._handleUpdateCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.FORBIDDEN) + expect(mockRes.json).toBeCalledWith(expectedResponse) + expect( + MockAdminFormService.updateFormCollaborators, + ).not.toHaveBeenCalled() + }) + + it('should return 404 when the form could not be found', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormNotFoundError(ERROR_MESSAGE)), + ) + const mockRes = expressHandler.mockResponse() + const expectedResponse = { message: ERROR_MESSAGE } + + // Act + await AdminFormController._handleUpdateCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.NOT_FOUND) + expect(mockRes.json).toBeCalledWith(expectedResponse) + expect( + MockAdminFormService.updateFormCollaborators, + ).not.toHaveBeenCalled() + }) + + it('should return 410 when the form has been archived', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormDeletedError(ERROR_MESSAGE)), + ) + const mockRes = expressHandler.mockResponse() + const expectedResponse = { message: ERROR_MESSAGE } + + // Act + await AdminFormController._handleUpdateCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.GONE) + expect(mockRes.json).toBeCalledWith(expectedResponse) + expect( + MockAdminFormService.updateFormCollaborators, + ).not.toHaveBeenCalled() + }) + + it('should return 422 when the session user could not be retrieved from the database', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new MissingUserError(ERROR_MESSAGE)), + ) + const expectedResponse = { message: ERROR_MESSAGE } + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.UNPROCESSABLE_ENTITY) + expect(mockRes.json).toBeCalledWith(expectedResponse) + expect( + MockAdminFormService.updateFormCollaborators, + ).not.toHaveBeenCalled() + }) + + it('should return 500 when a database error occurs', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new DatabaseError(ERROR_MESSAGE)), + ) + const expectedResponse = { message: ERROR_MESSAGE } + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController._handleUpdateCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.INTERNAL_SERVER_ERROR) + expect(mockRes.json).toBeCalledWith(expectedResponse) + expect( + MockAdminFormService.updateFormCollaborators, + ).not.toHaveBeenCalled() + }) + }) + + describe('handleGetFormCollaborators', () => { + const MOCK_FORM_ID = new ObjectId().toHexString() + const MOCK_USER_ID = new ObjectId().toHexString() + const MOCK_USER = { + _id: MOCK_USER_ID, + email: 'somerandom@example.com', + } as IPopulatedUser + const MOCK_REQ = expressHandler.mockRequest({ + params: { + formId: MOCK_FORM_ID, + }, + session: { + user: MOCK_USER, + }, + }) + + const MOCK_COLLABORATORS = [ + { + email: `fakeuser@gov.sg`, + write: false, + }, + ] + const MOCK_FORM = { + admin: MOCK_USER, + _id: MOCK_FORM_ID, + permissionList: MOCK_COLLABORATORS, + } as IPopulatedForm + + beforeEach(() => { + // Mock various services to return expected results. + MockUserService.getPopulatedUserById.mockReturnValue(okAsync(MOCK_USER)) + MockAuthService.getFormAfterPermissionChecks.mockReturnValue( + okAsync(MOCK_FORM), + ) + }) + + it('should return 200 with the collaborators when the request is successful', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.OK) + expect(mockRes.send).toBeCalledWith(MOCK_COLLABORATORS) + }) + + it('should return 403 when the user does not have sufficient permissions to retrieve collaborators', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new ForbiddenFormError(ERROR_MESSAGE)), + ) + const mockRes = expressHandler.mockResponse() + const expectedResponse = { message: ERROR_MESSAGE } + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.FORBIDDEN) + expect(mockRes.json).toBeCalledWith(expectedResponse) + }) + + it('should return 404 when the form could not be found', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormNotFoundError(ERROR_MESSAGE)), + ) + const mockRes = expressHandler.mockResponse() + const expectedResponse = { message: ERROR_MESSAGE } + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.NOT_FOUND) + expect(mockRes.json).toBeCalledWith(expectedResponse) + }) + + it('should return 410 when the form has been archived', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormDeletedError(ERROR_MESSAGE)), + ) + const mockRes = expressHandler.mockResponse() + const expectedResponse = { message: ERROR_MESSAGE } + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.GONE) + expect(mockRes.json).toBeCalledWith(expectedResponse) + }) + + it('should return 422 when the current user could not be retrieved from the database', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new MissingUserError(ERROR_MESSAGE)), + ) + const expectedResponse = { message: ERROR_MESSAGE } + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.UNPROCESSABLE_ENTITY) + expect(mockRes.json).toBeCalledWith(expectedResponse) + }) + + it('should return 500 when a database error occurs', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new DatabaseError(ERROR_MESSAGE)), + ) + const expectedResponse = { message: ERROR_MESSAGE } + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.INTERNAL_SERVER_ERROR) + expect(mockRes.json).toBeCalledWith(expectedResponse) + }) + }) + + describe('handleGetFormField', () => { + const MOCK_USER_ID = new ObjectId().toHexString() + const MOCK_FORM_ID = new ObjectId().toHexString() + const MOCK_USER = { + _id: MOCK_USER_ID, + email: 'somerandom@example.com', + } as IPopulatedUser + const MOCK_FIELDS = [ + generateDefaultField(BasicField.Rating), + generateDefaultField(BasicField.Table), + ] + const MOCK_FIELD_ID = String(MOCK_FIELDS[1]._id) + + const MOCK_FORM = { + admin: MOCK_USER, + _id: MOCK_FORM_ID, + form_fields: MOCK_FIELDS, + title: 'mock title', + } as IPopulatedForm + + const MOCK_REQ = expressHandler.mockRequest({ + params: { + formId: MOCK_FORM_ID, + fieldId: MOCK_FIELD_ID, + }, + session: { + user: { + _id: MOCK_USER_ID, + }, + }, + }) + + beforeEach(() => { + // Mock various services to return expected results. + MockUserService.getPopulatedUserById.mockReturnValue(okAsync(MOCK_USER)) + MockAuthService.getFormAfterPermissionChecks.mockReturnValue( + okAsync(MOCK_FORM), + ) + }) + + it('should return 200 when deletion is successful', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + MockAdminFormService.getFormField.mockReturnValueOnce(ok(MOCK_FIELDS[1])) + + // Act + await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(200) + expect(mockRes.json).toHaveBeenCalledWith(MOCK_FIELDS[1]) + expect(MockAdminFormService.getFormField).toHaveBeenCalledWith( + MOCK_FORM, + MOCK_FIELD_ID, + ) + }) + + it('should return 403 when current user does not have permissions to retrieve form fields', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + const expectedErrorString = 'no write permissions' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new ForbiddenFormError(expectedErrorString)), + ) + + // Act + await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(403) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAdminFormService.deleteFormField).not.toHaveBeenCalled() + }) + + it('should return 404 when field to retrieve cannot be found', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + MockAdminFormService.getFormField.mockReturnValueOnce( + err(new FieldNotFoundError('Field to retrieve not found')), + ) + + // Act + await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(404) + expect(mockRes.json).toHaveBeenCalledWith({ + message: 'Field to retrieve not found', + }) + expect(MockAdminFormService.getFormField).toHaveBeenCalledWith( + MOCK_FORM, + MOCK_FIELD_ID, + ) + }) + + it('should return 404 when form to retrieve form field for cannot be found', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + + const expectedErrorString = 'nope' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormNotFoundError(expectedErrorString)), + ) + + // Act + await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(404) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAdminFormService.getFormField).not.toHaveBeenCalled() + }) + + it('should return 410 when form to retrieve form field for is already archived', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + + const expectedErrorString = 'already deleted' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormDeletedError(expectedErrorString)), + ) + + // Act + await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(410) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAdminFormService.getFormField).not.toHaveBeenCalled() + }) + + it('should return 422 when user in session cannot be retrieved from the database', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + + const expectedErrorString = 'user not in session??!!' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new MissingUserError(expectedErrorString)), + ) + + // Act + await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(422) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() + expect(MockAdminFormService.getFormField).not.toHaveBeenCalled() + }) + + it('should return 500 when generic database error occurs during form field retrieval', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + + const expectedErrorString = 'some database error bam' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new DatabaseError(expectedErrorString)), + ) + + // Act + await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + expect(MockAdminFormService.getFormField).not.toHaveBeenCalled() + }) + }) }) diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts index ee8a3c7537..8d5a50847e 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts @@ -12,6 +12,7 @@ import getFormModel, { getEncryptedFormModel, } from 'src/app/models/form.server.model' import { + ApplicationError, DatabaseConflictError, DatabaseError, DatabasePayloadSizeError, @@ -24,6 +25,7 @@ import { EditFieldActions, VALID_UPLOAD_FILE_TYPES } from 'src/shared/constants' import { AuthType, BasicField, + EndPage, FormLogoState, FormMetaView, FormSettings, @@ -69,10 +71,14 @@ import { duplicateForm, editFormFields, getDashboardForms, + getFormField, reorderFormField, transferFormOwnership, + updateEndPage, updateForm, + updateFormCollaborators, updateFormField, + updateFormLogic, updateFormSettings, } from '../admin-form.service' import { @@ -1549,6 +1555,57 @@ describe('admin-form.service', () => { }) }) + describe('updateFormCollaborators', () => { + it('should return the list of collaborators when update is successful', async () => { + // Arrange + const newCollaborators = [ + { + email: `fakeuser@gov.sg`, + write: false, + }, + ] + const mockForm = ({ + title: 'some mock form', + updateFormCollaborators: jest + .fn() + .mockResolvedValue({ permissionList: newCollaborators }), + } as unknown) as IPopulatedForm + + // Act + const actual = await updateFormCollaborators(mockForm, newCollaborators) + + // Assert + expect(mockForm.updateFormCollaborators).toHaveBeenCalledWith( + newCollaborators, + ) + expect(actual._unsafeUnwrap()).toEqual(newCollaborators) + }) + + it('should return an application error when updating the form model fails', async () => { + // Arrange + const newCollaborators = [ + { + email: `fakeuser@gov.sg`, + write: false, + }, + ] + const mockForm = ({ + title: 'some mock form', + updateFormCollaborators: jest + .fn() + .mockRejectedValue(new DatabaseError()), + } as unknown) as IPopulatedForm + + // Act + const actual = await updateFormCollaborators(mockForm, newCollaborators) + + // Assert + expect(mockForm.updateFormCollaborators).toHaveBeenCalledWith( + newCollaborators, + ) + expect(actual._unsafeUnwrapErr()).toBeInstanceOf(ApplicationError) + }) + }) describe('deleteFormField', () => { let deleteSpy: jest.SpyInstance @@ -1625,4 +1682,223 @@ describe('admin-form.service', () => { ) }) }) + + describe('updateEndPage', () => { + const updateSpy = jest.spyOn(FormModel, 'updateEndPageById') + const MOCK_FORM_ID = new ObjectId().toHexString() + const MOCK_NEW_END_PAGE: EndPage = { + title: 'expected end page title', + buttonLink: 'https://some-button-link.example.com', + buttonText: 'expected button text', + paragraph: 'some paragraph', + } + + it('should return updated end page when update is successful', async () => { + // Arrange + const mockUpdatedForm = { + endPage: MOCK_NEW_END_PAGE, + } as IFormDocument + updateSpy.mockResolvedValueOnce(mockUpdatedForm) + + // Act + const actual = await updateEndPage(MOCK_FORM_ID, MOCK_NEW_END_PAGE) + + // Assert + expect(actual._unsafeUnwrap()).toEqual(MOCK_NEW_END_PAGE) + }) + + it('should return FormNotFoundError when form cannot be found', async () => { + // Arrange + updateSpy.mockResolvedValueOnce(null) + + // Act + const actual = await updateEndPage(MOCK_FORM_ID, MOCK_NEW_END_PAGE) + + // Assert + expect(actual._unsafeUnwrapErr()).toEqual(new FormNotFoundError()) + }) + + it('should return DatabaseError when database model update throws an error', async () => { + // Arrange + const expectedErrorMsg = 'some error' + updateSpy.mockRejectedValueOnce(new Error(expectedErrorMsg)) + + // Act + const actual = await updateEndPage(MOCK_FORM_ID, MOCK_NEW_END_PAGE) + + // Assert + const actualError = actual._unsafeUnwrapErr() + expect(actualError).toBeInstanceOf(DatabaseError) + expect(actualError.message).toIncludeMultiple([ + expectedErrorMsg, + 'Please refresh and try again.', + ]) + }) + }) + + describe('updateFormLogic', () => { + const logicId1 = new ObjectId() + const logicId2 = new ObjectId() + const mockEmailFormId = new ObjectId() + const mockEncryptFormId = new ObjectId() + + const mockFormLogicOld = { + form_logics: [ + { + _id: logicId1, + logicType: 'showFields', + } as ILogicSchema, + { + _id: logicId2, + logicType: 'showFields', + } as ILogicSchema, + ], + } + + const updatedLogic = { + _id: logicId1, + logicType: 'preventSubmit', + } as ILogicSchema + + const mockFormLogicUpdated = { + form_logics: [ + { + _id: logicId1, + logicType: 'preventSubmit', + } as ILogicSchema, + { + _id: logicId2, + logicType: 'showFields', + } as ILogicSchema, + ], + } + + const UPDATE_SPY = jest.spyOn(FormModel, 'updateFormLogic') + + let mockEmailForm: IPopulatedForm, + mockEncryptForm: IPopulatedForm, + mockEmailFormUpdated: IPopulatedForm, + mockEncryptFormUpdated: IPopulatedForm + + beforeEach(() => { + mockEmailForm = ({ + _id: mockEmailFormId, + status: Status.Public, + responseMode: ResponseMode.Email, + ...mockFormLogicOld, + } as unknown) as IPopulatedForm + mockEncryptForm = ({ + _id: mockEncryptFormId, + status: Status.Public, + responseMode: ResponseMode.Encrypt, + ...mockFormLogicOld, + } as unknown) as IPopulatedForm + mockEmailFormUpdated = ({ + ...mockEmailForm, + ...mockFormLogicUpdated, + } as unknown) as IPopulatedForm + mockEncryptFormUpdated = ({ + ...mockEncryptForm, + ...mockFormLogicUpdated, + } as unknown) as IPopulatedForm + }) + + it('should return ok(updated logic) on successful form logic update for email mode form', async () => { + // Arrange + UPDATE_SPY.mockResolvedValue(mockEmailFormUpdated as IFormSchema) + + // Act + const actualResult = await updateFormLogic( + mockEmailForm, + logicId1.toHexString(), + updatedLogic, + ) + + // Assert + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(updatedLogic) + + expect(UPDATE_SPY).toHaveBeenCalledWith( + mockEmailForm._id.toHexString(), + logicId1.toHexString(), + updatedLogic, + ) + }) + + it('should return ok(updated logic) on successful form logic update for encrypt mode form', async () => { + // Arrange + UPDATE_SPY.mockResolvedValue(mockEncryptFormUpdated as IFormSchema) + + // Act + const actualResult = await updateFormLogic( + mockEncryptForm, + logicId1.toHexString(), + updatedLogic, + ) + + // Assert + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(updatedLogic) + + expect(UPDATE_SPY).toHaveBeenCalledWith( + mockEncryptFormId.toHexString(), + logicId1.toHexString(), + updatedLogic, + ) + }) + + it('should return LogicNotFoundError if logic does not exist on form', async () => { + // Act + const wrongLogicId = new ObjectId().toHexString() + const actualResult = await updateFormLogic( + mockEmailForm, + wrongLogicId, + updatedLogic, + ) + + // Assert + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual(new LogicNotFoundError()) + expect(UPDATE_SPY).not.toHaveBeenCalled() + }) + }) + + describe('getFormField', () => { + it('should return the form field when retrieval is successful', async () => { + // Arrange + const MOCK_FIELD = generateDefaultField(BasicField.Image) + const MOCK_FORM = { + title: 'some mock form', + // Append created field to end of form_fields. + form_fields: [MOCK_FIELD], + _id: new ObjectId(), + } as IFormSchema + + // Act + const actual = await getFormField(MOCK_FORM, String(MOCK_FIELD._id)) + + // Assert + expect(actual._unsafeUnwrap()).toEqual(MOCK_FIELD) + }) + + it("should return FieldNotFoundError when the fieldId does not exist in the form's fields", async () => { + // Arrange + const MOCK_ID = new ObjectId().toHexString() + const MOCK_FORM = ({ + title: 'some mock form', + // Append created field to end of form_fields. + form_fields: [], + _id: new ObjectId(), + } as unknown) as IFormSchema + const expectedError = new FieldNotFoundError( + `Attempted to retrieve field ${MOCK_ID} from ${MOCK_FORM._id} but field was not present`, + ) + + // Act + const actual = await getFormField(MOCK_FORM, MOCK_ID) + + // Assert + expect(actual._unsafeUnwrapErr()).toEqual(expectedError) + }) + }) }) diff --git a/src/app/modules/form/admin-form/admin-form.controller.ts b/src/app/modules/form/admin-form/admin-form.controller.ts index 8114fcd208..5123d2e08a 100644 --- a/src/app/modules/form/admin-form/admin-form.controller.ts +++ b/src/app/modules/form/admin-form/admin-form.controller.ts @@ -14,15 +14,22 @@ import { FormMetaView, FormSettings, IForm, + IFormDocument, IPopulatedForm, + LogicConditionState, + LogicDto, + LogicIfValue, + LogicType, ResponseMode, } from '../../../../types' import { EncryptSubmissionDto, + EndPageUpdateDto, ErrorDto, FieldCreateDto, FieldUpdateDto, FormFieldDto, + PermissionsUpdateDto, SettingsUpdateDto, } from '../../../../types/api' import { createLoggerWithLabel } from '../../../config/logger' @@ -233,6 +240,54 @@ export const handleGetAdminForm: RequestHandler<{ formId: string }> = ( ) } +/** + * Handler for GET /api/v3/admin/forms/:formId/collaborators + * @security session + * + * @returns 200 with collaborators + * @returns 403 when current user does not have read permissions for the form + * @returns 404 when form cannot be found + * @returns 410 when retrieving collaborators for an archived form + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ +export const handleGetFormCollaborators: RequestHandler< + { formId: string }, + PermissionsUpdateDto | ErrorDto +> = (req, res) => { + const { formId } = req.params + const sessionUserId = (req.session as Express.AuthedSession).user._id + + return ( + // Step 1: Retrieve currently logged in user. + UserService.getPopulatedUserById(sessionUserId) + .andThen((user) => + // Step 2: Check whether user has read permissions to form + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Read, + }), + ) + .map(({ permissionList }) => + res.status(StatusCodes.OK).send(permissionList), + ) + .mapErr((error) => { + logger.error({ + message: 'Error retrieving form collaborators', + meta: { + action: 'handleGetFormCollaborators', + ...createReqMeta(req), + }, + error, + }) + + const { statusCode, errorMessage } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) + }) + ) +} + /** * Handler for GET /:formId/adminform/preview. * @security session @@ -1730,6 +1785,115 @@ export const handleReorderFormField = [ _handleReorderFormField, ] as RequestHandler[] +/** + * NOTE: Exported for testing. + * Private handler for PUT /forms/:formId/logic/:logicId + * @precondition Must be preceded by request validation + * @security session + * + * @returns 200 with success message and updated logic object when successfully updated + * @returns 403 when user does not have permissions to update logic + * @returns 404 when form cannot be found + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ +export const _handleUpdateLogic: RequestHandler< + { formId: string; logicId: string }, + LogicDto | ErrorDto, + LogicDto +> = (req, res) => { + const { formId, logicId } = req.params + const updatedLogic = { ...req.body } + const sessionUserId = (req.session as Express.AuthedSession).user._id + + // Step 1: Retrieve currently logged in user. + return ( + UserService.getPopulatedUserById(sessionUserId) + .andThen((user) => + // Step 2: Retrieve form with write permission check. + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Write, + }), + ) + // Step 3: Update form logic + .andThen((retrievedForm) => + AdminFormService.updateFormLogic(retrievedForm, logicId, updatedLogic), + ) + .map((updatedLogic) => res.status(StatusCodes.OK).json(updatedLogic)) + .mapErr((error) => { + logger.error({ + message: 'Error occurred when updating form logic', + meta: { + action: 'handleUpdateLogic', + ...createReqMeta(req), + userId: sessionUserId, + formId, + logicId, + updatedLogic, + }, + error, + }) + const { errorMessage, statusCode } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) + }) + ) +} + +/** + * Handler for PUT /forms/:formId/logic/:logicId + */ +export const handleUpdateLogic = [ + celebrate( + { + [Segments.BODY]: Joi.object({ + // Ensures given logic is same as accessed logic + _id: Joi.string().valid(Joi.ref('$params.logicId')).required(), + logicType: Joi.string() + .valid(...Object.values(LogicType)) + .required(), + conditions: Joi.array() + .items( + Joi.object({ + field: Joi.string().required(), + state: Joi.string() + .valid(...Object.values(LogicConditionState)) + .required(), + value: Joi.alternatives() + .try( + Joi.number(), + Joi.string(), + Joi.array().items(Joi.string()), + Joi.array().items(Joi.number()), + ) + .required(), + ifValueType: Joi.string() + .valid(...Object.values(LogicIfValue)) + .required(), + }).unknown(true), + ) + .required(), + show: Joi.alternatives().conditional('logicType', { + is: LogicType.ShowFields, + then: Joi.array().items(Joi.string()).required(), + }), + preventSubmitMessage: Joi.alternatives().conditional('logicType', { + is: LogicType.PreventSubmit, + then: Joi.string().required(), + }), + // Allow other field related key-values to be provided and let the model + // layer handle the validation. + }).unknown(true), + }, + undefined, + // Required so req.body can be validated against values in req.params. + // See https://github.com/arb/celebrate#celebrateschema-joioptions-opts. + { reqContext: true }, + ), + _handleUpdateLogic, +] as RequestHandler[] + /** * Handler for DELETE /forms/:formId/fields/:fieldId * @security session @@ -1780,3 +1944,200 @@ export const handleDeleteFormField: RequestHandler< }) ) } + +/** + * NOTE: Exported for testing. + * Private handler for PUT /forms/:formId/end-page + * @precondition Must be preceded by request validation + * @security session + * + * @returns 200 with updated end page + * @returns 403 when current user does not have permissions to create a form field + * @returns 404 when form cannot be found + * @returns 410 when updating the end page for an archived form + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ +export const _handleUpdateEndPage: RequestHandler< + { formId: string }, + IFormDocument['endPage'] | ErrorDto, + EndPageUpdateDto +> = (req, res) => { + const { formId } = req.params + const sessionUserId = (req.session as Express.AuthedSession).user._id + + // Step 1: Retrieve currently logged in user. + return ( + UserService.getPopulatedUserById(sessionUserId) + .andThen((user) => + // Step 2: Retrieve form with write permission check. + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Write, + }), + ) + // Step 3: User has permissions, proceed to allow updating of end page + .andThen(() => AdminFormService.updateEndPage(formId, req.body)) + .map((updatedEndPage) => res.status(StatusCodes.OK).json(updatedEndPage)) + .mapErr((error) => { + logger.error({ + message: 'Error occurred when updating end page', + meta: { + action: '_handleUpdateEndPage', + ...createReqMeta(req), + userId: sessionUserId, + formId, + body: req.body, + }, + error, + }) + const { errorMessage, statusCode } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) + }) + ) +} + +/** + * Handler for PUT /forms/:formId/end-page + */ +export const handleUpdateEndPage = [ + celebrate({ + [Segments.BODY]: Joi.object({ + title: Joi.string(), + paragraph: Joi.string().allow(''), + buttonLink: Joi.string().uri().allow(''), + buttonText: Joi.string().allow(''), + // TODO(#1895): Remove when deprecated `buttons` key is removed from all forms in the database + }).unknown(true), + }), + _handleUpdateEndPage, +] as RequestHandler[] + +/** + * Handler for GET /admin/forms/:formId/fields/:fieldId + * @security session + * + * @returns 200 with form field when retrieval is successful + * @returns 403 when current user does not have permissions to retrieve form field + * @returns 404 when form cannot be found + * @returns 404 when form field cannot be found + * @returns 410 when retrieving form field of an archived form + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ +export const handleGetFormField: RequestHandler< + { + formId: string + fieldId: string + }, + ErrorDto | FormFieldDto +> = (req, res) => { + const { formId, fieldId } = req.params + const sessionUserId = (req.session as Express.AuthedSession).user._id + + return ( + // Step 1: Retrieve currently logged in user. + UserService.getPopulatedUserById(sessionUserId) + .andThen((user) => + // Step 2: Retrieve form with read permission check. + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Read, + }), + ) + .andThen((form) => AdminFormService.getFormField(form, fieldId)) + .map((formField) => res.status(StatusCodes.OK).json(formField)) + .mapErr((error) => { + logger.error({ + message: 'Error occurred when retrieving form field', + meta: { + action: 'handleGetFormField', + ...createReqMeta(req), + userId: sessionUserId, + formId, + fieldId, + }, + error, + }) + const { errorMessage, statusCode } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) + }) + ) +} + +/** + * NOTE: Exported for testing. + * Private handler for PUT /api/v3/admin/forms/:formId/collaborators + * @precondition Must be preceded by request validation + * @security session + * + * @returns 200 with updated collaborators and permissions + * @returns 403 when current user does not havße permissions to update the collaborators + * @returns 404 when form cannot be found + * @returns 410 when updating collaborators for an archived form + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ +export const _handleUpdateCollaborators: RequestHandler< + { formId: string }, + PermissionsUpdateDto | ErrorDto, + PermissionsUpdateDto +> = (req, res) => { + const { formId } = req.params + const sessionUserId = (req.session as Express.AuthedSession).user._id + // Step 1: Get the form after permission checks + return ( + UserService.getPopulatedUserById(sessionUserId) + .andThen((user) => + // Step 2: Retrieve form with write permission check. + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Write, + }), + ) + // Step 2: Update the form collaborators + .andThen((form) => + AdminFormService.updateFormCollaborators(form, req.body), + ) + .map((updatedCollaborators) => + res.status(StatusCodes.OK).json(updatedCollaborators), + ) + .mapErr((error) => { + logger.error({ + message: 'Error occurred when updating collaborators', + meta: { + action: '_handleUpdateCollaborators', + ...createReqMeta(req), + userId: sessionUserId, + formId, + formCollaborators: req.body, + }, + error, + }) + const { errorMessage, statusCode } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) + }) + ) +} + +/** + * Handler for PUT /api/v3/admin/forms/:formId/collaborators + */ +export const handleUpdateCollaborators = [ + celebrate({ + [Segments.BODY]: Joi.array().items( + Joi.object({ + email: Joi.string() + .required() + .email() + .message('Please enter a valid email'), + write: Joi.bool().optional(), + _id: Joi.string().optional(), + }), + ), + }), + _handleUpdateCollaborators, +] as RequestHandler[] diff --git a/src/app/modules/form/admin-form/admin-form.service.ts b/src/app/modules/form/admin-form/admin-form.service.ts index df930ed054..8ac0b05401 100644 --- a/src/app/modules/form/admin-form/admin-form.service.ts +++ b/src/app/modules/form/admin-form/admin-form.service.ts @@ -1,7 +1,7 @@ import { PresignedPost } from 'aws-sdk/clients/s3' import { assignIn, last, omit } from 'lodash' import mongoose from 'mongoose' -import { errAsync, okAsync, ResultAsync } from 'neverthrow' +import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow' import { Except, Merge } from 'type-fest' import { @@ -17,10 +17,14 @@ import { IForm, IFormDocument, IFormSchema, + ILogicSchema, IPopulatedForm, IUserSchema, + LogicDto, + Permission, } from '../../../../types' import { + EndPageUpdateDto, FieldCreateDto, FieldUpdateDto, SettingsUpdateDto, @@ -48,7 +52,7 @@ import { TransferOwnershipError, } from '../form.errors' import { getFormModelByResponseMode } from '../form.service' -import { getFormFieldById } from '../form.utils' +import { getFormFieldById, getLogicById } from '../form.utils' import { PRESIGNED_POST_EXPIRY_SECS } from './admin-form.constants' import { @@ -636,6 +640,35 @@ export const updateForm = ( }) } +/** + * Updates the collaborators of a given form + * @param form the form to update collaborators fo + * @param updatedCollaborators the new list of collaborators + * + * @returns ok(collaborators) if form updates successfully + * @returns err(PossibleDatabaseError) if any database errors occurs + */ +export const updateFormCollaborators = ( + form: IPopulatedForm, + updatedCollaborators: Permission[], +): ResultAsync => { + return ResultAsync.fromPromise( + form.updateFormCollaborators(updatedCollaborators), + (error) => { + logger.error({ + message: 'Error encountered while updating form collaborators', + meta: { + action: 'updateFormCollaborators', + formId: form._id, + }, + error, + }) + + return transformMongoError(error) + }, + ).andThen(({ permissionList }) => okAsync(permissionList)) +} + /** * Updates form settings. * @param originalForm The original form to update settings for @@ -695,7 +728,10 @@ export const updateFormSettings = ( export const deleteFormLogic = ( form: IPopulatedForm, logicId: string, -): ResultAsync => { +): ResultAsync< + IFormSchema, + DatabaseError | LogicNotFoundError | FormNotFoundError +> => { // First check if specified logic exists if (!form.form_logics.some((logic) => logic.id === logicId)) { logger.error({ @@ -733,6 +769,64 @@ export const deleteFormLogic = ( }) } +/** + * Updates form logic. + * @param form The original form to update logic in + * @param logicId the logicId to update + * @param updatedLogic Object containing the updated logic + * @returns ok(updated logic dto) on success + * @returns err(database errors) if db error is thrown during logic update + * @returns err(LogicNotFoundError) if logicId does not exist on form + */ +export const updateFormLogic = ( + form: IPopulatedForm, + logicId: string, + updatedLogic: LogicDto, +): ResultAsync< + ILogicSchema, + DatabaseError | LogicNotFoundError | FormNotFoundError +> => { + // First check if specified logic exists + if (!form.form_logics.some((logic) => logic._id.toHexString() === logicId)) { + logger.error({ + message: 'Error occurred - logicId to be updated does not exist', + meta: { + action: 'updateFormLogic', + formId: form._id, + logicId, + }, + }) + return errAsync(new LogicNotFoundError()) + } + + // Update specified logic + return ResultAsync.fromPromise( + FormModel.updateFormLogic(form._id.toHexString(), logicId, updatedLogic), + (error) => { + logger.error({ + message: 'Error occurred when updating form logic', + meta: { + action: 'updateFormLogic', + formId: form._id, + logicId, + updatedLogic, + }, + error, + }) + return transformMongoError(error) + }, + // On success, return updated form logic + ).andThen((updatedForm) => { + if (!updatedForm) { + return errAsync(new FormNotFoundError()) + } + const updatedLogic = getLogicById(updatedForm.form_logics, logicId) + return updatedLogic + ? okAsync(updatedLogic) + : errAsync(new LogicNotFoundError()) // Possible race condition if logic gets deleted after the initial logicId check but before the db update + }) +} + /** * Deletes a form field from the given form. * @param form The form to delete the specified form field for @@ -779,3 +873,61 @@ export const deleteFormField = ( return okAsync(updatedForm) }) } + +/** + * Update the end page of the given form + * @param formId the id of the form to update the end page for + * @param newEndPage the new end page object to replace the current one + * @returns ok(updated end page object) when update is successful + * @returns err(FormNotFoundError) if form cannot be found + * @returns err(PossibleDatabaseError) if endpage update fails + */ +export const updateEndPage = ( + formId: string, + newEndPage: EndPageUpdateDto, +): ResultAsync< + IFormDocument['endPage'], + PossibleDatabaseError | FormNotFoundError +> => { + return ResultAsync.fromPromise( + FormModel.updateEndPageById(formId, newEndPage), + (error) => { + logger.error({ + message: 'Error occurred when updating form end page', + meta: { + action: 'updateEndPage', + formId, + newEndPage, + }, + error, + }) + return transformMongoError(error) + }, + ).andThen((updatedForm) => { + if (!updatedForm) { + return errAsync(new FormNotFoundError()) + } + return okAsync(updatedForm.endPage) + }) +} + +/** + * Retrieves a form field from the given form. + * @param form The form to retrieve the specified form field for + * @param fieldId the id of the form field + * @returns ok(form field) on success + * @returns err(FieldNotFoundError) if the fieldId does not exist in form's fields + */ +export const getFormField = ( + form: IPopulatedForm, + fieldId: string, +): Result => { + const formField = getFormFieldById(form.form_fields, fieldId) + if (!formField) + return err( + new FieldNotFoundError( + `Attempted to retrieve field ${fieldId} from ${form._id} but field was not present`, + ), + ) + return ok(formField) +} diff --git a/src/app/modules/form/form.errors.ts b/src/app/modules/form/form.errors.ts index 38b9382d6e..2ef6c2f07c 100644 --- a/src/app/modules/form/form.errors.ts +++ b/src/app/modules/form/form.errors.ts @@ -81,7 +81,7 @@ export class AuthTypeMismatchError extends ApplicationError { export class FormAuthNoEsrvcIdError extends ApplicationError { constructor(formId: string) { super( - `Attempted to validate form ${formId} whhich did not have an eServiceId`, + `Attempted to validate form ${formId} which did not have an eServiceId`, ) } } diff --git a/src/app/modules/form/form.utils.ts b/src/app/modules/form/form.utils.ts index f914221f31..e80a70022c 100644 --- a/src/app/modules/form/form.utils.ts +++ b/src/app/modules/form/form.utils.ts @@ -2,6 +2,7 @@ import { IEncryptedFormSchema, IFieldSchema, IFormSchema, + ILogicSchema, IPopulatedEmailForm, IPopulatedForm, Permission, @@ -99,3 +100,24 @@ export const getFormFieldById = ( return formFields.find((f) => fieldId === String(f._id)) ?? null } + +/** + * Finds and returns form logic in given form by its id + * @param form_logics the logics to search from + * @param logicId the id of the logic to retrieve + * @returns the logic if found, `null` otherwise + */ +export const getLogicById = ( + form_logics: IFormSchema['form_logics'], + logicId: ILogicSchema['_id'], +): ILogicSchema | null => { + if (!form_logics) { + return null + } + + if (isMongooseDocumentArray(form_logics)) { + return form_logics.id(logicId) + } + + return form_logics.find((logic) => logicId === String(logic._id)) ?? null +} diff --git a/src/app/modules/submission/email-submission/email-submission.controller.ts b/src/app/modules/submission/email-submission/email-submission.controller.ts index e6040f9a30..71ca5a381b 100644 --- a/src/app/modules/submission/email-submission/email-submission.controller.ts +++ b/src/app/modules/submission/email-submission/email-submission.controller.ts @@ -50,8 +50,6 @@ const submitEmailModeForm: RequestHandler< return ( // Retrieve form FormService.retrieveFullFormById(formId) - .andThen((form) => EmailSubmissionService.checkFormIsEmailMode(form)) - // NOTE: This is on the top most level because errors are reported together for the first two .mapErr((error) => { logger.error({ message: 'Error while retrieving form from database', @@ -60,6 +58,16 @@ const submitEmailModeForm: RequestHandler< }) return error }) + .andThen((form) => + EmailSubmissionService.checkFormIsEmailMode(form).mapErr((error) => { + logger.warn({ + message: 'Attempt to submit non-email-mode form', + meta: logMeta, + error, + }) + return error + }), + ) .andThen((form) => // Check that form is public // If it is, pass through and return the original form diff --git a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.form.routes.spec.ts b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.form.routes.spec.ts index bf9d51d15d..9f46dc0d82 100644 --- a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.form.routes.spec.ts +++ b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.form.routes.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { ObjectId } from 'bson-ext' import mongoose from 'mongoose' -import { errAsync } from 'neverthrow' +import { err, errAsync } from 'neverthrow' import supertest, { Session } from 'supertest-session' import getFormModel, { @@ -13,7 +13,7 @@ import { DatabaseError, DatabasePayloadSizeError, } from 'src/app/modules/core/core.errors' -import { IUserSchema, ResponseMode, Status } from 'src/types' +import { BasicField, IUserSchema, ResponseMode, Status } from 'src/types' import { createAuthedSession, @@ -21,6 +21,7 @@ import { } from 'tests/integration/helpers/express-auth' import { setupApp } from 'tests/integration/helpers/express-setup' import { buildCelebrateError } from 'tests/unit/backend/helpers/celebrate' +import { generateDefaultField } from 'tests/unit/backend/helpers/generate-form-data' import dbHandler from 'tests/unit/backend/helpers/jest-db' import { jsonParseStringify } from 'tests/unit/backend/helpers/serialize-data' @@ -1274,4 +1275,158 @@ describe('admin-form.form.routes', () => { expect(response.body).toEqual({ message: 'User not found' }) }) }) + + describe('GET /admin/forms/:formId/fields/', () => { + it('should return 200 with success message when form field is successfully retrieved', async () => { + // Arrange + const MOCK_FIELD = generateDefaultField(BasicField.Rating) + const MOCK_FORM = await EmailFormModel.create({ + title: 'Form to retrieve', + emails: [defaultUser.email], + admin: defaultUser._id, + form_fields: [MOCK_FIELD], + }) + + // Act + const response = await request.get( + `/admin/forms/${MOCK_FORM._id}/fields/${MOCK_FIELD._id}`, + ) + + // Assert + expect(response.status).toEqual(200) + expect(response.body).toEqual(jsonParseStringify(MOCK_FIELD)) + }) + + it('should return 403 when user does not have permissions to retrieve form field', async () => { + // Arrange + // Create separate user + const collabUser = ( + await dbHandler.insertFormCollectionReqs({ + userId: new ObjectId(), + mailName: 'collab-user', + shortName: 'collabUser', + }) + ).user + const MOCK_FIELD = generateDefaultField(BasicField.Rating) + const MOCK_FORM = await EncryptFormModel.create({ + title: 'form that user has no read access to', + admin: collabUser._id, + publicKey: 'some random key', + }) + + // Act + const response = await request.get( + `/admin/forms/${MOCK_FORM._id}/fields/${MOCK_FIELD._id}`, + ) + + // Assert + expect(response.status).toEqual(403) + expect(response.body).toEqual({ + message: `User ${defaultUser.email} not authorized to perform read operation on Form ${MOCK_FORM._id} with title: ${MOCK_FORM.title}.`, + }) + }) + + it('should return 404 when form to retrieve cannot be found', async () => { + // Arrange + const MOCK_FIELD = generateDefaultField(BasicField.Rating) + const invalidFormId = new ObjectId().toHexString() + + // Act + const response = await request.get( + `/admin/forms/${invalidFormId}/fields/${MOCK_FIELD._id}`, + ) + + // Assert + expect(response.status).toEqual(404) + expect(response.body).toEqual({ message: 'Form not found' }) + }) + + it('should return 404 when form field to retrieve cannot be found', async () => { + // Arrange + const MOCK_FIELD = generateDefaultField(BasicField.Rating) + const MOCK_FORM = await EmailFormModel.create({ + title: 'Form to retrieve', + emails: [defaultUser.email], + admin: defaultUser._id, + form_fields: [], + }) + + // Act + const response = await request.get( + `/admin/forms/${MOCK_FORM._id}/fields/${MOCK_FIELD._id}`, + ) + + // Assert + expect(response.status).toEqual(404) + expect(response.body).toEqual({ + message: `Attempted to retrieve field ${MOCK_FIELD._id} from ${MOCK_FORM._id} but field was not present`, + }) + }) + + it('should return 410 when form is already archived', async () => { + // Arrange + const MOCK_FIELD = generateDefaultField(BasicField.Rating) + const archivedForm = await EmailFormModel.create({ + title: 'Form already archived', + emails: [defaultUser.email], + admin: defaultUser._id, + status: Status.Archived, + }) + + // Act + const response = await request.get( + `/admin/forms/${archivedForm._id}/fields/${MOCK_FIELD._id}`, + ) + + // Assert + expect(response.status).toEqual(410) + expect(response.body).toEqual({ message: 'Form has been archived' }) + }) + + it('should return 422 when user in session cannot be found in the database', async () => { + // Arrange + const MOCK_FORM = await EmailFormModel.create({ + title: 'Form to retrieve', + emails: [defaultUser.email], + admin: defaultUser._id, + }) + const MOCK_FIELD = generateDefaultField(BasicField.Rating) + // Delete user after login. + await dbHandler.clearCollection(UserModel.collection.name) + + // Act + const response = await request.get( + `/admin/forms/${MOCK_FORM._id}/fields/${MOCK_FIELD._id}`, + ) + + // Assert + expect(response.status).toEqual(422) + expect(response.body).toEqual({ message: 'User not found' }) + }) + + it('should return 500 when database error occurs whilst archiving form', async () => { + // Arrange + const MOCK_FORM = await EmailFormModel.create({ + title: 'Form to retrieve', + emails: [defaultUser.email], + admin: defaultUser._id, + }) + const MOCK_FIELD = generateDefaultField(BasicField.Rating) + // Mock database error during retrieval. + jest + .spyOn(AdminFormService, 'getFormField') + .mockReturnValueOnce(err(new DatabaseError())) + + // Act + const response = await request.get( + `/admin/forms/${MOCK_FORM._id}/fields/${MOCK_FIELD._id}`, + ) + + // Assert + expect(response.status).toEqual(500) + expect(response.body).toEqual({ + message: 'Something went wrong. Please try again.', + }) + }) + }) }) diff --git a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.logic.routes.spec.ts b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.logic.routes.spec.ts index 6c5cdeadaa..f0797fdd6a 100644 --- a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.logic.routes.spec.ts +++ b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.logic.routes.spec.ts @@ -3,7 +3,7 @@ import mongoose from 'mongoose' import supertest, { Session } from 'supertest-session' import getUserModel from 'src/app/models/user.server.model' -import { ILogicSchema } from 'src/types' +import { ILogicSchema, LogicType } from 'src/types' import { createAuthedSession } from 'tests/integration/helpers/express-auth' import { setupApp } from 'tests/integration/helpers/express-setup' @@ -196,4 +196,221 @@ describe('admin-form.logic.routes', () => { expect(response.body).toEqual(expectedResponse) }) }) + + describe('PUT /forms/:formId/logic/:logicId', () => { + it('should return 200 on successful form logic update for email mode form', async () => { + // Arrange + const formLogicId = new ObjectId() + const { form: formToUpdate, user } = await dbHandler.insertEmailForm({ + formOptions: { + form_logics: [ + { + _id: formLogicId, + logicType: LogicType.ShowFields, + } as ILogicSchema, + ], + }, + }) + + const updatedLogic = ({ + _id: formLogicId, + logicType: LogicType.PreventSubmit, + conditions: [], + preventSubmitMessage: 'Some message', + } as unknown) as ILogicSchema + + const session = await createAuthedSession(user.email, request) + + // Act + const response = await session + .put(`/admin/forms/${formToUpdate._id}/logic/${formLogicId}`) + .send(updatedLogic) + + // Assert + expect(response.status).toEqual(200) + }) + + it('should return 200 on successful form logic update for encrypt mode form', async () => { + // Arrange + const formLogicId = new ObjectId() + const { form: formToUpdate, user } = await dbHandler.insertEncryptForm({ + formOptions: { + form_logics: [ + { + _id: formLogicId, + logicType: LogicType.ShowFields, + } as ILogicSchema, + ], + }, + }) + + const updatedLogic = ({ + _id: formLogicId, + logicType: LogicType.PreventSubmit, + conditions: [], + preventSubmitMessage: 'Some message', + } as unknown) as ILogicSchema + + const session = await createAuthedSession(user.email, request) + + // Act + const response = await session + .put(`/admin/forms/${formToUpdate._id}/logic/${formLogicId}`) + .send(updatedLogic) + + // Assert + expect(response.status).toEqual(200) + }) + + it('should return 403 when current user does not have permissions to update form logic', async () => { + // Arrange + const formLogicId = new ObjectId() + const { form: formToUpdate, agency } = await dbHandler.insertEncryptForm({ + formOptions: { + form_logics: [ + { + _id: formLogicId, + } as ILogicSchema, + ], + }, + }) + + const updatedLogic = ({ + _id: formLogicId, + logicType: LogicType.PreventSubmit, + conditions: [], + preventSubmitMessage: 'Some message', + } as unknown) as ILogicSchema + + const diffUser = await dbHandler.insertUser({ + mailName: 'newUser', + agencyId: agency._id, + }) + // Log in as different user. + const session = await createAuthedSession(diffUser.email, request) + + // Act + const response = await session + .put(`/admin/forms/${formToUpdate._id}/logic/${formLogicId}`) + .send(updatedLogic) + + // Assert + expect(response.status).toEqual(403) + expect(response.body).toEqual({ + message: expect.stringContaining( + 'not authorized to perform write operation', + ), + }) + }) + + it('should return 404 with error message if logicId does not exist', async () => { + // Arrange + const formLogicId = new ObjectId() + const wrongLogicId = new ObjectId() + const { form: formToUpdate, user } = await dbHandler.insertEmailForm({ + formOptions: { + form_logics: [ + { + _id: formLogicId, + } as ILogicSchema, + ], + }, + }) + + const updatedLogic = ({ + _id: wrongLogicId, + logicType: LogicType.PreventSubmit, + conditions: [], + preventSubmitMessage: 'Some message', + } as unknown) as ILogicSchema + + const session = await createAuthedSession(user.email, request) + + // Act + const response = await session + .put(`/admin/forms/${formToUpdate._id}/logic/${wrongLogicId}`) + .send(updatedLogic) + + // Assert + const expectedResponse = { + message: 'logicId does not exist on form', + } + expect(response.status).toEqual(404) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 404 with error message if form does not exist', async () => { + // Arrange + const formLogicId = new ObjectId() + const { user } = await dbHandler.insertEmailForm({ + formOptions: { + form_logics: [ + { + _id: formLogicId, + } as ILogicSchema, + ], + }, + }) + + const updatedLogic = ({ + _id: formLogicId, + logicType: LogicType.PreventSubmit, + conditions: [], + preventSubmitMessage: 'Some message', + } as unknown) as ILogicSchema + + const session = await createAuthedSession(user.email, request) + + // Act + const wrongFormId = new ObjectId() + const response = await session + .put(`/admin/forms/${wrongFormId}/logic/${formLogicId}`) + .send(updatedLogic) + + // Assert + const expectedResponse = { + message: 'Form not found', + } + expect(response.status).toEqual(404) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 422 when userId cannot be found in the database', async () => { + // Arrange + const formLogicId = new ObjectId() + const { form: formToUpdate, user } = await dbHandler.insertEmailForm({ + formOptions: { + form_logics: [ + { + _id: formLogicId, + } as ILogicSchema, + ], + }, + }) + + const updatedLogic = ({ + _id: formLogicId, + logicType: LogicType.PreventSubmit, + conditions: [], + preventSubmitMessage: 'Some message', + } as unknown) as ILogicSchema + + const session = await createAuthedSession(user.email, request) + + // Delete user after login. + await dbHandler.clearCollection(UserModel.collection.name) + + // Act + const response = await session + .put(`/admin/forms/${formToUpdate._id}/logic/${formLogicId}`) + .send(updatedLogic) + + // Assert + const expectedResponse = { + message: 'User not found', + } + expect(response.status).toEqual(422) + expect(response.body).toEqual(expectedResponse) + }) + }) }) diff --git a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.settings.routes.spec.ts b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.settings.routes.spec.ts index c6efd1968c..45ae661a3e 100644 --- a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.settings.routes.spec.ts +++ b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.settings.routes.spec.ts @@ -1,8 +1,10 @@ import { ObjectId } from 'bson-ext' import mongoose from 'mongoose' +import { errAsync } from 'neverthrow' import supertest, { Session } from 'supertest-session' import getUserModel from 'src/app/models/user.server.model' +import { DatabaseError } from 'src/app/modules/core/core.errors' import { Status } from 'src/types' import { SettingsUpdateDto } from 'src/types/api' @@ -10,7 +12,9 @@ import { createAuthedSession } from 'tests/integration/helpers/express-auth' import { setupApp } from 'tests/integration/helpers/express-setup' import { buildCelebrateError } from 'tests/unit/backend/helpers/celebrate' import dbHandler from 'tests/unit/backend/helpers/jest-db' +import { jsonParseStringify } from 'tests/unit/backend/helpers/serialize-data' +import * as UserService from '../../../../../../modules/user/user.service' import { AdminFormsRouter } from '../admin-forms.routes' const UserModel = getUserModel(mongoose) @@ -34,7 +38,7 @@ describe('admin-form.settings.routes', () => { }) afterAll(async () => await dbHandler.closeDatabase()) - describe('PATCH /admin/forms/:formId/settings', () => { + describe('PUT /admin/forms/:formId/settings', () => { it('should return 200 with latest form settings on successful update for email mode forms', async () => { // Arrange const { form: formToUpdate, user } = await dbHandler.insertEmailForm() @@ -265,4 +269,266 @@ describe('admin-form.settings.routes', () => { }) }) }) + + describe('PUT /admin/forms/:formId/collaborators', () => { + const MOCK_COLLABORATORS = [ + { + email: `fakeuser@test.gov.sg`, + write: false, + }, + ] + it('should return 200 when the collaborators are updated successfully', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm() + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify(MOCK_COLLABORATORS) + + // Act + const response = await session + .put(`/admin/forms/${form._id}/collaborators`) + .send(MOCK_COLLABORATORS) + + // Assert + expect(response.status).toEqual(200) + // NOTE: This is not strict equality because mongoose attaches an extra _id parameter + expect(response.body).toMatchObject(expectedResponse) + }) + + it('should return 403 when the current session user does not have sufficient permissions to update the collaborators', async () => { + // Arrange + const { form } = await dbHandler.insertEmailForm() + const fakeUser = await dbHandler.insertUser({ + mailName: 'fakeUser', + agencyId: new ObjectId(), + }) + const session = await createAuthedSession(fakeUser.email, request) + const expectedResponse = jsonParseStringify({ + message: `User ${fakeUser.email} not authorized to perform write operation on Form ${form._id} with title: ${form.title}.`, + }) + + // Act + const response = await session + .put(`/admin/forms/${form._id}/collaborators`) + .send(MOCK_COLLABORATORS) + + // Assert + expect(response.status).toEqual(403) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 404 when the form could not be found', async () => { + // Arrange + const { user } = await dbHandler.insertEmailForm() + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'Form not found', + }) + + // Act + const response = await session + .put(`/admin/forms/${new ObjectId().toHexString()}/collaborators`) + .send(MOCK_COLLABORATORS) + + // Assert + expect(response.status).toEqual(404) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 410 when the form has been archived', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm({ + formOptions: { + status: Status.Archived, + }, + }) + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'Form has been archived', + }) + + // Act + const response = await session + .put(`/admin/forms/${form._id}/collaborators`) + .send(MOCK_COLLABORATORS) + + // Assert + expect(response.status).toEqual(410) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 422 when the current session user cannot be retrieved', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm() + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'User not found', + }) + await dbHandler.clearCollection(UserModel.collection.name) + + // Act + const response = await session + .put(`/admin/forms/${form._id}/collaborators`) + .send(MOCK_COLLABORATORS) + + // Assert + expect(response.status).toEqual(422) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 500 when a database error occurs', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm() + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'Something went wrong. Please try again.', + }) + jest + .spyOn(UserService, 'getPopulatedUserById') + .mockReturnValueOnce(errAsync(new DatabaseError())) + + // Act + const response = await session + .put(`/admin/forms/${form._id}/collaborators`) + .send(MOCK_COLLABORATORS) + + // Assert + expect(response.status).toEqual(500) + expect(response.body).toEqual(expectedResponse) + }) + }) + + describe('GET /admin/forms/:formId/collaborators', () => { + const MOCK_COLLABORATORS = [ + { + email: `fakeuser@test.gov.sg`, + write: false, + }, + ] + it('should return the list of collaborators on a valid request', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm({ + formOptions: { + permissionList: MOCK_COLLABORATORS, + }, + }) + const session = await createAuthedSession(user.email, request) + + // Act + const response = await session.get( + `/admin/forms/${form._id}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(200) + expect(response.body).toMatchObject( + jsonParseStringify(MOCK_COLLABORATORS), + ) + }) + + it('should return 403 when the current user does not have read permissions for the specified form', async () => { + // Arrange + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + permissionList: MOCK_COLLABORATORS, + }, + }) + const fakeUser = await dbHandler.insertUser({ + mailName: 'fakeUser', + agencyId: new ObjectId(), + }) + const session = await createAuthedSession(fakeUser.email, request) + const expectedResponse = jsonParseStringify({ + message: `User ${fakeUser.email} not authorized to perform read operation on Form ${form._id} with title: ${form.title}.`, + }) + + // Act + const response = await session.get( + `/admin/forms/${form._id}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(403) + expect(response.body).toMatchObject(jsonParseStringify(expectedResponse)) + }) + + it('should return 404 when the form could not be found', async () => { + // Arrange + const { user } = await dbHandler.insertEmailForm() + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'Form not found', + }) + + // Act + const response = await session.get( + `/admin/forms/${new ObjectId().toHexString()}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(404) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 410 when the form has been archived', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm({ + formOptions: { + status: Status.Archived, + }, + }) + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'Form has been archived', + }) + + // Act + const response = await session.get( + `/admin/forms/${form._id}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(410) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 422 when the current session user cannot be retrieved', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm() + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'User not found', + }) + await dbHandler.clearCollection(UserModel.collection.name) + + // Act + const response = await session.get( + `/admin/forms/${form._id}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(422) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 500 when a database error occurs', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm() + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'Something went wrong. Please try again.', + }) + jest + .spyOn(UserService, 'getPopulatedUserById') + .mockReturnValueOnce(errAsync(new DatabaseError())) + + // Act + const response = await session.get( + `/admin/forms/${form._id}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(500) + expect(response.body).toEqual(expectedResponse) + }) + }) }) diff --git a/src/app/routes/api/v3/admin/forms/admin-forms.form.routes.ts b/src/app/routes/api/v3/admin/forms/admin-forms.form.routes.ts index 948b75db8a..bdf822be16 100644 --- a/src/app/routes/api/v3/admin/forms/admin-forms.form.routes.ts +++ b/src/app/routes/api/v3/admin/forms/admin-forms.form.routes.ts @@ -128,6 +128,20 @@ AdminFormsFormRouter.route( * @returns 500 when database error occurs during deletion */ .delete(AdminFormController.handleDeleteFormField) + /** + * Retrives the form field using the fieldId from the specified form + * @route GET /admin/forms/:formId/fields/:fieldId + * @security session + * + * @returns 200 with form field when retrieval is successful + * @returns 403 when current user does not have permissions to retrieve form field + * @returns 404 when form cannot be found + * @returns 404 when form field cannot be found + * @returns 410 when retrieving form field of an archived form + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ + .get(AdminFormController.handleGetFormField) AdminFormsFormRouter.post( '/:formId([a-fA-F0-9]{24})/fields/:fieldId([a-fA-F0-9]{24})/reorder', @@ -138,3 +152,8 @@ AdminFormsFormRouter.post( '/:formId([a-fA-F0-9]{24})/fields', AdminFormController.handleCreateFormField, ) + +AdminFormsFormRouter.put( + '/:formId([a-fA-F0-9]{24})/end-page', + AdminFormController.handleUpdateEndPage, +) diff --git a/src/app/routes/api/v3/admin/forms/admin-forms.logic.routes.ts b/src/app/routes/api/v3/admin/forms/admin-forms.logic.routes.ts index 595c89671f..b914d01e06 100644 --- a/src/app/routes/api/v3/admin/forms/admin-forms.logic.routes.ts +++ b/src/app/routes/api/v3/admin/forms/admin-forms.logic.routes.ts @@ -19,3 +19,19 @@ export const AdminFormsLogicRouter = Router() AdminFormsLogicRouter.route( '/:formId([a-fA-F0-9]{24})/logic/:logicId([a-fA-F0-9]{24})', ).delete(AdminFormController.handleDeleteLogic) + +/** + * Updates a logic. + * @route PUT /admin/forms/:formId/logic/:logicId + * @group admin + * @produces application/json + * @consumes application/json + * @returns 200 with success message when successfully updated + * @returns 403 when user does not have permissions to update logic + * @returns 404 when form cannot be found + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ +AdminFormsLogicRouter.route( + '/:formId([a-fA-F0-9]{24})/logic/:logicId([a-fA-F0-9]{24})', +).put(AdminFormController.handleUpdateLogic) diff --git a/src/app/routes/api/v3/admin/forms/admin-forms.settings.routes.ts b/src/app/routes/api/v3/admin/forms/admin-forms.settings.routes.ts index 9880aa6a61..5768e55e89 100644 --- a/src/app/routes/api/v3/admin/forms/admin-forms.settings.routes.ts +++ b/src/app/routes/api/v3/admin/forms/admin-forms.settings.routes.ts @@ -3,10 +3,7 @@ import { Router } from 'express' import { AuthType, Status } from '../../../../../../types' import { SettingsUpdateDto } from '../../../../../../types/api' -import { - handleGetSettings, - handleUpdateSettings, -} from '../../../../../modules/form/admin-form/admin-form.controller' +import * as AdminFormController from '../../../../../modules/form/admin-form/admin-form.controller' export const AdminFormsSettingsRouter = Router() @@ -52,7 +49,7 @@ AdminFormsSettingsRouter.route('/:formId([a-fA-F0-9]{24})/settings') * @returns 422 when user in session cannot be retrieved from the database * @returns 500 when database error occurs */ - .patch(updateSettingsValidator, handleUpdateSettings) + .patch(updateSettingsValidator, AdminFormController.handleUpdateSettings) /** * Retrieve the settings of the specified form * @route GET /admin/forms/:formId/settings @@ -65,4 +62,37 @@ AdminFormsSettingsRouter.route('/:formId([a-fA-F0-9]{24})/settings') * @returns 409 when saving form settings incurs a conflict in the database * @returns 500 when database error occurs */ - .get(handleGetSettings) + .get(AdminFormController.handleGetSettings) + +AdminFormsSettingsRouter.route('/:formId([a-fA-F0-9]{24})/collaborators') + /** + * Updates the collaborator list for a given formId + * @route PUT /admin/forms/:formId/collaborators + * @group admin + * @precondition Must be preceded by request validation + * @security session + * + * @returns 200 with updated collaborators and permissions + * @returns 403 when current user does not have permissions to update the collaborators + * @returns 404 when form cannot be found + * @returns 410 when updating collaborators for an archived form + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ + .put(AdminFormController.handleUpdateCollaborators) + /** + * Retrieves the collaborators for a given formId + * @route GET /admin/forms/:formId/collaborators + * @group admin + * @precondition Must be preceded by request validation + * @security session + + * + * @returns 200 with collaborators + * @returns 403 when current user does not have read permissions for the form + * @returns 404 when form cannot be found + * @returns 410 when retrieving collaborators for an archived form + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ + .get(AdminFormController.handleGetFormCollaborators) diff --git a/src/server.ts b/src/app/server.ts similarity index 75% rename from src/server.ts rename to src/app/server.ts index b72a87aa7b..9b3de0eabd 100755 --- a/src/server.ts +++ b/src/app/server.ts @@ -1,6 +1,6 @@ -import config from './app/config/config' -import { createLoggerWithLabel } from './app/config/logger' -import loadApp from './app/loaders' +import config from './config/config' +import { createLoggerWithLabel } from './config/logger' +import loadApp from './loaders' const logger = createLoggerWithLabel(module) diff --git a/src/app/utils/hash.ts b/src/app/utils/hash.ts index 4e4e840375..ed93fd501b 100644 --- a/src/app/utils/hash.ts +++ b/src/app/utils/hash.ts @@ -26,7 +26,7 @@ export class HashingError extends ApplicationError { * @returns err(ApplicationError) if hashing error occurs */ export const hashData = ( - dataToHash: unknown, + dataToHash: string | Buffer, logMeta: Record = {}, saltRounds?: number, ): ResultAsync => { @@ -56,7 +56,7 @@ export const hashData = ( * @returns err(ApplicationError) if error occurs whilst comparing hashes */ export const compareHash = ( - data: unknown, + data: string | Buffer, encrypted: string, logMeta: Record = {}, ): ResultAsync => { diff --git a/src/public/modules/core/componentViews/sg-govt-banner.html b/src/public/modules/core/componentViews/sg-govt-banner.html index 55c98b8e49..3cd8b73b0b 100644 --- a/src/public/modules/core/componentViews/sg-govt-banner.html +++ b/src/public/modules/core/componentViews/sg-govt-banner.html @@ -1,4 +1,8 @@ -
+
diff --git a/src/public/modules/core/components/sg-govt-banner.client.component.js b/src/public/modules/core/components/sg-govt-banner.client.component.js index f9aef80dc8..13568e2912 100644 --- a/src/public/modules/core/components/sg-govt-banner.client.component.js +++ b/src/public/modules/core/components/sg-govt-banner.client.component.js @@ -2,14 +2,16 @@ angular.module('core').component('sgGovtBannerComponent', { templateUrl: 'modules/core/componentViews/sg-govt-banner.html', - controller: ['$location', SgGovtBannerController], + controller: ['$location', '$attrs', SgGovtBannerController], controllerAs: 'vm', }) -function SgGovtBannerController($location) { +function SgGovtBannerController($location, $attrs) { const vm = this // Only show banner if the website is a Singapore Government Website. // See https://www.designsystem.gov.sg/docs/masthead/. vm.showBanner = $location.host().includes('.gov.sg') + // The x-padding-small attribute maps to paddingSmall instead of xPaddingSmall. + vm.paddingSmall = Object.prototype.hasOwnProperty.call($attrs, 'paddingSmall') } diff --git a/src/public/modules/core/css/sg-govt-banner.css b/src/public/modules/core/css/sg-govt-banner.css index ba0bf6a0f4..12359b4b10 100644 --- a/src/public/modules/core/css/sg-govt-banner.css +++ b/src/public/modules/core/css/sg-govt-banner.css @@ -70,6 +70,12 @@ background-color: #f0f0f0; } +#sg-govt-banner.banner-x-padding-small { + padding-left: 4px; + padding-right: 4px; + background-color: #f0f0f0; +} + @media screen and (max-width: 992px) { #sg-govt-banner { padding: 0; diff --git a/src/public/modules/forms/admin/controllers/admin-form.client.controller.js b/src/public/modules/forms/admin/controllers/admin-form.client.controller.js index 85c2d9993c..d8e3c5965a 100644 --- a/src/public/modules/forms/admin/controllers/admin-form.client.controller.js +++ b/src/public/modules/forms/admin/controllers/admin-form.client.controller.js @@ -151,6 +151,7 @@ function AdminFormController( errorMessage = 'This page seems outdated, and your changes could not be saved. Please refresh.' break + case StatusCodes.FORBIDDEN: case StatusCodes.UNAUTHORIZED: errorMessage = 'Your changes could not be saved as your account lacks the requisite privileges.' @@ -284,6 +285,15 @@ function AdminFormController( } } + $scope.updateFormEndPage = (newEndPage) => { + return $q + .when(AdminFormService.updateFormEndPage($scope.myform._id, newEndPage)) + .then((updatedEndPage) => { + $scope.myform.endPage = updatedEndPage + }) + .catch(handleUpdateError) + } + /** * Calls the update form settings API * @param {Object} settingsToUpdate the object with new values for settings. diff --git a/src/public/modules/forms/admin/controllers/collaborator-modal.client.controller.js b/src/public/modules/forms/admin/controllers/collaborator-modal.client.controller.js index 0e2fd74c54..797c0419f4 100644 --- a/src/public/modules/forms/admin/controllers/collaborator-modal.client.controller.js +++ b/src/public/modules/forms/admin/controllers/collaborator-modal.client.controller.js @@ -1,10 +1,13 @@ 'use strict' -const HttpStatus = require('http-status-codes') +const { get } = require('lodash') +const { StatusCodes } = require('http-status-codes') +const AdminFormService = require('../../../../services/AdminFormService') angular .module('forms') .controller('CollaboratorModalController', [ + '$q', '$scope', '$timeout', '$uibModalInstance', @@ -27,6 +30,7 @@ const ROLES = { } function CollaboratorModalController( + $q, $scope, $timeout, $uibModalInstance, @@ -88,22 +92,18 @@ function CollaboratorModalController( } /** - * Calls FormAPI update to update the permission list of a form + * Calls AdminFormService to update the permission list (collaborators) of a form * @param {Array} permissionList - New permission list for the form */ $scope.updatePermissionList = (permissionList) => { - return FormApi.update( - { formId: $scope.myform._id }, - { form: { permissionList } }, - ) - .$promise.then((savedForm) => { - $scope.myform = savedForm + return $q + .when( + AdminFormService.updateCollaborators($scope.myform._id, permissionList), + ) + .then((updatedCollaborators) => { + $scope.myform.permissionList = updatedCollaborators externalScope.refreshFormDataFromCollab($scope.myform) }) - .catch((err) => { - Toastr.error(err.data.message) - return err - }) } /** @@ -121,7 +121,18 @@ function CollaboratorModalController( let { write } = $scope.roleToPermissions(newRole) let permissionList = _.cloneDeep($scope.myform.permissionList) permissionList[index].write = write - $scope.updatePermissionList(permissionList) + $scope.updatePermissionList(permissionList).catch((err) => { + // NOTE: Refer to https://axios-http.com/docs/handling_errors + // Axios errors are wrapped in 2 layers of indirection, which means the actual message on the error has to be extracted manually + Toastr.error( + get( + err, + 'response.data.message', + 'Sorry, an error occurred. Please refresh the page and try again later.', + ), + ) + return err + }) } } @@ -135,7 +146,18 @@ function CollaboratorModalController( (user) => user.email.toLowerCase() !== email.toLowerCase(), ), ) - $scope.updatePermissionList(permissionList) + $scope.updatePermissionList(permissionList).catch((err) => { + // NOTE: Refer to https://axios-http.com/docs/handling_errors + // Axios errors are wrapped in 2 layers of indirection, which means the actual message on the error has to be extracted manually + Toastr.error( + get( + err, + 'response.data.message', + 'Sorry, an error occurred. Please refresh the page and try again later.', + ), + ) + return err + }) } /** @@ -215,27 +237,30 @@ function CollaboratorModalController( ) $scope.btnStatus = 2 // pressed; loading - $scope.updatePermissionList(permissionList).then((err) => { - if (err) { + $scope + .updatePermissionList(permissionList) + .then(() => { + // If no error, clear email input + $scope.btnStatus = 3 // pressed; saved + $scope.closeEditCollaboratorDropdowns() + + $timeout(() => { + resetCollabForm() + }, 1000) + }) + .catch((err) => { // Make the alert message correspond to the error code - if (err.status === HttpStatus.BAD_REQUEST) { - Toastr.error('Outdated admin page, please refresh.') - } else if (err.status === HttpStatus.UNPROCESSABLE_ENTITY) { + if (err.response.status === StatusCodes.BAD_REQUEST) { + Toastr.error( + 'Please ensure that the email entered is a valid government email. If the error still persists, refresh and try again later.', + ) + } else if (err.response.status === StatusCodes.UNPROCESSABLE_ENTITY) { Toastr.error(`${email} is not part of a whitelisted agency.`) } else { Toastr.error('Error adding collaborator.') } resetCollabForm() - return - } - // If no error, clear email input - $scope.btnStatus = 3 // pressed; saved - $scope.closeEditCollaboratorDropdowns() - - $timeout(() => { - resetCollabForm() - }, 1000) - }) + }) } /** diff --git a/src/public/modules/forms/admin/controllers/edit-end-page-modal.client.controller.js b/src/public/modules/forms/admin/controllers/edit-end-page-modal.client.controller.js index b2e1e04ec1..f6c6c8890a 100644 --- a/src/public/modules/forms/admin/controllers/edit-end-page-modal.client.controller.js +++ b/src/public/modules/forms/admin/controllers/edit-end-page-modal.client.controller.js @@ -7,11 +7,11 @@ angular .controller('EditEndPageController', [ '$uibModalInstance', 'myform', - 'updateField', + 'updateEndPage', EditEndPageController, ]) -function EditEndPageController($uibModalInstance, myform, updateField) { +function EditEndPageController($uibModalInstance, myform, updateEndPage) { const vm = this vm.logoUrl = getFormLogo(myform) @@ -31,33 +31,6 @@ function EditEndPageController($uibModalInstance, myform, updateField) { vm.myform.endPage.buttonText = '' } - vm.showButtons = false - vm.lastButtonID = 0 - - // add new Button to the endPage - vm.addButton = function () { - let newButton = {} - newButton.bgColor = '#ddd' - newButton.color = '#ffffff' - newButton.text = 'Button' - newButton._id = Math.floor(100000 * Math.random()) - - vm.myform.endPage.buttons.push(newButton) - } - - // delete particular Button from endPage - vm.deleteButton = function (button) { - let currID - for (let i = 0; i < vm.myform.endPage.buttons.length; i++) { - currID = vm.myform.endPage.buttons[i]._id - - if (currID === button._id) { - vm.myform.endPage.buttons.splice(i, 1) - break - } - } - } - vm.saveEndPage = function (isValid) { if (isValid) { // Check if http(s):// is appended, if not append it @@ -80,7 +53,7 @@ function EditEndPageController($uibModalInstance, myform, updateField) { vm.myform.endPage.buttonLink = inputLink - updateField({ endPage: vm.myform.endPage }).then((error) => { + updateEndPage({ newEndPage: vm.myform.endPage }).then((error) => { if (!error) { $uibModalInstance.close() } diff --git a/src/public/modules/forms/admin/controllers/edit-logic-modal.client.controller.js b/src/public/modules/forms/admin/controllers/edit-logic-modal.client.controller.js index d3a817f195..3be8cff745 100644 --- a/src/public/modules/forms/admin/controllers/edit-logic-modal.client.controller.js +++ b/src/public/modules/forms/admin/controllers/edit-logic-modal.client.controller.js @@ -3,6 +3,7 @@ const { range } = require('lodash') const { LogicType } = require('../../../../../types') const FormLogic = require('../../services/form-logic/form-logic.client.service') +const AdminFormService = require('../../../../services/AdminFormService') angular .module('forms') @@ -11,6 +12,8 @@ angular 'externalScope', 'updateLogic', 'FormFields', + '$q', + 'Toastr', EditLogicModalController, ]) @@ -19,6 +22,8 @@ function EditLogicModalController( externalScope, updateLogic, FormFields, + $q, + Toastr, ) { const vm = this @@ -263,19 +268,40 @@ function EditLogicModalController( if (isNew) { vm.formLogics.push(vm.logic) + updateLogic({ form_logics: vm.formLogics }).then((error) => { + if (!error) { + $uibModalInstance.close() + } + }) // Not new, and logic index is provided } else if (logicIndex !== -1) { - vm.formLogics[logicIndex] = vm.logic + vm.updateExistingLogic(logicIndex, vm.logic) } - - updateLogic({ form_logics: vm.formLogics }).then((error) => { - if (!error) { - $uibModalInstance.close() - } - }) } vm.cancel = function () { $uibModalInstance.close() } + + vm.updateExistingLogic = function (logicIndex, updatedLogic) { + const logicIdToUpdate = vm.formLogics[logicIndex]._id + $q.when( + AdminFormService.updateFormLogic( + vm.myform._id, + logicIdToUpdate, + updatedLogic, + ), + ) + .then((updatedLogic) => { + const updatedFormLogics = [...vm.formLogics] + updatedFormLogics[logicIndex] = updatedLogic + vm.formLogics = updatedFormLogics + externalScope.myform.form_logics = updatedFormLogics // update global myform + $uibModalInstance.close() + }) + .catch((logicUpdateError) => { + console.error(logicUpdateError) + Toastr.error('Failed to update logic, please refresh and try again!') + }) + } } diff --git a/src/public/modules/forms/admin/directiveViews/edit-form.client.view.html b/src/public/modules/forms/admin/directiveViews/edit-form.client.view.html index 5f400ca260..16a3bb35a9 100644 --- a/src/public/modules/forms/admin/directiveViews/edit-form.client.view.html +++ b/src/public/modules/forms/admin/directiveViews/edit-form.client.view.html @@ -235,7 +235,6 @@ Only {{ maxMyInfoFields }} MyInfo fields are allowed in Email mode. You currently have {{ numMyInfoFields }} MyInfo field(s). - Learn more
diff --git a/src/public/modules/forms/admin/directives/edit-form.client.directive.js b/src/public/modules/forms/admin/directives/edit-form.client.directive.js index 25a0012df2..81caa1619f 100644 --- a/src/public/modules/forms/admin/directives/edit-form.client.directive.js +++ b/src/public/modules/forms/admin/directives/edit-form.client.directive.js @@ -28,6 +28,7 @@ function editFormDirective() { scope: { myform: '=', updateForm: '&', + updateFormEndPage: '&', }, controller: [ '$scope', @@ -329,7 +330,7 @@ function editFormController( controllerAs: 'vm', resolve: { myform: () => $scope.myform, - updateField: () => updateField, + updateEndPage: () => $scope.updateFormEndPage, }, }) } diff --git a/src/public/modules/forms/admin/views/edit-end-page.client.modal.html b/src/public/modules/forms/admin/views/edit-end-page.client.modal.html index adabf099e9..7063bb7315 100644 --- a/src/public/modules/forms/admin/views/edit-end-page.client.modal.html +++ b/src/public/modules/forms/admin/views/edit-end-page.client.modal.html @@ -63,10 +63,20 @@
+
+ + Please enter a valid URL +
diff --git a/src/public/modules/forms/base/directives/validate-url.client.directive.js b/src/public/modules/forms/base/directives/validate-url.client.directive.js index ce781187f5..87de3c92a1 100644 --- a/src/public/modules/forms/base/directives/validate-url.client.directive.js +++ b/src/public/modules/forms/base/directives/validate-url.client.directive.js @@ -1,6 +1,9 @@ 'use strict' -const { isValidHttpsUrl } = require('../../../../../shared/util/url-validation') +const { + isValidHttpsUrl, + isValidUrl, +} = require('../../../../../shared/util/url-validation') angular.module('forms').directive('validateUrl', validateUrl) @@ -8,9 +11,13 @@ function validateUrl() { return { restrict: 'A', require: 'ngModel', - link: function (_scope, _elem, _attrs, ctrl) { - ctrl.$validators.urlValidator = (modelValue) => - ctrl.$isEmpty(modelValue) || isValidHttpsUrl(modelValue) + link: function (_scope, _elem, attrs, ctrl) { + ctrl.$validators.urlValidator = (modelValue) => { + if (attrs.allowHttp) { + return ctrl.$isEmpty(modelValue) || isValidUrl(modelValue) + } + return ctrl.$isEmpty(modelValue) || isValidHttpsUrl(modelValue) + } }, } } diff --git a/src/public/modules/forms/base/views/submit-form.client.view.html b/src/public/modules/forms/base/views/submit-form.client.view.html index 7580166554..b35c730180 100644 --- a/src/public/modules/forms/base/views/submit-form.client.view.html +++ b/src/public/modules/forms/base/views/submit-form.client.view.html @@ -1,3 +1,4 @@ + diff --git a/src/public/modules/forms/config/forms.client.routes.js b/src/public/modules/forms/config/forms.client.routes.js index 5f5467c213..6a385d9c84 100644 --- a/src/public/modules/forms/config/forms.client.routes.js +++ b/src/public/modules/forms/config/forms.client.routes.js @@ -133,7 +133,8 @@ angular.module('forms').config([ 'build@viewForm': { template: ` + update-form="updateForm(update)" + update-form-end-page="updateFormEndPage(newEndPage)"> `, }, 'logic@viewForm': { diff --git a/src/public/services/AdminFormService.ts b/src/public/services/AdminFormService.ts index 98e4c56033..23c655ab80 100644 --- a/src/public/services/AdminFormService.ts +++ b/src/public/services/AdminFormService.ts @@ -1,10 +1,12 @@ import axios from 'axios' -import { FormSettings } from '../../types' +import { FormSettings, LogicDto } from '../../types' import { + EndPageUpdateDto, FieldCreateDto, FieldUpdateDto, FormFieldDto, + PermissionsUpdateDto, SettingsUpdateDto, } from '../../types/api' @@ -22,6 +24,15 @@ export const updateFormSettings = async ( .then(({ data }) => data) } +export const getSingleFormField = async ( + formId: string, + fieldId: string, +): Promise => { + return axios + .get(`${ADMIN_FORM_ENDPOINT}/${formId}/fields/${fieldId}`) + .then(({ data }) => data) +} + export const updateSingleFormField = async ( formId: string, fieldId: string, @@ -47,6 +58,18 @@ export const createSingleFormField = async ( .then(({ data }) => data) } +export const updateCollaborators = async ( + formId: string, + collaboratorsToUpdate: PermissionsUpdateDto, +): Promise => { + return axios + .put( + `${ADMIN_FORM_ENDPOINT}/${formId}/collaborators`, + collaboratorsToUpdate, + ) + .then(({ data }) => data) +} + /** * Reorders the field to the given new position. * @param formId the id of the form to perform the field reorder @@ -70,7 +93,7 @@ export const reorderSingleFormField = async ( /** * Delete a single form field by its id in given form - * @param formId the form to delete the field from + * @param formId the id of the form to delete the field from * @param fieldId the id of the field to delete * @returns void on success */ @@ -81,6 +104,24 @@ export const deleteSingleFormField = async ( return axios.delete(`${ADMIN_FORM_ENDPOINT}/${formId}/fields/${fieldId}`) } +/** + * Updates the end page for the given form referenced by its id + * @param formId the id of the form to update end page for + * @param newEndPage the new endpage to replace with + * @returns the updated end page on success + */ +export const updateFormEndPage = async ( + formId: string, + newEndPage: EndPageUpdateDto, +): Promise => { + return axios + .put( + `${ADMIN_FORM_ENDPOINT}/${formId}/end-page`, + newEndPage, + ) + .then(({ data }) => data) +} + export const deleteFormLogic = async ( formId: string, logicId: string, @@ -89,3 +130,16 @@ export const deleteFormLogic = async ( .delete(`${ADMIN_FORM_ENDPOINT}/${formId}/logic/${logicId}`) .then(() => true) } + +export const updateFormLogic = async ( + formId: string, + logicId: string, + updatedLogic: LogicDto, +): Promise => { + return axios + .put( + `${ADMIN_FORM_ENDPOINT}/${formId}/logic/${logicId}`, + updatedLogic, + ) + .then(({ data }) => data) +} diff --git a/src/shared/util/url-validation.ts b/src/shared/util/url-validation.ts index 872bb35dc3..8d21a98778 100644 --- a/src/shared/util/url-validation.ts +++ b/src/shared/util/url-validation.ts @@ -5,6 +5,7 @@ import validator from 'validator' * @param url */ export const isValidHttpsUrl = (url: string): boolean => { + // TODO(#1788): Remove redundant type assertions once frontend is fully in Typescript. if (typeof url !== 'string') { return false } @@ -13,3 +14,18 @@ export const isValidHttpsUrl = (url: string): boolean => { require_protocol: true, }) } + +/** + * Checks that string is a valid HTTP or HTTPS URL. + * @param url the url to check + * @returns true if valid, false otherwise + */ +export const isValidUrl = (url: string): boolean => { + // TODO(#1788): Remove redundant type assertions once frontend is fully in Typescript. + if (typeof url !== 'string') { + return false + } + return validator.isURL(url, { + protocols: ['https', 'http'], + }) +} diff --git a/src/types/api/form.ts b/src/types/api/form.ts index 6892d28a89..bdeedf4fc6 100644 --- a/src/types/api/form.ts +++ b/src/types/api/form.ts @@ -2,7 +2,7 @@ import { LeanDocument } from 'mongoose' import { ConditionalPick, Primitive } from 'type-fest' import { FormField, FormFieldSchema, FormFieldWithId } from '../field' -import { FormSettings } from '../form' +import { EndPage, FormSettings, Permission } from '../form' export type SettingsUpdateDto = Partial @@ -17,3 +17,7 @@ export type FormFieldDto = ConditionalPick< LeanDocument, Primitive > + +export type PermissionsUpdateDto = Permission[] + +export type EndPageUpdateDto = EndPage diff --git a/src/types/form.ts b/src/types/form.ts index ad4ea873de..50bfb13a05 100644 --- a/src/types/form.ts +++ b/src/types/form.ts @@ -10,7 +10,7 @@ import { IFieldSchema, MyInfoAttribute, } from './field' -import { ILogicSchema } from './form_logic' +import { ILogicSchema, LogicDto } from './form_logic' import { FormLogoState, IFormLogo } from './form_logo' import { IPopulatedUser, IUserSchema, PublicUser } from './user' @@ -98,6 +98,7 @@ export type EndPage = { export type Permission = { email: string write: boolean + _id?: string } export type Webhook = { @@ -183,6 +184,11 @@ export interface IFormSchema extends IForm, Document, PublicView { newField: FormFieldWithId, ): Promise + updateFormCollaborators( + this: T, + updateFormCollaborators: Permission[], + ): Promise + /** * Reorders field corresponding to given fieldId to given newPosition * @param fieldId the id of the field to reorder @@ -311,6 +317,21 @@ export interface IFormModel extends Model { userId: IUserSchema['_id'], userEmail: IUserSchema['email'], ): Promise + /** + * Update the end page of form with given endpage object. + * @param formId the id of the form to update + * @param newEndPage the new EndPage object to replace with + * @returns the updated form document if form exists, null otherwise + */ + updateEndPageById( + formId: string, + newEndPage: EndPage, + ): Promise + updateFormLogic( + formId: string, + logicId: string, + updatedLogic: LogicDto, + ): Promise } export type IEncryptedFormModel = IFormModel & Model diff --git a/src/types/form_logic.ts b/src/types/form_logic.ts index 0a488b7d11..87d6f4d476 100644 --- a/src/types/form_logic.ts +++ b/src/types/form_logic.ts @@ -107,3 +107,8 @@ export type LogicCondition = | CategoricalLogicCondition | BinaryLogicCondition | NumericalLogicCondition + +/** + * Logic POJO with functions removed + */ +export type LogicDto = ILogic & { _id?: Document['_id'] } diff --git a/tests/unit/backend/helpers/generate-form-data.ts b/tests/unit/backend/helpers/generate-form-data.ts index 5be6395eac..2fcc97e109 100644 --- a/tests/unit/backend/helpers/generate-form-data.ts +++ b/tests/unit/backend/helpers/generate-form-data.ts @@ -31,6 +31,7 @@ import { IMobileFieldSchema, INumberField, IRatingField, + IRatingFieldSchema, IShortTextField, IShortTextFieldSchema, ISingleAnswerResponse, @@ -153,6 +154,15 @@ export const generateDefaultField = ( getQuestion: () => defaultParams.title, ...customParams, } as IHomenoFieldSchema + case BasicField.Rating: + return { + ...defaultParams, + ratingOptions: { + shape: 'Heart', + steps: 5, + }, + ...customParams, + } as IRatingFieldSchema default: return { ...defaultParams,